GitHub - Seydulla/ailab_hack_backend: Kinda Fit. Hackathon project

AI-Powered Fitness Assistant Backend

An intelligent fitness coaching system that provides personalized workout recommendations using AI-driven conversational interfaces, vector similarity search, and real-time data synchronization.

πŸ“‹ Table of Contents

🎯 Overview

This backend service powers an AI fitness assistant that:

  1. Collects user profiles through natural conversation (age, weight, height, goals, injuries, etc.)
  2. Generates personalized workouts using AI and vector similarity search
  3. Tracks workout performance with detailed metrics and AI-generated summaries
  4. Learns from past sessions to improve future recommendations
  5. Auto-syncs data between PostgreSQL and Qdrant vector database in real-time

πŸ—οΈ Architecture

System Components

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Client Application                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚ HTTP/REST
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Express.js API Server                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚            Workflow Engine (workflow.ts)                  β”‚  β”‚
β”‚  β”‚  β€’ Profile Intake  β€’ Exercise Recommendation             β”‚  β”‚
β”‚  β”‚  β€’ Confirmations   β€’ Summary Generation                  β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  β”‚              β”‚              β”‚              β”‚
  β”‚ Gemini AI    β”‚ PostgreSQL   β”‚ Qdrant      β”‚ Redis
  β”‚ (LLM + Embed)β”‚ (Primary DB) β”‚ (Vector DB) β”‚ (Sessions)
  β”‚              β”‚              β”‚              β”‚
β”Œβ”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Google    β”‚ β”‚ PostgreSQL β”‚ β”‚  Qdrant    β”‚ β”‚   Redis    β”‚
β”‚  Gemini    β”‚ β”‚            β”‚ β”‚            β”‚ β”‚            β”‚
β”‚  - 2.0     β”‚ β”‚ - Users    β”‚ β”‚ - Exercise β”‚ β”‚ - Session  β”‚
β”‚    Flash   β”‚ β”‚ - Exercise β”‚ β”‚   Vectors  β”‚ β”‚   State    β”‚
β”‚  - Text    β”‚ β”‚ - Sessions β”‚ β”‚ - Workout  β”‚ β”‚ - History  β”‚
β”‚    Embed   β”‚ β”‚ - Results  β”‚ β”‚   Vectors  β”‚ β”‚ - TTL 24h  β”‚
β”‚    004     β”‚ β”‚            β”‚ β”‚            β”‚ β”‚            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”
              β”‚  PostgreSQL  β”‚
              β”‚  LISTEN/     β”‚
              β”‚  NOTIFY      β”‚
              β”‚  Triggers    β”‚
              β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚  Trigger Listener    β”‚
              β”‚  (triggerListener.ts)β”‚
              β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚                          β”‚
    β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”
    β”‚ Exercise    β”‚        β”‚  Workout     β”‚
    β”‚ Sync        β”‚        β”‚  Sync        β”‚
    β”‚ (DB β†’ Vec)  β”‚        β”‚  (DB β†’ Vec)  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Component Responsibilities

Express.js API Server (src/index.ts, src/app.ts)

  • Handles HTTP requests and routing
  • Manages connections to all services
  • Graceful shutdown handling

Workflow Engine (src/services/workflow.ts)

  • Orchestrates multi-step conversational flow
  • Manages state transitions
  • Integrates AI responses with data validation

Session Manager (src/services/session.ts)

  • Redis-based session storage
  • 24-hour TTL for automatic cleanup
  • Maintains conversation history and user context

Qdrant Services (src/services/qdrant.ts)

  • Initializes vector collections
  • Manages exercise and workout embeddings

Sync Services

  • Exercise Sync (src/services/exerciseSync.ts): Syncs exercises from PostgreSQL to Qdrant
  • Workout Sync (src/services/workoutSync.ts): Syncs completed workouts and searches similar sessions

Trigger Listener (src/services/triggerListener.ts)

  • Listens to PostgreSQL NOTIFY events
  • Automatically triggers sync operations
  • Reconnects on connection loss

Utilities (src/utils.ts)

  • Text embedding generation
  • TOON format encoding/decoding (compact JSON alternative)
  • Profile data extraction and validation
  • AI summary generation
  • Retry logic for reliability

πŸ› οΈ Technologies

Core Stack

Technology Purpose Version
Node.js Runtime environment 22+
TypeScript Type-safe JavaScript 5.7.2
Express HTTP server framework 4.21.1

Databases & Storage

Technology Purpose Details
PostgreSQL Primary database 18-alpine, stores exercises, sessions, results
Qdrant Vector database Stores embeddings for similarity search
Redis Session cache 7-alpine, 24h TTL for session state

