Responsive Mobile Dashboard

Response:
 ```css
/* Sunaiva/talking-widget/dashboard/css/mobile.css */

:root {
  --bg-primary: #0B0C0E;
  --bg-secondary: #15171A;
  --bg-tertiary: #1F2127;
  --accent-primary: #3B82F6;
  --accent-hover: #2563EB;
  --text-primary: #F1F5F9;
  --text-secondary: #94A3B8;
  --text-muted: #64748B;
  --border-color: #2D3139;
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
  --transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
  --touch-target: 44px;
}

/* Reset and Base Mobile Styles */
* {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

@media (max-width: 768px) {
  body {
    font-size: 16px;
    line-height: 1.5;
    background-color: var(--bg-primary);
    color: var(--text-primary);
    padding-bottom: 80px; /* Space for bottom nav */
    overflow-x: hidden;
  }

  /* Hamburger Menu Button */
  .hamburger-btn {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    width: var(--touch-target);
    height: var(--touch-target);
    background: transparent;
    border: none;
    cursor: pointer;
    padding: 12px;
    gap: 5px;
    z-index: 1000;
    position: fixed;
    top: 12px;
    left: 12px;
    border-radius: 8px;
    transition: background-color var(--transition-base);
  }

  .hamburger-btn:hover,
  .hamburger-btn:focus-visible {
    background-color: var(--bg-tertiary);
  }

  .hamburger-btn:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
  }

  .hamburger-line {
    display: block;
    width: 24px;
    height: 2px;
    background-color: var(--text-primary);
    transition: transform var(--transition-base), opacity var(--transition-base);
    border-radius: 2px;
  }

  .hamburger-btn[aria-expanded="true"] .hamburger-line:nth-child(1) {
    transform: rotate(45deg) translate(5px, 5px);
  }

  .hamburger-btn[aria-expanded="true"] .hamburger-line:nth-child(2) {
    opacity: 0;
  }

  .hamburger-btn[aria-expanded="true"] .hamburger-line:nth-child(3) {
    transform: rotate(-45deg) translate(5px, -5px);
  }

  /* Side Navigation Drawer */
  .nav-drawer {
    position: fixed;
    top: 0;
    left: 0;
    width: 280px;
    height: 100vh;
    background-color: var(--bg-secondary);
    transform: translateX(-100%);
    transition: transform var(--transition-base);
    z-index: 999;
    box-shadow: var(--shadow-lg);
    overflow-y: auto;
    border-right: 1px solid var(--border-color);
  }

  .nav-drawer[data-open="true"] {
    transform: translateX(0);
  }

  .nav-overlay {
    position: fixed;
    inset: 0;
    background-color: rgba(0, 0, 0, 0.7);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--transition-base), visibility var(--transition-base);
    z-index: 998;
    backdrop-filter: blur(4px);
  }

  .nav-overlay[data-visible="true"] {
    opacity: 1;
    visibility: visible;
  }

  .nav-header {
    padding: 24px 20px;
    border-bottom: 1px solid var(--border-color);
    display: flex;
    align-items: center;
    gap: 12px;
  }

  .nav-logo {
    width: 40px;
    height: 40px;
    background-color: var(--accent-primary);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: bold;
    color: white;
  }

  .nav-title {
    font-size: 1.25rem;
    font-weight: 600;
    color: var(--text-primary);
  }

  .nav-list {
    list-style: none;
    padding: 12px;
    margin: 0;
  }

  .nav-item {
    margin-bottom: 4px;
  }

  .nav-link {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    color: var(--text-secondary);
    text-decoration: none;
    border-radius: 8px;
    transition: all var(--transition-base);
    min-height: var(--touch-target);
  }

  .nav-link:hover,
  .nav-link:focus-visible {
    background-color: var(--bg-tertiary);
    color: var(--text-primary);
  }

  .nav-link:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: -2px;
  }

  .nav-link.active {
    background-color: rgba(59, 130, 246, 0.1);
    color: var(--accent-primary);
  }

  /* Main Content Adjustments */
  .main-content {
    margin-left: 0;
    padding: 80px 16px 24px;
    transition: transform var(--transition-base);
  }

  /* Header */
  .dashboard-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 64px;
    background-color: var(--bg-secondary);
    border-bottom: 1px solid var(--border-color);
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 16px 0 72px; /* Space for hamburger */
    z-index: 100;
  }

  .header-title {
    font-size: 1.25rem;
    font-weight: 600;
    color: var(--text-primary);
  }

  /* Stacked Analytics Cards */
  .analytics-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 16px;
    margin-bottom: 24px;
  }

  .analytics-card {
    background-color: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: 12px;
    padding: 20px;
    transition: transform var(--transition-base), box-shadow var(--transition-base);
  }

  .analytics-card:active {
    transform: scale(0.98);
  }

  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 16px;
  }

  .card-title {
    font-size: 0.875rem;
    color: var(--text-secondary);
    font-weight: 500;
    margin: 0;
  }

  .card-value {
    font-size: 1.75rem;
    font-weight: 700;
    color: var(--text-primary);
    margin: 8px 0;
  }

  .card-change {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 0.875rem;
    font-weight: 600;
    padding: 4px 8px;
    border-radius: 6px;
    background-color: var(--bg-tertiary);
  }

  .card-change.positive {
    color: #10B981;
  }

  .card-change.negative {
    color: #EF4444;
  }

  /* Touch-friendly Buttons */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: var(--touch-target);
    min-width: var(--touch-target);
    padding: 12px 20px;
    font-size: 0.9375rem;
    font-weight: 600;
    border-radius: 10px;
    border: none;
    cursor: pointer;
    transition: all var(--transition-base);
    touch-action: manipulation;
    user-select: none;
  }

  .btn-primary {
    background-color: var(--accent-primary);
    color: white;
  }

  .btn-primary:hover {
    background-color: var(--accent-hover);
  }

  .btn-primary:active {
    transform: scale(0.96);
  }

  .btn-secondary {
    background-color: var(--bg-tertiary);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
  }

  .btn-icon {
    padding: 12px;
    aspect-ratio: 1;
  }

  /* Swipeable Tables */
  .table-container {
    position: relative;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    -ms-overflow-style: none;
    margin: 0 -16px;
    padding: 0 16px;
  }

  .table-container::-webkit-scrollbar {
    display: none;
  }

  .table-wrapper {
    position: relative;
  }

  .swipe-indicator {
    position: absolute;
    right: 16px;
    top: 50%;
    transform: translateY(-50%);
    width: 40px;
    height: 40px;
    background: linear-gradient(to left, var(--bg-secondary), transparent);
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding-right: 8px;
    pointer-events: none;
    opacity: 0.6;
    animation: swipeHint 2s ease-in-out infinite;
  }

  @keyframes swipeHint {
    0%, 100% { transform: translateY(-50%) translateX(0); }
    50% { transform: translateY(-50%) translateX(-8px); }
  }

  .data-table {
    width: max-content;
    min-width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    background-color: var(--bg-secondary);
    border-radius: 12px;
    overflow: hidden;
  }

  .data-table th,
  .data-table td {
    padding: 16px;
    text-align: left;
    white-space: nowrap;
    border-bottom: 1px solid var(--border-color);
  }

  .data-table th {
    background-color: var(--bg-tertiary);
    font-weight: 600;
    color: var(--text-secondary);
    font-size: 0.875rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }

  .data-table td {
    color: var(--text-primary);
  }

  .data-table tr:last-child td {
    border-bottom: none;
  }

  /* Bottom Navigation Bar */
  .bottom-nav {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    height: 64px;
    background-color: var(--bg-secondary);
    border-top: 1px solid var(--border-color);
    display: flex;
    justify-content: space-around;
    align-items: center;
    z-index: 1000;
    padding: 0 8px;
    box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.3);
  }

  .bottom-nav-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    flex: 1;
    height: 100%;
    color: var(--text-secondary);
    text-decoration: none;
    font-size: 0.75rem;
    font-weight: 500;
    transition: color var(--transition-base);
    position: relative;
    min-width: var(--touch-target);
  }

  .bottom-nav-item::before {
    content: '';
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 0;
    height: 3px;
    background-color: var(--accent-primary);
    border-radius: 0 0 4px 4px;
    transition: width var(--transition-base);
  }

  .bottom-nav-item:hover,
  .bottom-nav-item:focus-visible {
    color: var(--text-primary);
  }

  .bottom-nav-item.active {
    color: var(--accent-primary);
  }

  .bottom-nav-item.active::before {
    width: 24px;
  }

  .bottom-nav-item:focus-visible {
    outline: none;
    background-color: var(--bg-tertiary);
  }

  .bottom-nav-icon {
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  /* Chart Optimizations */
  .chart-container {
    position: relative;
    width: 100%;
    height: 250px;
    background-color: var(--bg-secondary);
    border-radius: 12px;
    padding: 16px;
    margin-bottom: 16px;
  }

  .chart-canvas {
    width: 100% !important;
    height: 100% !important;
  }

  .chart-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 12px;
  }

  .chart-title {
    font-size: 1rem;
    font-weight: 600;
    color: var(--text-primary);
  }

  .chart-legend {
    display: flex;
    gap: 16px;
    flex-wrap: wrap;
    font-size: 0.875rem;
  }

  .legend-item {
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--text-secondary);
  }

  .legend-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background-color: var(--accent-primary);
  }

  /* Section Spacing */
  .section {
    margin-bottom: 32px;
  }

  .section-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
  }

  .section-title {
    font-size: 1.125rem;
    font-weight: 600;
    color: var(--text-primary);
    margin: 0;
  }

  /* Touch Feedback */
  @media (hover: none) {
    .btn:active,
    .analytics-card:active,
    .nav-link:active {
      opacity: 0.8;
    }
  }

  /* Reduced Motion */
  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
    }
  }

  /* Safe Area Support for Notched Devices */
  @supports (padding-bottom: env(safe-area-inset-bottom)) {
    body {
      padding-bottom: calc(80px + env(safe-area-inset-bottom));
    }

    .bottom-nav {
      padding-bottom: env(safe-area-inset-bottom);
      height: calc(64px + env(safe-area-inset-bottom));
    }
  }
}

/* Tablet Adjustments */
@media (min-width: 769px) and (max-width: 1024px) {
  .analytics-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  .chart-container {
    height: 300px;
  }
}
```