AI & Machine Learning

Technology Purpose Details
Google Gemini 2.0 Flash LLM for conversations Profile intake, recommendations, summaries
Text-Embedding-004 Text embeddings 768-dim vectors for similarity search
TOON Format Compact data encoding More efficient than JSON for AI prompts

Infrastructure

Technology Purpose Details
Docker Containerization Multi-service orchestration
Nginx Reverse proxy SSL termination, load balancing
PM2 Process manager Production app management

✨ Key Features

1. AI-Powered Conversational Interface

  • Natural language profile collection
  • Context-aware responses
  • Multi-turn conversation handling
  • Confirmation/cancellation flows

2. Intelligent Exercise Recommendations

  • Vector similarity search for exercise matching
  • Injury-aware filtering (excludes affected body parts)
  • Past workout analysis for personalized suggestions
  • Difficulty level adaptation

3. Real-Time Data Synchronization

  • PostgreSQL triggers automatically notify changes
  • Background listener processes sync events
  • Exercises sync to Qdrant on INSERT/UPDATE/DELETE
  • Workout sessions sync on completion
  • Retry logic for reliability

4. Personalized Workout Learning

  • Stores past workout performance
  • Searches similar sessions using vector embeddings
  • Uses historical data to improve recommendations
  • Considers accuracy scores, mistakes, and completion rates

5. Comprehensive Performance Tracking

  • Per-exercise metrics (reps, duration, calories, mistakes)
  • Overall session metrics (completion %, accuracy, efficiency)
  • AI-generated encouraging summaries
  • Mistake tracking with counts

πŸ”„ System Flows

Main User Workflow (Sequential)

These flows represent the sequential journey a user takes from starting a conversation to completing a workout.

Flow 1: Profile Intake

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   User      β”‚
β”‚  "I want    β”‚
β”‚  to start"  β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Workflow: PROFILE_INTAKE            β”‚
β”‚                                      β”‚
β”‚  1. Check existing session (Redis)  β”‚
β”‚  2. Create/load session state       β”‚
β”‚  3. Send to Gemini AI with prompt  β”‚
β”‚  4. Extract profile data if presentβ”‚
β”‚  5. Validate completeness          β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”œβ”€β”€ Incomplete ──┐
       β”‚                β”‚
       β”‚                β–Ό
       β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚         β”‚ Ask for more β”‚
       β”‚         β”‚ information  β”‚
       β”‚         β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                β”‚
       β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”œβ”€β”€ Complete
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Workflow: PROFILE_CONFIRMATION     β”‚
β”‚                                     β”‚
β”‚  1. Store profile in session        β”‚
β”‚  2. Present data to user            β”‚
β”‚  3. Request confirmation            β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”œβ”€β”€ User: "CONFIRM"
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Proceed to Exercise                β”‚
β”‚  Recommendation                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Flow 2: Exercise Recommendation

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Workflow: EXERCISE_RECOMMENDATION   β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Step 1: Search Query Refinement     β”‚
β”‚                                       β”‚
β”‚  β€’ Send profile + injuries to AI     β”‚
β”‚  β€’ AI generates optimized query      β”‚
β”‚  β€’ AI identifies body parts to       β”‚
β”‚    exclude based on injuries         β”‚
β”‚                                       β”‚
β”‚  Example:                            β”‚
β”‚    Injury: "knee pain"               β”‚
β”‚    β†’ Exclude: ["knees", "legs"]      β”‚
β”‚    β†’ Query: "upper body strength"    β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Step 2: Find Similar Past Workouts  β”‚
β”‚                                       β”‚
β”‚  β€’ Embed refined query (768-dim)     β”‚
β”‚  β€’ Search Qdrant workout_sessions    β”‚
β”‚  β€’ Filter by user_id                 β”‚
β”‚  β€’ Get top 5 similar sessions        β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Step 3: Retrieve Past Performance   β”‚
β”‚                                       β”‚
β”‚  β€’ Query PostgreSQL for session      β”‚
β”‚    details (exercises, metrics)      β”‚
β”‚  β€’ Include: accuracy, mistakes,      β”‚
β”‚    completion %, calories burned     β”‚
β”‚  β€’ Encode to TOON format (compact)   β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Step 4: Search Exercise Database    β”‚
β”‚                                       β”‚
β”‚  β€’ Search Qdrant exercises           β”‚
β”‚  β€’ Use refined query embedding       β”‚
β”‚  β€’ Apply body part filters           β”‚
β”‚  β€’ Get top 50 candidates             β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Step 5: AI Workout Generation       β”‚
β”‚                                       β”‚
β”‚  Prompt includes:                    β”‚
β”‚  β€’ Profile data (TOON)               β”‚
β”‚  β€’ Similar sessions data (TOON)      β”‚
β”‚  β€’ Available exercises (TOON)        β”‚
β”‚                                       β”‚
β”‚  AI decides:                         β”‚
β”‚  β€’ Which exercises to include        β”‚
β”‚  β€’ Reps/duration for each            β”‚
β”‚  β€’ Rest periods                      β”‚
β”‚  β€’ Exercise order                    β”‚
β”‚  β€’ Warm-up/cool-down phases          β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Step 6: Exercise Validation         β”‚
β”‚                                       β”‚
β”‚  β€’ Parse AI response (TOON/JSON)     β”‚
β”‚  β€’ Validate each exercise ID         β”‚
β”‚  β€’ Fetch full details from DB        β”‚
β”‚  β€’ Ensure data integrity             β”‚
β”‚  β€’ Skip invalid exercises            β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Workflow: EXERCISE_CONFIRMATION     β”‚
β”‚                                       β”‚
β”‚  β€’ Store recommendations in session  β”‚
β”‚  β€’ Present to user                   β”‚
β”‚  β€’ Request confirmation              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Flow 3: Workout Completion

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  User completes workout         β”‚
β”‚  Mobile app sends results       β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  POST /api/workflow                      β”‚
β”‚  step: EXERCISE_SUMMARY                  β”‚
β”‚                                          β”‚
β”‚  Body (as JSON string):                  β”‚
β”‚  {                                       β”‚
β”‚    target_duration_seconds: 1800,        β”‚
β”‚    completed_reps_count: 85,             β”‚
β”‚    target_reps_count: 100,               β”‚
β”‚    calories_burned: 250.5,               β”‚
β”‚    completion_percentage: 85.0,          β”‚
β”‚    total_mistakes: 12,                   β”‚
β”‚    accuracy_score: 88.5,                 β”‚
β”‚    efficiency_score: 82.3,               β”‚
β”‚    total_exercise: 5,                    β”‚
β”‚    exercises: [                          β”‚
β”‚      {                                   β”‚
β”‚        exercise_id: "ex_001",            β”‚
β”‚        exercise_title: "Squats",         β”‚
β”‚        time_spent: 300,                  β”‚
β”‚        repeats: 3,                       β”‚
β”‚        total_reps: 36,                   β”‚
β”‚        calories: 80.2,                   β”‚
β”‚        mistakes: [...],                  β”‚
β”‚        average_accuracy: 0.92            β”‚
β”‚      },                                  β”‚
β”‚      ...                                 β”‚
β”‚    ],                                    β”‚
β”‚    notes: "Felt good overall"            β”‚
β”‚  }                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Workflow: processExerciseSummary        β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Step 1: Generate AI Summary             β”‚
β”‚                                          β”‚
β”‚  β€’ Send results to Gemini                β”‚
β”‚  β€’ AI analyzes performance               β”‚
β”‚  β€’ Creates encouraging summary           β”‚
β”‚  β€’ Highlights strengths/improvements     β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Step 2: PostgreSQL Transaction          β”‚
β”‚                                          β”‚
β”‚  BEGIN;                                  β”‚
β”‚                                          β”‚
β”‚  1. INSERT past_sessions                 β”‚
β”‚     β†’ Returns session ID (UUID)          β”‚
β”‚                                          β”‚
β”‚  2. INSERT session_exercises             β”‚
β”‚     β†’ Links exercises to session         β”‚
β”‚     β†’ Maintains order                    β”‚
β”‚                                          β”‚
β”‚  3. INSERT session_exercise_results      β”‚
β”‚     β†’ Stores per-exercise metrics        β”‚
β”‚     β†’ Stores mistakes as JSONB           β”‚
β”‚                                          β”‚
β”‚  COMMIT;                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”‚ (Automatically triggers sync)
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PostgreSQL Trigger Fires                β”‚
β”‚  β†’ past_session_insert_trigger           β”‚
β”‚  β†’ NOTIFY past_session_changes           β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Trigger Listener Receives Event         β”‚
β”‚  β†’ Calls syncWorkoutSessionToQdrant()    β”‚
β”‚  β†’ Stores workout embedding in Qdrant    β”‚
β”‚  β†’ Tagged with user_id for filtering     β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Step 3: Clear Session                   β”‚
β”‚                                          β”‚
β”‚  β€’ Delete from Redis                     β”‚
β”‚  β€’ Session complete                      β”‚
β”‚  β€’ Ready for new workflow                β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Return AI Summary to User               β”‚
β”‚                                          β”‚
β”‚  Example:                                β”‚
β”‚  "Great workout! You completed 85%       β”‚
β”‚   with 88.5% accuracy. Squats looked     β”‚
β”‚   excellent with 92% form. Watch for     β”‚
β”‚   hip sagging in push-ups. Keep it up!" β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Background Processes (Automatic)