```javascript
// Sunaiva/talking-widget/dashboard/js/mobile.js

class MobileDashboard {
  constructor() {
    this.isMenuOpen = false;
    this.touchStartX = 0;
    this.touchEndX = 0;
    this.charts = new Map();

    this.init();
  }

  init() {
    this.initHamburgerMenu();
    this.initBottomNavigation();
    this.initSwipeableTables();
    this.initTouchFeedback();
    this.initKeyboardNavigation();
    this.initChartResizing();
    this.initFocusTrap();

    // Handle orientation change
    window.addEventListener('orientationchange', () => {
      setTimeout(() => this.handleResize(), 100);
    });
  }

  /**
   * Hamburger Menu Toggle with Accessibility
   */
  initHamburgerMenu() {
    this.hamburgerBtn = document.querySelector('.hamburger-btn');
    this.navDrawer = document.querySelector('.nav-drawer');
    this.navOverlay = document.querySelector('.nav-overlay');
    this.focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';

    if (!this.hamburgerBtn || !this.navDrawer) return;

    this.hamburgerBtn.addEventListener('click', () => this.toggleMenu());

    // Close on overlay click
    if (this.navOverlay) {
      this.navOverlay.addEventListener('click', () => this.closeMenu());
    }

    // Close on escape key
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape' && this.isMenuOpen) {
        this.closeMenu();
      }
    });
  }

  toggleMenu() {
    this.isMenuOpen = !this.isMenuOpen;
    this.hamburgerBtn.setAttribute('aria-expanded', this.isMenuOpen);
    this.navDrawer.setAttribute('data-open', this.isMenuOpen);

    if (this.navOverlay) {
      this.navOverlay.setAttribute('data-visible', this.isMenuOpen);
    }

    // Manage focus
    if (this.isMenuOpen) {
      this.firstFocusableElement = this.navDrawer.querySelectorAll(this.focusableElements)[0];
      this.focusableContent = this.navDrawer.querySelectorAll(this.focusableElements);
      this.lastFocusableElement = this.focusableContent[this.focusableContent.length - 1];

      document.body.style.overflow = 'hidden';
      setTimeout(() => this.firstFocusableElement?.focus(), 100);
    } else {
      document.body.style.overflow = '';
      this.hamburgerBtn.focus();
    }
  }

  closeMenu() {
    if (this.isMenuOpen) {
      this.toggleMenu();
    }
  }

  /**
   * Focus Trap for Navigation Drawer
   */
  initFocusTrap() {
    this.navDrawer?.addEventListener('keydown', (e) => {
      if (e.key !== 'Tab') return;

      if (e.shiftKey) {
        if (document.activeElement === this.firstFocusableElement) {
          this.lastFocusableElement.focus();
          e.preventDefault();
        }
      } else {
        if (document.activeElement === this.lastFocusableElement) {
          this.firstFocusableElement.focus();
          e.preventDefault();
        }
      }
    });
  }

  /**
   * Bottom Navigation Active State Management
   */
  initBottomNavigation() {
    const navItems = document.querySelectorAll('.bottom-nav-item');
    const currentPath = window.location.pathname;

    navItems.forEach(item => {
      const href = item.getAttribute('href');
      if (href && currentPath.includes(href)) {
        item.classList.add('active');
        item.setAttribute('aria-current', 'page');
      }

      // Touch feedback
      item.addEventListener('touchstart', () => {
        item.style.transform = 'scale(0.95)';
      }, { passive: true });

      item.addEventListener('touchend', () => {
        item.style.transform = '';
      }, { passive: true });
    });
  }

  /**
   * Swipeable Tables with Touch Events
   */
  initSwipeableTables() {
    const tables = document.querySelectorAll('.table-container');

    tables.forEach(container => {
      let isDown = false;
      let startX;
      let scrollLeft;

      // Mouse events
      container.addEventListener('mousedown', (e) => {
        isDown = true;
        container.style.cursor = 'grabbing';
        startX = e.pageX - container.offsetLeft;
        scrollLeft = container.scrollLeft;
      });

      container.addEventListener('mouseleave', () => {
        isDown = false;
        container.style.cursor = 'grab';
      });

      container.addEventListener('mouseup', () => {
        isDown = false;
        container.style.cursor = 'grab';
      });

      container.addEventListener('mousemove', (e) => {
        if (!isDown) return;
        e.preventDefault();
        const x = e.pageX - container.offsetLeft;
        const walk = (x - startX) * 2;
        container.scrollLeft = scrollLeft - walk;
      });

      // Touch events
      container.addEventListener('touchstart', (e) => {
        isDown = true;
        startX = e.touches[0].pageX - container.offsetLeft;
        scrollLeft = container.scrollLeft;
      }, { passive: true });

      container.addEventListener('touchend', () => {
        isDown = false;
      }, { passive: true });

      container.addEventListener('touchmove', (e) => {
        if (!isDown) return;
        const x = e.touches[0].pageX - container.offsetLeft;
        const walk = (x - startX) * 2;
        container.scrollLeft = scrollLeft - walk;
      }, { passive: true });

      // Hide swipe indicator when scrolled
      container.addEventListener('scroll', () => {
        const indicator = container.querySelector('.swipe-indicator');
        if (indicator && container.scrollLeft > 20) {
          indicator.style.opacity = '0';
        }
      }, { passive: true });
    });
  }

  /**
   * Touch Feedback for Buttons
   */
  initTouchFeedback() {
    const buttons = document.querySelectorAll('.btn, .analytics-card, [role="button"]');

    buttons.forEach(btn => {
      btn.addEventListener('touchstart', function() {
        this.style.transform = 'scale(0.98)';
      }, { passive: true });

      btn.addEventListener('touchend', function() {
        this.style.transform = '';
      }, { passive: true });
    });
  }

  /**
   * Keyboard Navigation Enhancement
   */
  initKeyboardNavigation() {
    // Skip to main content link
    const skipLink = document.createElement('a');
    skipLink.href = '#main-content';
    skipLink.textContent = 'Skip to main content';
    skipLink.style.cssText = `
      position: absolute;
      top: -40px;
      left: 0;
      background: var(--accent-primary);
      color: white;
      padding: 8px;
      text-decoration: none;
      z-index: 10000;
      border-radius: 0 0 4px 0;
    `;

    skipLink.addEventListener('focus', () => {
      skipLink.style.top = '0';
    });

    skipLink.addEventListener('blur', () => {
      skipLink.style.top = '-40px';
    });

    document.body.prepend(skipLink);

    // Focus management for cards
    const cards = document.querySelectorAll('.analytics-card');
    cards.forEach(card => {
      if (!card.hasAttribute('tabindex')) {
        card.setAttribute('tabindex', '0');
      }

      card.addEventListener('keypress', (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          card.click();
        }
      });
    });
  }

  /**
   * Chart Resizing for Mobile
   */
  initChartResizing() {
    const chartContainers = document.querySelectorAll('.chart-container');

    const resizeObserver = new ResizeObserver(entries => {
      entries.forEach(entry => {
        const canvas = entry.target.querySelector('canvas');
        if (canvas && canvas.chart) {
          // If using Chart.js or similar
          canvas.chart.resize();
        }
      });
    });

    chartContainers.forEach(container => {
      resizeObserver.observe(container);
    });

    // Store observer for cleanup if needed
    this.resizeObserver = resizeObserver;
  }

  handleResize() {
    // Resize all charts
    document.querySelectorAll('canvas').forEach(canvas => {
      if (canvas.chart) {
        canvas.chart.resize();
      }
    });
  }

  /**
   * Register Chart Instance for Responsive Handling
   */
  registerChart(canvasId, chartInstance) {
    const canvas = document.getElementById(canvasId);
    if (canvas) {
      canvas.chart = chartInstance;
      this.charts.set(canvasId, chartInstance);
    }
  }

  /**
   * Utility: Debounce Function
   */
  debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout);
        func(...args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
    };
  }

  /**
   * Cleanup Method
   */
  destroy() {
    if (this.resizeObserver) {
      this.resizeObserver.disconnect();
    }
    document.removeEventListener('keydown', this.handleKeydown);
  }
}

// Initialize when DOM is ready
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => {
    window.mobileDashboard = new MobileDashboard();
  });
} else {
  window.mobileDashboard = new MobileDashboard();
}

// Export for module usage if needed
if (typeof module !== 'undefined' && module.exports) {
  module.exports = MobileDashboard;
}
```