These processes run continuously in the background, independent of user interactions, to keep data synchronized across systems.

Flow 4: Real-Time Data Sync

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PostgreSQL: INSERT/UPDATE/      β”‚
β”‚  DELETE on exercises table       β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Trigger: notify_exercise_changeβ”‚
β”‚                                  β”‚
β”‚  β€’ Captures exercise_id          β”‚
β”‚  β€’ Identifies operation type     β”‚
β”‚  β€’ Sends NOTIFY event            β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Trigger Listener                β”‚
β”‚  (Always running)                β”‚
β”‚                                  β”‚
β”‚  β€’ Receives NOTIFY               β”‚
β”‚  β€’ Parses payload                β”‚
β”‚  β€’ Routes to handler             β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”œβ”€β”€ INSERT/UPDATE
       β”‚   β”‚
       β”‚   β–Ό
       β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚  β”‚ Exercise Sync           β”‚
       β”‚  β”‚                         β”‚
       β”‚  β”‚ 1. Fetch from DB        β”‚
       β”‚  β”‚ 2. Build embedding text β”‚
       β”‚  β”‚ 3. Generate embedding   β”‚
       β”‚  β”‚ 4. Upsert to Qdrant     β”‚
       β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       └── DELETE
           β”‚
           β–Ό
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚ Delete from Qdrant      β”‚
          β”‚                         β”‚
          β”‚ β€’ Remove vector point   β”‚
          β”‚ β€’ Maintain consistency  β”‚
          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PostgreSQL: INSERT on           β”‚
β”‚  past_sessions table             β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Trigger: notify_past_session   β”‚
β”‚  _change                         β”‚
β”‚                                  β”‚
β”‚  β€’ Captures session details      β”‚
β”‚  β€’ Sends NOTIFY event            β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Workout Sync                    β”‚
β”‚                                  β”‚
β”‚  1. Fetch session exercises      β”‚
β”‚  2. Build combined embedding     β”‚
β”‚  3. Generate embedding           β”‚
β”‚  4. Store in Qdrant with         β”‚
β”‚     user_id and session_id       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

This background process ensures that:

  • Any exercise added/updated in PostgreSQL is automatically synced to Qdrant
  • Any completed workout session is automatically embedded and stored in Qdrant
  • The vector database stays in sync with the relational database
  • No manual intervention is required

πŸ” Internal Processes

Profile Data Extraction

The system uses a structured approach to extract profile information from conversational text:

  1. AI Response Format: Gemini is instructed to output profile data in a structured format:
<PROFILE_DATA>
{
  age: 28,
  weight: 75,
  height: 180,
  gender: "MALE",
  goals: "build muscle",
  injuries: "none",
  lifestyle: "active",
  equipment: "gym access"
}
</PROFILE_DATA>
  1. Extraction Logic (utils.ts::extractProfileData):
  • Searches for <PROFILE_DATA> tags in AI response
  • Attempts TOON decoding first (compact format)
  • Falls back to JSON parsing
  • Validates each field individually
  • Returns partial profile if incomplete
  1. Validation (utils.ts::isProfileComplete):
  • Checks all required fields: age, weight, height, gender, goals, injuries
  • Optional fields: lifestyle, equipment
  • Returns boolean for workflow decision

Vector Embedding Generation

Purpose: Convert text to 768-dimensional vectors for similarity search

Process:

  1. Exercise Embeddings (utils.ts::buildEmbeddingText):
// Combines multiple fields
const embeddingText = [
  exercise.title, // "Squats"
  exercise.description, // "A compound lower body..."
  exercise.body_parts, // "legs, glutes, core"
  exercise.dif_level, // "MEDIUM"
  exercise.common_mistakes, // "Knees caving inward..."
  exercise.position, // "STANDING"
  exercise.steps, // "1. Stand with feet..."
  exercise.tips, // "Keep knees aligned..."
].join('\n');

// Send to Google's text-embedding-004
const embedding = await embedText(embeddingText);
// Returns: [0.123, -0.456, 0.789, ...] (768 numbers)
  1. Workout Session Embeddings (workoutSync.ts::buildWorkoutEmbeddingText):