### Key Features Implemented:

**CSS (mobile.css):**
1. **CSS Custom Properties** - Dark theme with #0B0C0E background and #3B82F6 accent
2. **Hamburger Menu** - Animated three-line to X transformation, fixed positioning
3. **Stacked Cards** - Single column grid layout (`grid-template-columns: 1fr`) on mobile
4. **Swipeable Tables** - Horizontal scroll with `-webkit-overflow-scrolling: touch`, visual swipe indicator
5. **Touch Targets** - Minimum 44px for all interactive elements per WCAG guidelines
6. **Bottom Navigation** - Fixed positioning with safe area support for notched devices
7. **Optimized Charts** - Responsive height (250px mobile), resize-ready containers
8. **Accessibility** - Focus visible states, reduced motion support, ARIA attributes

**JavaScript (mobile.js):**
1. **Class Architecture** - ES6+ class with modular initialization methods
2. **Menu Toggle** - ARIA-expanded management, focus trapping, Escape key support
3. **Touch Events** - Passive listeners for scroll performance, touch feedback animations
4. **Swipe Detection** - Mouse and touch event handling for table scrolling
5. **Resize Observer** - Efficient chart resizing without polling
6. **Keyboard Navigation** - Skip links, Enter/Space activation for cards, focus management
7. **Chart Registry** - Method to register chart instances for automatic resizing

**Accessibility Features:**
- `aria-expanded`, `aria-current`, `aria-label` attributes
- Focus trap in navigation drawer
- Skip to main content link
- Keyboard operability (Escape, Tab, Enter, Space)
- Focus visible indicators
- Screen reader friendly bottom navigation

**Performance Optimizations:**
- Passive event listeners for touch/scroll
- CSS transforms instead of layout-triggering properties
- ResizeObserver instead of resize events
- `touch-action: manipulation` for buttons (prevents double-tap zoom)


Onboarding Wizard Component

Response:
```javascript
/**
 * Sunaiva Onboarding Wizard
 * Multi-step onboarding with progress persistence
 * File: Sunaiva/talking-widget/dashboard/js/onboarding-wizard.js
 */

class OnboardingWizard {
  constructor(containerSelector) {
    this.container = document.querySelector(containerSelector);
    if (!this.container) {
      console.error('Wizard container not found');
      return;
    }

    // Configuration
    this.totalSteps = 5;
    this.storageKey = 'sunaiva_onboarding_progress';

    // State
    this.currentStep = 1;
    this.formData = this.loadProgress() || this.getInitialData();
    this.isSubmitting = false;

    // Initialize
    this.init();
  }

  getInitialData() {
    return {
      business: {
        name: '',
        industry: '',
        phone: ''
      },
      plan: {
        tier: 'pro', // starter, pro, enterprise
        billing: 'monthly'
      },
      widget: {
        voice: 'female-1',
        greeting: 'Hello! How can I help you today?',
        primaryColor: '#3B82F6',
        position: 'bottom-right'
      },
      install: {
        verified: false,
        domain: ''
      }
    };
  }

  init() {
    this.render();
    this.attachEventListeners();
    this.updateUI();
    this.setupAccessibility();
  }

  render() {
    this.container.innerHTML = `

            ${this.renderStepIndicators()}

          <form class="wizard-form" novalidate>
            ${this.renderStepContent()}
          </form>

            <button type="button" class="btn btn-secondary btn-back" aria-label="Go back to previous step">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
                <path d="M19 12H5M12 19l-7-7 7-7"/>
              </svg>
              Back
            </button>

              <button type="button" class="btn btn-ghost btn-save" aria-label="Save progress and exit">
                Save & Exit
              </button>
              <button type="button" class="btn btn-primary btn-next" aria-label="Continue to next step">
                Next Step
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
                  <path d="M5 12h14M12 5l7 7-7 7"/>
                </svg>
              </button>

            Step 1 of ${this.totalSteps}

    `;

    // Cache DOM elements
    this.elements = {
      form: this.container.querySelector('.wizard-form'),
      progressFill: this.container.querySelector('.progress-fill'),
      stepIndicators: this.container.querySelectorAll('.step-indicator'),
      contentArea: this.container.querySelector('.wizard-content'),
      btnBack: this.container.querySelector('.btn-back'),
      btnNext: this.container.querySelector('.btn-next'),
      btnSave: this.container.querySelector('.btn-save'),
      currentStepNum: this.container.querySelector('.current-step-num')
    };
  }

  renderStepIndicators() {
    const steps = [
      { id: 1, label: 'Business Info', icon: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5' },
      { id: 2, label: 'Choose Plan', icon: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z' },
      { id: 3, label: 'Configure', icon: 'M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4' },
      { id: 4, label: 'Install', icon: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4' },
      { id: 5, label: 'Test', icon: 'M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z' }
    ];

    return steps.map(step => `
      <li class="step-indicator ${step.id === 1 ? 'active' : ''} ${step.id < 1 ? 'completed' : ''}"
          data-step="${step.id}"
          role="listitem"
          aria-label="Step ${step.id}: ${step.label}${step.id === 1 ? ' (current)' : ''}">

          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="${step.icon}"/>
          </svg>

        ${step.label}

    `).join('');
  }

  renderStepContent() {
    switch(this.currentStep) {
      case 1:
        return this.renderBusinessStep();
      case 2:
        return this.renderPlanStep();
      case 3:
        return this.renderConfigureStep();
      case 4:
        return this.renderInstallStep();
      case 5:
        return this.renderTestStep();
      default:
        return '';
    }
  }

  renderBusinessStep() {
    return `

Tell us about your business