// Fetches all exercises in a completed workout
// Builds embeddings for each (without common_mistakes)
// Concatenates all exercise texts
// Generates single embedding representing entire workout
  1. Profile Query Embeddings (workflow.ts::processExerciseRecommendation):
// Sends profile to AI for refinement
// AI optimizes search query based on goals/injuries
// Embeds refined query
// Uses for similarity search

TOON Format Encoding

Why TOON? More compact than JSON, reducing token usage in AI prompts

Example:

// Original data
const data = {
  ex1: { exerciseId: 'squat_001', reps: 12, duration: null },
  ex2: { exerciseId: 'pushup_002', reps: 10, duration: null },
};

// JSON: 124 characters
JSON.stringify(data);

// TOON: ~80 characters (35% smaller)
toonEncode(data);

Usage in Project:

  • Encoding profile data for AI prompts
  • Encoding exercise lists for recommendations
  • Encoding past session data for context
  • Decoding AI responses with structured data

Injury-Based Exercise Filtering

Objective: Prevent recommending exercises that could aggravate injuries

Implementation:

  1. Query Refinement (utils.ts::refineSearchQueryWithGemini):
// Input: "Age: 28, Goals: build muscle, Injuries: knee pain"
const response = await gemini.sendMessage({
  message: `User profile: ${profileText}
             Injuries: ${injuries}
             Create refined search query and identify body parts to exclude.`
});

// AI returns:
{
  refinedQuery: "upper body strength exercises, core stability",
  excludeBodyParts: ["knees", "legs", "lower body"]
}
  1. Qdrant Filtering (workflow.ts::processExerciseRecommendation):
const searchOptions = {
  vector: embedding,
  limit: 50,
  filter: {
    must_not: [
      {
        key: 'bodyParts',
        match: {
          any: ['knees', 'legs', 'lower body'],
        },
      },
    ],
  },
};

// Only returns exercises that DON'T target excluded body parts
const results = await qdrant.search(EXERCISES_COLLECTION_NAME, searchOptions);

Session State Management

Storage: Redis with 24-hour TTL

Session Structure:

interface SessionState {
  userId: string;
  step: WorkflowStep; // Current stage
  conversationHistory: Message[]; // All messages
  profileData?: IUserProfile; // After collection
  exerciseRecommendations?: IExercise[]; // After generation
  selectedExercises?: IExercise[]; // After confirmation
  createdAt: string;
  updatedAt: string;
}

Operations:

  1. Get Session (session.ts::getSession):
const sessionKey = `session:${sessionId}`;
const data = await redisClient.get(sessionKey);
return data ? JSON.parse(data) : null;
  1. Set Session (session.ts::setSession):
const sessionKey = `session:${sessionId}`;
await redisClient.setEx(
  sessionKey,
  86400, // 24 hours
  JSON.stringify(session)
);
  1. Update Session (session.ts::updateSession):
// Partial update - merges with existing
const existingSession = await getSession(sessionId);
const updatedSession = { ...existingSession, ...updates };
await setSession(sessionId, updatedSession);
  1. Auto-Cleanup: Redis automatically deletes sessions after 24 hours of inactivity

Retry Logic

Purpose: Handle transient failures in external services

Implementation (utils.ts::withRetry):

await withRetry(
  async () => {
    // Operation that might fail
    return await qdrantClient.upsert(...);
  },
  {
    maxRetries: 3,        // Try 3 times
    delayMs: 1000,        // Wait 1s, 2s, 3s between attempts
    operationName: 'sync exercise to Qdrant'
  }
);

Used for:

  • Qdrant upsert operations
  • Embedding generation
  • Database queries in sync operations

PostgreSQL Triggers

Purpose: Automatically sync data changes to Qdrant without manual intervention

Exercise Trigger (schema.sql):

-- Function
CREATE OR REPLACE FUNCTION notify_exercise_change()
RETURNS TRIGGER AS $$
BEGIN
  PERFORM pg_notify(
    'exercise_changes',
    json_build_object(
      'exercise_id', NEW.id,
      'operation', TG_OP
    )::text
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Triggers (INSERT, UPDATE, DELETE)
CREATE TRIGGER exercise_insert_trigger
  AFTER INSERT ON exercises
  FOR EACH ROW
  EXECUTE FUNCTION notify_exercise_change();

Workflow Session Trigger:

CREATE TRIGGER past_session_insert_trigger
  AFTER INSERT ON past_sessions
  FOR EACH ROW
  EXECUTE FUNCTION notify_past_session_change();

Listener (triggerListener.ts):

// Dedicated PostgreSQL connection for LISTEN
client.query('LISTEN exercise_changes');
client.query('LISTEN past_session_changes');

client.on('notification', async msg => {
  if (msg.channel === 'exercise_changes') {
    const { exercise_id, operation } = JSON.parse(msg.payload);
    if (operation === 'DELETE') {
      await deleteExerciseFromQdrant(exercise_id);
    } else {
      await syncExerciseToQdrant(exercise_id);
    }
  }
});

πŸ“Š Data Flow

Exercise Data Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Admin adds      β”‚
β”‚  new exercise    β”‚
β”‚  to PostgreSQL   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  exercises Table                          β”‚
β”‚  β€’ id (UUID)                              β”‚
β”‚  β€’ external_id (unique)                   β”‚
β”‚  β€’ title, description, body_parts         β”‚
β”‚  β€’ difficulty, position, steps, tips      β”‚
β”‚  β€’ thumbnail_URL, video_URL               β”‚
β”‚  β€’ male_thumbnail_URL, male_video_URL     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Trigger: exercise_insert_trigger         β”‚
β”‚  β€’ Fires on INSERT                        β”‚
β”‚  β€’ Sends pg_notify()                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Listener: handleExerciseNotification()  β”‚
β”‚  β€’ Parses notification                    β”‚
β”‚  β€’ Calls syncExerciseToQdrant()           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  exerciseSync.ts                          β”‚
β”‚  1. Fetch exercise from DB                β”‚
β”‚  2. Build embedding text:                 β”‚
β”‚     title + description + body_parts +    β”‚
β”‚     difficulty + mistakes + position +    β”‚
β”‚     steps + tips                          β”‚
β”‚  3. Generate 768-dim embedding            β”‚
β”‚  4. Parse body_parts & steps to arrays    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Qdrant: exercises Collection             β”‚
β”‚  β€’ Point ID: exercise UUID                β”‚
β”‚  β€’ Vector: [0.123, -0.456, ...] (768)     β”‚
β”‚  β€’ Payload:                               β”‚
β”‚    - external_id                          β”‚
β”‚    - title                                β”‚
β”‚    - bodyParts: ["legs", "glutes"]        β”‚
β”‚    - description                          β”‚
β”‚    - difLevel: "MEDIUM"                   β”‚
β”‚    - commonMistakes                       β”‚
β”‚    - position: "STANDING"                 β”‚
β”‚    - steps: ["Stand...", "Lower..."]      β”‚
β”‚    - tips                                 β”‚
β”‚    - timestamps                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β”‚ (Later: Vector search)
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Workflow: Exercise Recommendation        β”‚
β”‚  β€’ Search by profile embedding            β”‚
β”‚  β€’ Filter by injury exclusions            β”‚
β”‚  β€’ Return top 50 matches                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Workout Session Data Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  User completes  β”‚
β”‚  workout         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  POST /api/workflow                       β”‚
β”‚  β€’ userId, sessionId                      β”‚
β”‚  β€’ Exercise results JSON                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  processExerciseSummary()                 β”‚
β”‚  1. Generate AI summary                   β”‚
β”‚  2. Begin transaction                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PostgreSQL Transaction                   β”‚
β”‚                                           β”‚
β”‚  INSERT past_sessions                     β”‚
β”‚    session_id: "session_abc"              β”‚
β”‚    user_id: "user_123"                    β”‚
β”‚    date: NOW()                            β”‚
β”‚    notes: AI summary                      β”‚
β”‚    accuracy_score: 88.5                   β”‚
β”‚    efficiency_score: 82.3                 β”‚
β”‚    completion_percentage: 85.0            β”‚
β”‚    calories_burned: 250.5                 β”‚
β”‚    ... (other metrics)                    β”‚
β”‚    RETURNING id β†’ session_db_id           β”‚
β”‚                                           β”‚
β”‚  INSERT session_exercises (for each)      β”‚
β”‚    session_id: session_db_id              β”‚
β”‚    exercise_id: "squat_001"               β”‚
β”‚    order_index: 0                         β”‚
β”‚                                           β”‚
β”‚  INSERT session_exercise_results (each)   β”‚
β”‚    session_id: session_db_id              β”‚
β”‚    exercise_id: "squat_001"               β”‚
β”‚    exercise_title: "Squats"               β”‚
β”‚    time_spent: 300                        β”‚
β”‚    repeats: 3                             β”‚
β”‚    total_reps: 36                         β”‚
β”‚    calories: 80.2                         β”‚
β”‚    mistakes: JSONB                        β”‚
β”‚    average_accuracy: 0.92                 β”‚
β”‚    order_index: 0                         β”‚
β”‚                                           β”‚
β”‚  COMMIT                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β”‚ (Trigger fires)
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Trigger: past_session_insert_trigger     β”‚
β”‚  β€’ Fires on INSERT to past_sessions       β”‚
β”‚  β€’ Sends pg_notify() with:                β”‚
β”‚    - session_id (DB UUID)                 β”‚
β”‚    - user_id                              β”‚
β”‚    - original_session_id (session_abc)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Listener: handlePastSessionNotification()β”‚
β”‚  β€’ Parses notification                    β”‚
β”‚  β€’ Calls syncWorkoutSessionToQdrant()     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  workoutSync.ts                           β”‚
β”‚  1. Query session exercises from DB:      β”‚
β”‚     SELECT e.* FROM exercises e           β”‚
β”‚     INNER JOIN session_exercise_results   β”‚
β”‚     WHERE session_id = ?                  β”‚
β”‚     ORDER BY order_index                  β”‚
β”‚                                           β”‚
β”‚  2. Build embedding text for each:        β”‚
β”‚     title + description + body_parts +    β”‚
β”‚     difficulty + position + steps + tips  β”‚
β”‚     (Note: NO common_mistakes)            β”‚
β”‚                                           β”‚
β”‚  3. Concatenate all exercise texts        β”‚
β”‚  4. Generate single 768-dim embedding     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Qdrant: workout_sessions Collection      β”‚
β”‚  β€’ Point ID: session DB UUID              β”‚
β”‚  β€’ Vector: [0.789, 0.234, ...] (768)      β”‚
β”‚  β€’ Payload:                               β”‚
β”‚    - user_id: "user_123"                  β”‚
β”‚    - session_id: "session_abc"            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β”‚ (Later: Next workout request)
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Exercise Recommendation Flow             β”‚
β”‚  β€’ Embed new profile query                β”‚
β”‚  β€’ Search workout_sessions                β”‚
β”‚  β€’ Filter: user_id = "user_123"           β”‚
β”‚  β€’ Find similar past workouts             β”‚
β”‚  β€’ Use performance data for better recs   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸš€ Setup & Installation

Prerequisites

  • Node.js 22+ and Yarn
  • Docker & Docker Compose
  • PostgreSQL 18 (via Docker)
  • Qdrant (via Docker)
  • Redis (via Docker)
  • Google Cloud account (for Gemini API)

Local Development

  1. Clone the repository
git clone <repository-url>
cd ailab_hack_backend
  1. Install dependencies
  1. Create environment file
  1. Configure environment variables
PORT=3000
NODE_ENV=development

# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ailab_hack

# Qdrant
QDRANT_URL=http://localhost:6333

# Redis
REDIS_URL=redis://localhost:6379

# Google Gemini API
GEMINI_API_KEY=your_gemini_api_key_here
  1. Start infrastructure services
# Start PostgreSQL, Qdrant, and Redis
docker-compose up -d

# Check services are running
docker-compose ps
  1. Initialize database schema
# Connect to PostgreSQL
psql postgresql://postgres:postgres@localhost:5432/ailab_hack

# Run schema
\i schema.sql

# (Optional) Insert sample exercises
\i insert_exercises.sql
  1. Start development server

The server will start on http://localhost:3000

Docker Development

Run everything in Docker:

# Start all services including the app
docker-compose -f docker-compose.yml up -d

# View logs
docker-compose logs -f

# Stop all services
docker-compose down

Testing the API

# Health check
curl http://localhost:3000/health

# Start a conversation
curl -X POST http://localhost:3000/api/workflow \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "test_user_1",
    "sessionId": "test_session_1",
    "messages": [
      {
        "role": "user",
        "content": "I want to start a workout program"
      }
    ]
  }'