We'll use this information to customize your AI assistant.

          Business Name *
          <input type="text"
                 id="business-name"
                 name="businessName"
                 class="form-input"
                 value="${this.formData.business.name}"
                 placeholder="Acme Corporation"
                 required
                 aria-required="true"
                 aria-describedby="business-name-error">

          Industry *
          <select id="business-industry"
                  name="industry"
                  class="form-select"
                  required
                  aria-required="true">
            <option value="" ${!this.formData.business.industry ? 'selected' : ''}>Select an industry</option>
            <option value="healthcare" ${this.formData.business.industry === 'healthcare' ? 'selected' : ''}>Healthcare</option>
            <option value="retail" ${this.formData.business.industry === 'retail' ? 'selected' : ''}>Retail & E-commerce</option>
            <option value="services" ${this.formData.business.industry === 'services' ? 'selected' : ''}>Professional Services</option>
            <option value="hospitality" ${this.formData.business.industry === 'hospitality' ? 'selected' : ''}>Hospitality</option>
            <option value="other" ${this.formData.business.industry === 'other' ? 'selected' : ''}>Other</option>
          </select>

          Business Phone *
          <input type="tel"
                 id="business-phone"
                 name="phone"
                 class="form-input"
                 value="${this.formData.business.phone}"
                 placeholder="+1 (555) 123-4567"
                 required
                 aria-required="true"
                 pattern="[\\+]?[(]?[0-9]{3}[)]?[-\\s\\.]?[0-9]{3}[-\\s\\.]?[0-9]{4,6}">
          Used for call forwarding and SMS notifications

    `;
  }

  renderPlanStep() {
    const plans = [
      {
        id: 'starter',
        name: 'Starter',
        price: { monthly: 29, yearly: 24 },
        features: ['1,000 minutes/month', '1 phone number', 'Basic analytics', 'Email support'],
        recommended: false
      },
      {
        id: 'pro',
        name: 'Professional',
        price: { monthly: 79, yearly: 66 },
        features: ['5,000 minutes/month', '3 phone numbers', 'Advanced analytics', 'Priority support', 'Custom voice'],
        recommended: true
      },
      {
        id: 'enterprise',
        name: 'Enterprise',
        price: { monthly: 199, yearly: 166 },
        features: ['Unlimited minutes', 'Unlimited numbers', 'White-label option', '24/7 dedicated support', 'API access', 'Custom integrations'],
        recommended: false
      }
    ];

    const isYearly = this.formData.plan.billing === 'yearly';
    const selectedPlan = this.formData.plan.tier;

    return `

Choose your plan

Select the plan that fits your business needs.

          Monthly
          <button type="button"
                  class="toggle-switch"
                  role="switch"
                  aria-checked="${isYearly}"
                  aria-label="Toggle yearly billing">

          </button>
          Yearly
          Save 20%

          ${plans.map(plan => {
            const price = isYearly ? plan.price.yearly : plan.price.monthly;
            const isSelected = selectedPlan === plan.id;

            return `

                ${plan.recommended ? '
Recommended
' : ''}

${plan.name}

                  $
                  ${price}
                  /mo

                  ${plan.features.map(feature => `

                      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
                        <polyline points="20 6 9 17 4 12"></polyline>
                      </svg>
                      ${feature}

                  `).join('')}

                <button type="button" class="btn btn-outline btn-select-plan" data-plan="${plan.id}">
                  ${isSelected ? 'Selected' : 'Select Plan'}
                </button>

            `;
          }).join('')}

    `;
  }

  renderConfigureStep() {
    const voices = [
      { id: 'female-1', name: 'Sarah', gender: 'Female', preview: 'Hello, I\'m Sarah.' },
      { id: 'female-2', name: 'Emma', gender: 'Female', preview: 'Hi there, I\'m Emma.' },
      { id: 'male-1', name: 'James', gender: 'Male', preview: 'Hello, I\'m James.' },
      { id: 'male-2', name: 'Michael', gender: 'Male', preview: 'Hi, I\'m Michael.' }
    ];

    return `

Configure your widget

Customize how your AI assistant looks and sounds.

Voice & Personality

                Select Voice

                  ${voices.map(voice => `

                        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                          <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
                          <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
                          <line x1="12" y1="19" x2="12" y2="23"></line>
                          <line x1="8" y1="23" x2="16" y2="23"></line>
                        </svg>

                        ${voice.name}
                        ${voice.gender}

                      <button type="button" class="btn btn-sm btn-ghost btn-preview" data-voice="${voice.id}" aria-label="Preview ${voice.name}'s voice">
                        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                          <polygon points="5 3 19 12 5 21 5 3"></polygon>
                        </svg>
                      </button>

                  `).join('')}

                Greeting Message
                <textarea id="widget-greeting"
                          name="greeting"
                          class="form-textarea"
                          rows="3"
                          maxlength="200"
                          aria-describedby="greeting-help">${this.formData.widget.greeting}</textarea>

                  This is the first message your assistant will say
                  ${this.formData.widget.greeting.length}/200

Appearance

                  Primary Color

                    <input type="color"
                           id="widget-color"
                           name="primaryColor"
                           class="color-input"
                           value="${this.formData.widget.primaryColor}"
                           aria-label="Select primary color">
                    ${this.formData.widget.primaryColor}

                  Widget Position

                    ${['bottom-right', 'bottom-left', 'top-right', 'top-left'].map(pos => `
                      <button type="button"
                              class="position-btn ${this.formData.widget.position === pos ? 'selected' : ''}"
                              data-position="${pos}"
                              aria-label="Position ${pos.replace('-', ' ')}"
                              aria-pressed="${this.formData.widget.position === pos}">

                      </button>
                    `).join('')}

              Live Preview
              Updated in real-time

                  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
                    <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
                  </svg>

                    ${this.formData.business.name || 'Your Business'}

                        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                          <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
                        </svg>

${this.formData.widget.greeting}

    `;
  }

  renderPlanStep() {
    const isYearly = this.formData.plan.billing === 'yearly';

    return `

Choose your plan