🌐 Deployment

Production Setup

See DEPLOYMENT.md for detailed production deployment instructions.

Quick overview:

  1. Provision server (Ubuntu 22.04+)
  2. Install dependencies (Node.js, Docker, Nginx)
  3. Clone repository to /var/www/ailab_hack_backend
  4. Configure environment (.env.production)
  5. Set up SSL with Certbot
  6. Configure Nginx as reverse proxy
  7. Start with PM2 for process management

Production Architecture

Internet
   β”‚
   β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Nginx          β”‚  Port 443 (HTTPS)
β”‚  SSL Terminationβ”‚
β”‚  Reverse Proxy  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PM2 Process    β”‚  Port 3000
β”‚  Manager        β”‚
β”‚  (Node.js App)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
    β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚         β”‚         β”‚         β”‚
    β–Ό         β–Ό         β–Ό         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Postgresβ”‚ β”‚ Qdrant β”‚ β”‚ Redis  β”‚ β”‚ Gemini β”‚
β”‚ Docker  β”‚ β”‚ Docker β”‚ β”‚ Docker β”‚ β”‚ API    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Environment Variables (Production)

NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost:5432/ailab_hack
QDRANT_URL=http://localhost:6333
REDIS_URL=redis://localhost:6379
GEMINI_API_KEY=your_production_key

Monitoring & Logs

# PM2 logs
pm2 logs ailab-hack

# Docker logs
docker-compose -f docker-compose.prod.yml logs -f

# Nginx logs
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log

# Database logs
docker-compose -f docker-compose.prod.yml logs postgres

πŸ“š API Documentation

For detailed API documentation including all endpoints, request/response formats, and examples, see:

API_DOCUMENTATION.md

Quick Reference

Endpoint Method Purpose
/health GET Health check
/api/workflow POST Main conversation & workflow endpoint

Workflow Steps:

  1. PROFILE_INTAKE - Collect user information
  2. PROFILE_CONFIRMATION - Confirm profile data
  3. EXERCISE_RECOMMENDATION - Generate workout
  4. EXERCISE_CONFIRMATION - Confirm exercises
  5. EXERCISE_SUMMARY - Submit workout results
  6. COMPLETED - Workflow finished

πŸ—„οΈ Database Schema

Tables

exercises

- id: UUID (primary key)
- external_id: VARCHAR (unique, used for integrations)
- title: VARCHAR
- description: TEXT
- body_parts: TEXT (comma-separated)
- dif_level: ENUM ('EASY', 'MEDIUM', 'HARD')
- common_mistakes: TEXT
- position: ENUM ('STANDING', 'SEATED', 'FLOOR')
- steps: TEXT (newline-separated)
- tips: TEXT
- thumbnail_URL: TEXT
- video_URL: TEXT
- male_thumbnail_URL: TEXT
- male_video_URL: TEXT
- created_at: TIMESTAMP
- updated_at: TIMESTAMP

past_sessions

- id: UUID (primary key)
- session_id: VARCHAR (unique, from client)
- user_id: VARCHAR (indexed)
- date: TIMESTAMP (indexed)
- notes: TEXT (includes AI summary)
- target_duration_seconds: INTEGER
- completed_reps_count: INTEGER
- target_reps_count: INTEGER
- calories_burned: DECIMAL(10, 2)
- completion_percentage: DECIMAL(5, 2)
- total_mistakes: INTEGER
- accuracy_score: DECIMAL(5, 2)
- efficiency_score: DECIMAL(5, 2)
- total_exercise: INTEGER
- actual_hold_time_seconds: INTEGER
- target_hold_time_seconds: INTEGER
- created_at: TIMESTAMP
- updated_at: TIMESTAMP

session_exercises

- id: UUID (primary key)
- session_id: UUID (FK β†’ past_sessions.id)
- exercise_id: VARCHAR (FK β†’ exercises.external_id)
- order_index: INTEGER
- UNIQUE(session_id, order_index)

session_exercise_results

- id: UUID (primary key)
- session_id: UUID (FK β†’ past_sessions.id)
- exercise_id: VARCHAR (FK β†’ exercises.external_id)
- exercise_title: VARCHAR
- time_spent: INTEGER (seconds)
- repeats: INTEGER (sets completed)
- total_reps: INTEGER
- total_duration: INTEGER (for timer-based)
- calories: DECIMAL(10, 2)
- average_accuracy: DECIMAL(3, 2)
- mistakes: JSONB (array of mistake objects)
- order_index: INTEGER
- UNIQUE(session_id, order_index)

Qdrant Collections

exercises

Vector Size: 768 dimensions
Distance: Cosine similarity
Payload:
  - external_id (string)
  - title (string)
  - bodyParts (array of strings)
  - description (string)
  - difLevel (string)
  - commonMistakes (string)
  - position (string)
  - steps (array of strings)
  - tips (string)
  - createdAt (ISO string)
  - updatedAt (ISO string)

workout_sessions

Vector Size: 768 dimensions
Distance: Cosine similarity
Payload:
  - user_id (string, indexed)
  - session_id (string)

Redis Keys

session:{sessionId}
  Value: JSON string of SessionState
  TTL: 86400 seconds (24 hours)