Select the plan that best fits your business needs.

            <button type="button"
                    class="toggle-btn ${!isYearly ? 'active' : ''}"
                    data-billing="monthly"
                    aria-pressed="${!isYearly}">
              Monthly
            </button>
            <button type="button"
                    class="toggle-btn ${isYearly ? 'active' : ''}"
                    data-billing="yearly"
                    aria-pressed="${isYearly}">
              Yearly
              Save 20%
            </button>

          ${this.renderPlanCards()}

          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
            <path d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
          </svg>
          30-day money-back guarantee. Cancel anytime.

    `;
  }

  renderPlanCards() {
    const plans = [
      {
        id: 'starter',
        name: 'Starter',
        description: 'Perfect for small businesses',
        price: { monthly: 29, yearly: 24 },
        features: ['1,000 minutes/month', '1 phone number', 'Basic analytics', 'Email support']
      },
      {
        id: 'pro',
        name: 'Professional',
        description: 'Best for growing businesses',
        price: { monthly: 79, yearly: 66 },
        features: ['5,000 minutes/month', '3 phone numbers', 'Advanced analytics', 'Priority support', 'Custom voice'],
        popular: true
      },
      {
        id: 'enterprise',
        name: 'Enterprise',
        description: 'For large organizations',
        price: { monthly: 199, yearly: 166 },
        features: ['Unlimited minutes', 'Unlimited numbers', 'White-label option', '24/7 dedicated support', 'API access', 'Custom integrations']
      }
    ];

    const isYearly = this.formData.plan.billing === 'yearly';

    return plans.map(plan => {
      const price = isYearly ? plan.price.yearly : plan.price.monthly;
      const isSelected = this.formData.plan.tier === plan.id;

      return `

          ${plan.popular ? '
Most Popular
' : ''}

${plan.name}

${plan.description}

            $
            ${price}
            /mo

            ${plan.features.map(feature => `

                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
                  <polyline points="20 6 9 17 4 12"></polyline>
                </svg>
                ${feature}

            `).join('')}

          <button type="button" class="btn btn-select ${isSelected ? 'btn-primary' : 'btn-outline'}">
            ${isSelected ? 'Selected' : 'Select Plan'}
          </button>

      `;
    }).join('');
  }

  renderConfigureStep() {
    return `

Configure your widget

Customize the appearance and behavior of your AI assistant.

Voice & Personality

                Select Voice

                  ${['female-1', 'female-2', 'male-1', 'male-2'].map((voice, idx) => {
                    const isSelected = this.formData.widget.voice === voice;
                    const names = ['Sarah', 'Emma', 'James', 'Michael'];
                    return `

                        ${names[idx]}
                        <button type="button" class="btn btn-icon btn-preview-voice" aria-label="Preview ${names[idx]}">
                          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                            <polygon points="5 3 19 12 5 21 5 3"></polygon>
                          </svg>
                        </button>

                    `;
                  }).join('')}

                Greeting Message
                <textarea id="widget-greeting"
                          class="form-textarea"
                          rows="3"
                          maxlength="200"
                          aria-describedby="greeting-help">${this.formData.widget.greeting}</textarea>

                  First message visitors will hear
                  ${this.formData.widget.greeting.length}/200

Appearance

                  Primary Color

                    <input type="color"
                           id="widget-color"
                           class="color-input"
                           value="${this.formData.widget.primaryColor}"
                           aria-label="Select primary color">
                    ${this.formData.widget.primaryColor}

                  Widget Position

                    ${['bottom-right', 'bottom-left', 'top-right', 'top-left'].map(pos => `
                      <button type="button"
                              class="position-btn ${this.formData.widget.position === pos ? 'selected' : ''}"
                              data-position="${pos}"
                              aria-label="Position ${pos.replace('-', ' ')}"
                              aria-pressed="${this.formData.widget.position === pos}">

                      </button>
                    `).join('')}

              Live Preview
              Real-time

                  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
                    <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
                  </svg>

                    ${this.formData.business.name || 'Your Business'}

                        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                          <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
                        </svg>

${this


Memory Vault Export API

Response:
 ```python
"""
Sunaiva AI Memory - Export API

Production-ready export endpoint for memory vault data with support for
JSON, Markdown, and CSV formats. Implements streaming for large datasets,
rate limiting (1 export/hour), and comprehensive error handling.
"""

import csv
import io
import json
import logging
import uuid
from datetime import datetime, timedelta
from enum import Enum
from typing import (
    Any,
    AsyncGenerator,
    Dict,
    List,
    Optional,
    Union,
)
from contextlib import asynccontextmanager

import aioredis
from fastapi import (
    APIRouter,
    Depends,
    HTTPException,
    Request,
    status,
)
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from sqlalchemy import select, and_
from sqlalchemy.ext.asyncio import (
    AsyncSession,
    async_sessionmaker,
    create_async_engine,
)
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy import (
    Column,
    DateTime,
    ForeignKey,
    String,
    Text,
    JSON as SQLJSON,
    Enum as SQLEnum,
)

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Database Setup (PostgreSQL)
DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/sunaiva_db"
engine = create_async_engine(DATABASE_URL, echo=False, future=True)
async_session_maker = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)

# Redis Setup for Rate Limiting
REDIS_URL = "redis://localhost:6379/0"
redis_pool: Optional[aioredis.Redis] = None

Base = declarative_base()

# =============================================================================
# Database Models
# =============================================================================

class EntityType(str, Enum):
    """Types of entities in the knowledge graph."""
    PERSON = "person"
    ORGANIZATION = "organization"
    CONCEPT = "concept"
    EVENT = "event"
    LOCATION = "location"
    TOPIC = "topic"

class RelationshipType(str, Enum):
    """Types of relationships between entities."""
    KNOWS = "knows"
    WORKS_AT = "works_at"
    LOCATED_IN = "located_in"
    RELATED_TO = "related_to"
    PREFERS = "prefers"
    DECIDED = "decided"

class User(Base):
    """User model."""
    __tablename__ = "users"

    id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    email = Column(String(255), unique=True, nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)

    # Relationships
    entities = relationship("Entity", back_populates="user", lazy="selectin")
    decisions = relationship("Decision", back_populates="user", lazy="selectin")
    preferences = relationship("Preference", back_populates="user", lazy="selectin")
    relationships = relationship("Relationship", back_populates="user", lazy="selectin")

class Entity(Base):
    """Knowledge graph entities."""
    __tablename__ = "entities"

    id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
    name = Column(String(255), nullable=False)
    entity_type = Column(SQLEnum(EntityType), nullable=False)
    description = Column(Text)
    metadata_json = Column(SQLJSON, default=dict)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    user = relationship("User", back_populates="entities")

class Decision(Base):
    """User decisions and outcomes."""
    __tablename__ = "decisions"

    id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
    title = Column(String(500), nullable=False)
    context = Column(Text)
    options = Column(SQLJSON)  # List of options considered
    selected_option = Column(Text)
    rationale = Column(Text)
    confidence_score = Column(String(10))  # Stored as string for precision
    created_at = Column(DateTime, default=datetime.utcnow)

    user = relationship("User", back_populates="decisions")

class Preference(Base):
    """User preferences and settings."""
    __tablename__ = "preferences"

    id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
    category = Column(String(100), nullable=False)  # e.g., "communication", "work", "lifestyle"
    key = Column(String(255), nullable=False)
    value = Column(Text, nullable=False)
    importance = Column(String(20), default="medium")  # low, medium, high
    source = Column(String(100))  # How was this preference inferred
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    user = relationship("User", back_populates="preferences")

class Relationship(Base):
    """Relationships between entities."""
    __tablename__ = "relationships"

    id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
    source_entity_id = Column(String(36), ForeignKey("entities.id"), nullable=False)
    target_entity_id = Column(String(36), ForeignKey("entities.id"), nullable=False)
    relationship_type = Column(SQLEnum(RelationshipType), nullable=False)
    strength = Column(String(10), default="1.0")  # 0.0 to 1.0
    evidence = Column(Text)
    created_at = Column(DateTime, default=datetime.utcnow)

    user = relationship("User", back_populates="relationships")
    source_entity = relationship("Entity", foreign_keys=[source_entity_id])
    target_entity = relationship("Entity", foreign_keys=[target_entity_id])

# =============================================================================
# Pydantic Schemas
# =============================================================================

class ExportFormat(str, Enum):
    """Supported export formats."""
    JSON = "json"
    MARKDOWN = "md"
    CSV = "csv"

class ExportMetadata(BaseModel):
    """Metadata for the export file."""
    user_id: str
    export_timestamp: datetime = Field(default_factory=datetime.utcnow)
    version: str = "1.0.0"
    record_counts: Dict[str, int] = Field(default_factory=dict)

class EntityExport(BaseModel):
    """Entity export schema."""
    id: str
    name: str
    entity_type: str
    description: Optional[str]
    metadata: Dict[str, Any] = Field(alias="metadata_json")
    created_at: datetime
    updated_at: Optional[datetime]

class DecisionExport(BaseModel):
    """Decision export schema."""
    id: str
    title: str
    context: Optional[str]
    options: List[str]
    selected_option: Optional[str]
    rationale: Optional[str]
    confidence_score: Optional[str]
    created_at: datetime

class PreferenceExport(BaseModel):
    """Preference export schema."""
    id: str
    category: str
    key: str
    value: str
    importance: str
    source: Optional[str]
    created_at: datetime

class RelationshipExport(BaseModel):
    """Relationship export schema."""
    id: str
    source_entity_id: str
    target_entity_id: str
    relationship_type: str
    strength: str
    evidence: Optional[str]
    created_at: datetime

# =============================================================================
# Rate Limiting
# =============================================================================

class RateLimiter:
    """
    Redis-based rate limiter for export operations.
    Limits users to 1 export per hour.
    """

    def __init__(self, redis_client: aioredis.Redis):
        self.redis = redis_client
        self.key_prefix = "export_limit"
        self.ttl_seconds = 3600  # 1 hour

    def _get_key(self, user_id: str) -> str:
        """Generate Redis key for user."""
        return f"{self.key_prefix}:{user_id}"

    async def is_allowed(self, user_id: str) -> bool:
        """
        Check if user is allowed to export.

        Args:
            user_id: UUID of the user

        Returns:
            bool: True if allowed, False if rate limited
        """
        key = self._get_key(user_id)
        exists = await self.redis.exists(key)
        return not exists

    async def record_export(self, user_id: str) -> None:
        """
        Record that user has performed an export.

        Args:
            user_id: UUID of the user
        """
        key = self._get_key(user_id)
        await self.redis.setex(key, self.ttl_seconds, datetime.utcnow().isoformat())

    async def get_retry_after(self, user_id: str) -> int:
        """
        Get seconds until next allowed export.

        Args:
            user_id: UUID of the user

        Returns:
            int: Seconds until retry is allowed
        """
        key = self._get_key(user_id)
        ttl = await self.redis.ttl(key)
        return max(ttl, 0)

# =============================================================================
# Data Access Layer
# =============================================================================

class ExportRepository:
    """
    Repository for fetching user data with streaming support.
    Uses server-side cursors for memory-efficient large dataset handling.
    """

    def __init__(self, session: AsyncSession):
        self.session = session

    async def get_entities_stream(
        self,
        user_id: str,
        batch_size: int = 100
    ) -> AsyncGenerator[Entity, None]:
        """
        Stream entities for user using server-side cursor.

        Args:
            user_id: User UUID
            batch_size: Number of records per fetch

        Yields:
            Entity objects
        """
        try:
            stmt = (
                select(Entity)
                .where(Entity.user_id == user_id)
                .order_by(Entity.created_at)
                .execution_options(stream_results=True, max_row_buffer=batch_size)
            )

            result = await self.session.stream(stmt)
            async for row in result.scalars():
                yield row

        except Exception as e:
            logger.error(f"Error streaming entities for user {user_id}: {e}")
            raise

    async def get_decisions_stream(
        self,
        user_id: str,
        batch_size: int = 100
    ) -> AsyncGenerator[Decision, None]:
        """Stream decisions for user."""
        try:
            stmt = (
                select(Decision)
                .where(Decision.user_id == user_id)
                .order_by(Decision.created_at)
                .execution_options(stream_results=True, max_row_buffer=batch_size)
            )

            result = await self.session.stream(stmt)
            async for row in result.scalars():
                yield row

        except Exception as e:
            logger.error(f"Error streaming decisions for user {user_id}: {e}")
            raise

    async def get_preferences_stream(
        self,
        user_id: str,
        batch_size: int = 100
    ) -> AsyncGenerator[Preference, None]:
        """Stream preferences for user."""
        try:
            stmt = (
                select(Preference)
                .where(Preference.user_id == user_id)
                .order_by(Preference.category, Preference.key)
                .execution_options(stream_results=True, max_row_buffer=batch_size)
            )

            result = await self.session.stream(stmt)
            async for row in result.scalars():
                yield row

        except Exception as e:
            logger.error(f"Error streaming preferences for user {user_id}: {e}")
            raise

    async def get_relationships_stream(
        self,
        user_id: str,
        batch_size: int = 100
    ) -> AsyncGenerator[Relationship, None]:
        """Stream relationships for user."""
        try:
            stmt = (
                select(Relationship)
                .where(Relationship.user_id == user_id)
                .order_by(Relationship.created_at)
                .execution_options(stream_results=True, max_row_buffer=batch_size)
            )

            result = await self.session.stream(stmt)
            async for row in result.scalars():
                yield row

        except Exception as e:
            logger.error(f"Error streaming relationships for user {user_id}: {e}")
            raise

    async def get_counts(self, user_id: str) -> Dict[str, int]:
        """Get total counts for each data type."""
        counts = {}

        for model, name in [
            (Entity, "entities"),
            (Decision, "decisions"),
            (Preference, "preferences"),
            (Relationship, "relationships")
        ]:
            stmt = select(model).where(model.user_id == user_id)
            result = await self.session.execute(select(stmt.subquery().c.id))
            counts[name] = len(result.all())

        return counts

# =============================================================================
# Export Generators
# =============================================================================

class ExportGenerator:
    """
    Generates export content in various formats using async streaming.
    """

    def __init__(self, repository: ExportRepository, user_id: str):
        self.repo = repository
        self.user_id = user_id
        self.metadata = ExportMetadata(user_id=user_id)

    async def generate_json(self) -> AsyncGenerator[str, None]:
        """
        Generate JSON export as a stream.
        Uses a structured format with all data types nested.
        """
        try:
            # Header
            yield '{\n'
            yield f'  "metadata": {self.metadata.json()},\n'
            yield '  "data": {\n'

            # Entities
            yield '    "entities": [\n'
            first = True
            async for entity in self.repo.get_entities_stream(self.user_id):
                if not first:
                    yield ',\n'
                first = False
                data = EntityExport.from_orm(entity).json()
                yield f'      {data}'
            yield '\n    ],\n'

            # Decisions
            yield '    "decisions": [\n'
            first = True
            async for decision in self.repo.get_decisions_stream(self.user_id):
                if not first:
                    yield ',\n'
                first = False
                data = DecisionExport.from_orm(decision).json()
                yield f'      {data}'
            yield '\n    ],\n'

            # Preferences
            yield '    "preferences": [\n'
            first = True
            async for pref in self.repo.get_preferences_stream(self.user_id):
                if not first:
                    yield ',\n'
                first = False
                data = PreferenceExport.from_orm(pref).json()
                yield f'      {data}'
            yield '\n    ],\n'

            # Relationships
            yield '    "relationships": [\n'
            first = True
            async for rel in self.repo.get_relationships_stream(self.user_id):
                if not first:
                    yield ',\n'
                first = False
                data = RelationshipExport.from_orm(rel).json()
                yield f'      {data}'
            yield '\n    ]\n'

            # Footer
            yield '  }\n}'

        except Exception as e:
            logger.error(f"Error generating JSON export: {e}")
            raise

    async def generate_csv(self) -> AsyncGenerator[str, None]:
        """
        Generate CSV export as a stream.
        Flattened format with type column to distinguish records.
        """
        try:
            output = io.StringIO()
            writer = csv.writer(output)

            # Header
            writer.writerow([
                'type', 'id', 'category', 'name', 'description',
                'value', 'created_at', 'updated_at', 'metadata'
            ])
            yield output.getvalue()
            output.seek(0)
            output.truncate(0)

            # Entities
            async for entity in self.repo.get_entities_stream(self.user_id):
                writer.writerow([
                    'entity',
                    entity.id,
                    entity.entity_type.value,
                    entity.name,
                    entity.description or '',
                    '',
                    entity.created_at.isoformat(),
                    entity.updated_at.isoformat() if entity.updated_at else '',
                    json.dumps(entity.metadata_json) if entity.metadata_json else ''
                ])
                yield output.getvalue()
                output.seek(0)
                output.truncate(0)

            # Decisions
            async for decision in self.repo.get_decisions_stream(self.user_id):
                writer.writerow([
                    'decision',
                    decision.id,
                    'decision',
                    decision.title,
                    decision.context or '',
                    decision.selected_option or '',
                    decision.created_at.isoformat(),
                    '',
                    json.dumps({
                        'options': decision.options,
                        'rationale': decision.rationale,
                        'confidence': decision.confidence_score
                    })
                ])
                yield output.getvalue()
                output.seek(0)
                output.truncate(0)

            # Preferences
            async for pref in self.repo.get_preferences_stream(self.user_id):
                writer.writerow([
                    'preference',
                    pref.id,
                    pref.category,
                    pref.key,
                    '',
                    pref.value,
                    pref.created_at.isoformat(),
                    pref.updated_at.isoformat() if pref.updated_at else '',
                    json.dumps({'importance': pref.importance, 'source': pref.source})
                ])
                yield output.getvalue()
                output.seek(0)
                output.truncate(0)

            # Relationships
            async for rel in self.repo.get_relationships_stream(self.user_id):
                writer.writerow([
                    'relationship',
                    rel.id,
                    rel.relationship_type.value,
                    f"{rel.source_entity_id}->{rel.target_entity_id}",
                    rel.evidence or '',
                    rel.strength,
                    rel.created_at.isoformat(),
                    '',
                    json.dumps({
                        'source_id': rel.source_entity_id,
                        'target_id': rel.target_entity_id
                    })
                ])
                yield output.getvalue()
                output.seek(0)
                output.truncate(0)

        except Exception as e:
            logger.error(f"Error generating CSV export: {e}")
            raise

    async def generate_markdown(self) -> AsyncGenerator[str, None]:
        """
        Generate Markdown export as a stream.
        Human-readable format with sections and tables.
        """
        try:
            # Header
            yield f"# Memory Vault Export\n\n"
            yield f"**User ID:** {self.user_id}  \n"
            yield f"**Exported:** {self.metadata.export_timestamp.strftime('%Y-%m-%d %H:%M:%S UTC')}  \n"
            yield f"**Version:** {self.metadata.version}\n\n"
            yield "---\n\n"

            # Entities Section
            yield "## Entities\n\n"
            async for entity in self.repo.get_entities_stream(self.user_id):
                yield f"### {entity.name}\n\n"
                yield f"- **Type:** {entity.entity_type.value}\n"
                yield f"- **ID:** `{entity.id}`\n"
                if entity.description:
                    yield f"- **Description:** {entity.description}\n"
                if entity.metadata_json:
                    yield f"- **Metadata:**\n"
                    for k, v in entity.metadata_json.items():
                        yield f"  - {k}: {v}\n"
                yield f"- **Created:** {entity.created_at.strftime('%Y-%m-%d')}\n\n"

            # Decisions Section
            yield "## Decisions\n\n"
            async for decision in self.repo.get_decisions_stream(self.user_id):
                yield f"### {decision.title}\n\n"
                yield f"**ID:** `{decision.id}`  \n"
                if decision.context:
                    yield f"**Context:** {decision.context}  \n"
                if decision.options:
                    yield f"\n**Options Considered:**\n"
                    for opt in decision.options:
                        prefix = "✓ " if opt == decision.selected_option else "  "
                        yield f"{prefix}- {opt}\n"
                if decision.rationale:
                    yield f"\n**Rationale:** {decision.rationale}  \n"
                if decision.confidence_score:
                    yield f"**Confidence:** {decision.confidence_score}  \n"
                yield f"**Date:** {decision.created_at.strftime('%Y-%m-%d')}\n\n"

            # Preferences Section
            yield "## Preferences\n\n"
            yield "| Category | Key | Value | Importance | Source |\n"
            yield "|----------|-----|-------|------------|--------|\n"
            async for pref in self.repo.get_preferences_stream(self.user_id):
                yield f"| {pref.category} | {pref.key} | {pref.value} | {pref.importance} | {pref.source or 'N/A'} |\n"
            yield "\n"

            # Relationships Section
            yield "## Relationships\n\n"
            async for rel in self.repo.get_relationships_stream(self.user_id):
                yield f"- **{rel.relationship_type.value}**  \n"
                yield f"  - From: `{rel.source_entity_id}`  \n"
                yield f"  - To: `{rel.target_entity_id}`  \n"
                yield f"  - Strength: {rel.strength}  \n"
                if rel.evidence:
                    yield f"  - Evidence: {rel.evidence}  \n"
                yield "\n"

        except Exception as e:
            logger.error(f"Error generating Markdown export: {e}")
            raise

# =============================================================================
# Dependencies
# =============================================================================

async def get_redis() -> aioredis.Redis:
    """Get Redis connection."""
    global redis_pool
    if redis_pool is None:
        redis_pool = aioredis.from_url(REDIS_URL, encoding="utf-8", decode_responses=True)
    return redis_pool

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    """Get database session."""
    async with async_session_maker() as session:
        try:
            yield session
        except Exception as e:
            await session.rollback()
            raise e
        finally:
            await session.close()

async def get_current_user(request: Request) -> User:
    """
    Extract current user from request.
    In production, this would validate JWT tokens.
    """
    # Placeholder: In real implementation, extract from JWT
    user_id = request.headers.get("x-user-id")
    if not user_id:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Missing authentication"
        )

    async with async_session_maker() as session:
        result = await session.execute(select(User).where(User.id == user_id))
        user = result.scalar_one_or_none()
        if not user:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="User not found"
            )
        return user

async def check_rate_limit(
    user: User = Depends(get_current_user),
    redis: aioredis.Redis = Depends(get_redis)
) -> None:
    """
    Dependency to check rate limiting before allowing export.
    Raises 429 if user has exported within the last hour.
    """
    limiter = RateLimiter(redis)

    if not await limiter.is_allowed(user.id):
        retry_after = await limiter.get_retry_after(user.id)
        raise HTTPException(
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            detail=f"Export rate limit exceeded. Try again in {retry_after} seconds.",
            headers={"Retry-After": str(retry_after)}
        )

# =============================================================================
# API Router
# =============================================================================

router = APIRouter(prefix="/api/v1/export", tags=["export"])

@router.get(
    "/{format}",
    response_class=StreamingResponse,
    summary="Export user memory vault",
    description="""
    Export the entire memory vault for the authenticated user in the requested format.

    Supports:
    - **json**: Structured JSON with nested objects
    - **csv**: Flattened CSV suitable for spreadsheets
    - **md**: Human-readable Markdown document

    Rate limited to 1 export per hour per user.
    Large exports are streamed to minimize memory usage.
    """,
    responses={
        200: {
            "description": "Successful export",
            "content": {
                "application/json": {"example": {"status": "streaming"}},
                "text/csv": {"example": "type,id,name..."},
                "text/markdown": {"example": "# Memory Vault Export..."}
            }
        },
        429: {
            "description": "Rate limit exceeded",
            "content": {
                "application/json": {
                    "example": {"detail": "Export rate limit exceeded. Try again in 3600 seconds."}
                }
            }
        }
    }
)
async def export_vault(
    format: ExportFormat,
    user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
    redis: aioredis.Redis = Depends(get_redis),
    _: None = Depends(check_rate_limit)
) -> StreamingResponse:
    """
    Export the user's entire memory vault.

    Args:
        format: Export format (json, csv, or md)
        user: Authenticated user (from dependency)
        db: Database session
        redis: Redis connection
        _: Rate limit check (side effect only)

    Returns:
        StreamingResponse with the export file
    """
    try:
        # Initialize repository and generator
        repo = ExportRepository(db)
        generator = ExportGenerator(repo, user.id)

        # Record the export attempt for rate limiting
        limiter = RateLimiter(redis)
        await limiter.record_export(user.id)

        # Configure response based on format
        if format == ExportFormat.JSON:
            media_type = "application/json"
            filename = f"sunaiva_export_{user.id}_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.json"
            content_generator = generator.generate_json()

        elif format == ExportFormat.CSV:
            media_type = "text/csv"
            filename = f"sunaiva_export_{user.id}_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.csv"
            content_generator = generator.generate_csv()

        elif format == ExportFormat.MARKDOWN:
            media_type = "text/markdown"
            filename = f"sunaiva_export_{user.id}_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.md"
            content_generator = generator.generate_markdown()

        else:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=f"Unsupported format: {format}"
            )

        logger.info(f"Starting export for user {user.id} in format {format}")

        return StreamingResponse(
            content=content_generator,
            media_type=media_type,
            headers={
                "Content-Disposition": f"attachment; filename={filename}",
                "X-Export-Format": format.value,
                "X-Rate-Limit": "1/hour"
            }
        )

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Export failed for user {user.id}: {str(e)}", exc_info=True)
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Export generation failed. Please try again later."
        )

# =============================================================================
# Health Check Endpoint
# =============================================================================

@router.get(
    "/health/status",
    summary="Check export service health",
    include_in_schema=False
)
async def health_check(
    db: AsyncSession = Depends(get_db),
    redis: aioredis.Redis = Depends(get_redis)
) -> Dict[str, str]:
    """Health check for export service dependencies."""
    status_result = {"status": "healthy"}

    try:
        # Check DB
        await db.execute(select(1))
        status_result["database"] = "connected"
    except Exception as e:
        status_result["database"] = f"error: {str(e)}"
        status_result["status"] = "degraded"

    try:
        # Check Redis
        await redis.ping()
        status_result["redis"] = "connected"
    except Exception as e:
        status_result["redis"] = f"error: {str(e)}"
        status_result["status"] = "degraded"

    return status_result
```


Memory Search with Semantic Similarity

Response:
 ```python
"""
Sunaiva AI Memory - Semantic Search Endpoint

Production-ready semantic search implementation using FastAPI, OpenAI embeddings,
Qdrant vector search, and PostgreSQL for metadata storage. Supports searching
across entities, decisions, and preferences with conversation source filtering
and context snippet retrieval.

File: Sunaiva/ai-memory/server/semantic_search.py
"""

import logging
import hashlib
import time
from typing import List, Optional, Dict, Any, Callable
from datetime import datetime
from enum import Enum
from dataclasses import dataclass

from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks, Query
from pydantic import BaseModel, Field, validator, ConfigDict
from qdrant_client import AsyncQdrantClient
from qdrant_client.models import (
    Filter,
    FieldCondition,
    MatchValue,
    SearchParams,
    ScoredPoint,
    Range as QdrantRange,
    MatchAny
)
from openai import AsyncOpenAI, APIError as OpenAIError, RateLimitError
from openai.types import CreateEmbeddingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import selectinload
import uuid
from functools import wraps

# Configure module logger
logger = logging.getLogger(__name__)

# ============================================================================
# ENUMS & CONSTANTS
# ============================================================================

class MemoryType(str, Enum):
    """Types of memories extractable from conversations."""
    ENTITY = "entity"
    DECISION = "decision"
    PREFERENCE = "preference"

class SearchSort(str, Enum):
    """Result sorting options."""
    RELEVANCE = "relevance"
    TIMESTAMP_DESC = "timestamp_desc"
    TIMESTAMP_ASC = "timestamp_asc"

EMBEDDING_MODEL = "text-embedding-3-small"
EMBEDDING_DIMENSIONS = 1536
MAX_QUERY_LENGTH = 1000
QDRANT_COLLECTION = "memory_embeddings"

# ============================================================================
# DATABASE MODELS (SQLAlchemy)
# ============================================================================

from sqlalchemy import Column, String, DateTime, ForeignKey, Text, Float, JSON, Enum as SQLEnum

class ConversationORM:
    """SQLAlchemy model for conversation sources."""
    __tablename__ = "conversations"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    vault_id = Column(UUID(as_uuid=True), ForeignKey("memory_vaults.id"), nullable=False, index=True)
    source = Column(String(255), nullable=False, index=True)  # e.g., "chatgpt_export", "claude_export"
    title = Column(String(500))
    created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
    metadata_json = Column("metadata", JSON, default=dict)

class MemoryItemORM:
    """SQLAlchemy model for extracted memory items."""
    __tablename__ = "memory_items"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    conversation_id = Column(UUID(as_uuid=True), ForeignKey("conversations.id"), nullable=False, index=True)
    vault_id = Column(UUID(as_uuid=True), ForeignKey("memory_vaults.id"), nullable=False, index=True)
    memory_type = Column(SQLEnum(MemoryType), nullable=False, index=True)
    content = Column(Text, nullable=False)
    content_embedding_id = Column(String(64), index=True)  # References Qdrant point ID
    context_before = Column(Text)  # 500 chars before for snippet
    context_after = Column(Text)   # 500 chars after for snippet
    extracted_metadata = Column("metadata", JSON, default=dict)  # Confidence scores, etc.
    created_at = Column(DateTime(timezone=True), default=datetime.utcnow, index=True)

# ============================================================================
# PYDANTIC SCHEMAS
# ============================================================================

class SemanticSearchRequest(BaseModel):
    """Request schema for semantic search."""
    model_config = ConfigDict(json_schema_extra={
        "example": {
            "query": "What did the user decide about the tech stack?",
            "memory_types": ["decision", "preference"],
            "conversation_sources": ["chatgpt_export"],
            "top_k": 10,
            "min_similarity": 0.75
        }
    })

    query: str = Field(
        ...,
        min_length=1,
        max_length=MAX_QUERY_LENGTH,
        description="Natural language search query"
    )
    memory_types: List[MemoryType] = Field(
        default=[MemoryType.ENTITY, MemoryType.DECISION, MemoryType.PREFERENCE],
        description="Memory categories to search"
    )
    conversation_sources: Optional[List[str]] = Field(
        default=None,
        description="Filter by source identifiers (e.g., ['chatgpt', 'claude'])"
    )
    date_range_start: Optional[datetime] = Field(
        default=None,
        description="ISO datetime for start of search window"
    )
    date_range_end: Optional[datetime] = Field(
        default=None,
        description="ISO datetime for end of search window"
    )
    top_k: int = Field(
        default=10,
        ge=1,
        le=50,
        description="Maximum results to return (1-50)"
    )
    min_similarity: float = Field(
        default=0.7,
        ge=0.0,
        le=1.0,
        description="Minimum cosine similarity threshold (0.0-1.0)"
    )
    include_context_snippets: bool = Field(
        default=True,
        description="Include surrounding conversation context"
    )
    sort_by: SearchSort = Field(
        default=SearchSort.RELEVANCE,
        description="Result ranking method"
    )

    @validator('date_range_end')
    def validate_date_range(cls, v: Optional[datetime], info) -> Optional[datetime]:
        """Ensure end date is after start date."""
        if v and info.data.get('date_range_start') and v < info.data['date_range_start']:
            raise ValueError('date_range_end must be after date_range_start')
        return v

class ContextSnippet(BaseModel):
    """Surrounding text context for a memory match."""
    before: Optional[str] = Field(
        default=None,
        max_length=1000,
        description="Text preceding the memory (up to 500 chars)"
    )
    matched_content: str = Field(
        ...,
        description="The exact memory content that matched"
    )
    after: Optional[str] = Field(
        default=None,
        max_length=1000,
        description="Text following the memory (up to 500 chars)"
    )

class SearchResult(BaseModel):
    """Individual search result with metadata."""
    memory_id: str = Field(..., description="UUID of the memory record")
    memory_type: MemoryType = Field(..., description="Category of memory")
    content: str = Field(..., description="Memory content text")
    similarity_score: float = Field(
        ...,
        ge=0.0,
        le=1.0,
        description="Cosine similarity score (1.0 = exact match)"
    )
    confidence_score: Optional[float] = Field(
        default=None,
        description="Extraction confidence from metadata"
    )
    conversation_id: str = Field(..., description="Parent conversation UUID")
    conversation_source: str = Field(..., description="Original export source")
    conversation_title: Optional[str] = Field(default=None, description="Conversation title")
    created_at: datetime = Field(..., description="Memory creation timestamp")
    context_snippet: Optional[ContextSnippet] = Field(
        default=None,
        description="Surrounding context if requested"
    )
    metadata: Dict[str, Any] = Field(
        default_factory=dict,
        description="Additional extraction metadata"
    )

class SemanticSearchResponse(BaseModel):
    """Response schema containing ranked search results."""
    query: str = Field(..., description="Normalized query string")
    normalized_query: Optional[str] = Field(
        default=None,
        description="Query after preprocessing"
    )
    total_results: int = Field(..., ge=0, description="Total matching results")
    results: List[SearchResult] = Field(..., description="Ranked results")
    search_time_ms: float = Field(..., description="Total execution time")
    embedding_model: str = Field(..., description="Embedding model version")
    applied_filters: Dict[str, Any] = Field(
        default_factory=dict,
        description="Filters applied to the search"
    )

# ============================================================================
# EXCEPTIONS
# ============================================================================

class SearchServiceError(Exception):
    """Base exception for search service failures."""
    pass

class EmbeddingServiceError(SearchServiceError):
    """Raised when embedding generation fails."""
    pass

class VectorDBError(SearchServiceError):
    """Raised when vector database operations fail."""
    pass

class DatabaseFetchError(SearchServiceError):
    """Raised when PostgreSQL operations fail."""
    pass

# ============================================================================
# SERVICES
# ============================================================================

class EmbeddingService:
    """
    Service for generating text embeddings using OpenAI API.
    Includes retry logic and input sanitization.
    """

    def __init__(
        self,
        openai_client: AsyncOpenAI,
        model: str = EMBEDDING_MODEL,
        max_retries: int = 3
    ):
        self.client = openai_client
        self.model = model
        self.max_retries = max_retries
        self.logger = logging.getLogger(self.__class__.__name__)

    def _sanitize_input(self, text: str) -> str:
        """Sanitize and normalize input text for embedding."""
        # Remove excessive whitespace, limit length
        cleaned = " ".join(text.strip().split())
        return cleaned[:MAX_QUERY_LENGTH]

    async def embed_query(self, query: str) -> List[float]:
        """
        Generate embedding vector for search query with retry logic.

        Args:
            query: Raw search query string

        Returns:
            Embedding vector as list of floats

        Raises:
            EmbeddingServiceError: If embedding generation fails after retries
        """
        cleaned_query = self._sanitize_input(query)
        last_exception = None

        for attempt in range(self.max_retries):
            try:
                response: CreateEmbeddingResponse = await self.client.embeddings.create(
                    input=cleaned_query,
                    model=self.model,
                    dimensions=EMBEDDING_DIMENSIONS
                )

                if not response.data or not response.data[0].embedding:
                    raise ValueError("Empty embedding received from API")

                self.logger.debug(
                    f"Embedding generated successfully (attempt {attempt + 1})"
                )
                return response.data[0].embedding

            except RateLimitError as e:
                last_exception = e
                wait_time = 2 ** attempt  # Exponential backoff
                self.logger.warning(f"Rate limit hit, waiting {wait_time}s...")
                await asyncio.sleep(wait_time)

            except OpenAIError as e:
                last_exception = e
                self.logger.error(f"OpenAI API error: {str(e)}")
                break  # Don't retry on client errors

            except Exception as e:
                last_exception = e
                self.logger.error(f"Unexpected embedding error: {str(e)}")
                break

        raise EmbeddingServiceError(
            f"Failed to generate embedding after {self.max_retries} attempts: {str(last_exception)}"
        )

class VectorSearchService:
    """
    Service for vector similarity search using Qdrant.
    Handles metadata filtering and hybrid search capabilities.
    """

    def __init__(
        self,
        qdrant_client: AsyncQdrantClient,
        collection_name: str


Conversation Parser - Gemini Export Format