Преглед на файлове

TG-37: Backend preparation and data exposure for macronutrients

FerRo988 преди 3 седмици
родител
ревизия
b651e06c10
променени са 5 файла, в които са добавени 123 реда и са изтрити 1 реда
  1. 36 0
      database.py
  2. 18 0
      documentation/Sprint4_UI_UX_Decisions.md
  3. 21 0
      documentation/Sprint5_Architecture_Decisions.md
  4. 26 0
      documentation/Sprint6_Dataset_Decisions.md
  5. 22 1
      main.py

+ 36 - 0
database.py

@@ -82,6 +82,18 @@ def create_tables():
         )
         ''')
         
+        # Create minimal user_profiles table for macro targets (US-07)
+        cursor.execute('''
+        CREATE TABLE IF NOT EXISTS user_profiles (
+            user_id INTEGER PRIMARY KEY,
+            target_calories INTEGER DEFAULT 2000,
+            target_protein_g INTEGER DEFAULT 150,
+            target_carbs_g INTEGER DEFAULT 200,
+            target_fat_g INTEGER DEFAULT 65,
+            FOREIGN KEY (user_id) REFERENCES users (id)
+        )
+        ''')
+        
         # Create index for rapid fuzzy search compatibility
         cursor.execute('CREATE INDEX IF NOT EXISTS idx_food_name ON foods(name COLLATE NOCASE)')
         
@@ -254,3 +266,27 @@ def get_user_chat_history(user_id: int, limit: int = 50) -> list[Dict[str, Any]]
         return []
     finally:
         if conn: conn.close()
+def get_user_profile(user_id: int) -> Optional[Dict[str, Any]]:
+    """Fetch the user's profile containing macro targets. Inserts defaults if none exists."""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM user_profiles WHERE user_id = ?", (user_id,))
+        row = cursor.fetchone()
+        
+        if not row:
+            # Create a default profile row if one does not exist
+            cursor.execute('''
+                INSERT INTO user_profiles (user_id) VALUES (?)
+            ''', (user_id,))
+            conn.commit()
+            cursor.execute("SELECT * FROM user_profiles WHERE user_id = ?", (user_id,))
+            row = cursor.fetchone()
+            
+        return dict(row) if row else None
+    except Exception as e:
+        logger.error(f"Error fetching user profile: {e}")
+        return None
+    finally:
+        if conn: conn.close()

+ 18 - 0
documentation/Sprint4_UI_UX_Decisions.md

@@ -0,0 +1,18 @@
+# Sprint 4: UI/UX & Frontend Decisions
+
+## Overview
+Sprint 4 focused on transitioning the application from a basic prototype to a modern, user-friendly web interface while adhering to the local-first philosophy.
+
+## Technical Choices
+*   **Vanilla Stack**: We chose **HTML/Vanilla CSS/Vanilla JavaScript** instead of a heavy framework like React or Vue. 
+    *   *Reasoning*: Reduces dependency overhead, keeps the local server incredibly lightweight, and simplifies deployment on the VM without needing Node.js build steps.
+*   **Design System**:
+    *   Implemented a CSS variables (`:root`) design system for easy theming (Glassmorphism, dark/light modes).
+    *   Added micro-interactions (hover states, smooth fading transitions) to improve perceived performance and user delight without relying on heavy external animation libraries.
+*   **State Management**:
+    *   Used simple DOM manipulation and `localStorage` for managing the auth token and UI transitions between the "Login Screen" and the "Chat Interface."
+
+## Key Deliverables
+*   `static/style.css`: Contains all custom animations and glassmorphism styling.
+*   `static/script.js`: Handles all frontend API interactions (login, chat streaming, searching).
+*   `static/index.html`: The single-page application skeleton.

+ 21 - 0
documentation/Sprint5_Architecture_Decisions.md

@@ -0,0 +1,21 @@
+# Sprint 5: Backend Architecture & Local LLM Integration
+
+## Overview
+Sprint 5 established the core backend infrastructure required to operate a fully local, privacy-first AI application without relying on external cloud APIs.
+
+## Technical Choices
+*   **Backend Framework**: **FastAPI** (Python)
+    *   *Reasoning*: Chosen for its native asynchronous capabilities (`async`/`await`), which are critical for handling streaming responses from the LLM without blocking other user requests. It also automatically generates OpenAPI documentation.
+*   **Database**: **SQLite**
+    *   *Reasoning*: Selected over heavy databases like PostgreSQL because this is a local application running on a constrained VM. SQLite is incredibly fast for read-heavy operations, requires zero setup, and naturally aligns with the "local file" privacy philosophy.
+    *   *Optimization*: Enabled `WAL` (Write-Ahead Logging) mode and `check_same_thread=False` to allow FastAPI's asynchronous workers to read/write concurrently without immediate locking issues.
+*   **AI Engine**: **Llama 3.1 8B via Ollama**
+    *   *Reasoning*: Selected because it provides exceptional natural language understanding while fitting within the RAM constraints of the local VM. Ollama handles the model serving locally on port `11434`.
+*   **Authentication**: **Custom Token-Based Auth**
+    *   *Reasoning*: Rather than relying on external OAuth providers (Google/Auth0), we built a local token system (`secrets.token_urlsafe`) stored in the SQLite database to ensure zero data leaves the server.
+
+## RAG (Retrieval-Augmented Generation) Design
+To prevent the LLM from hallucinating nutritional facts:
+1.  **Keyword Extraction**: The user's query is filtered for stopwords (e.g., "protein in salmon" -> ["protein", "salmon"]).
+2.  **Fuzzy Search**: The SQLite database (`foods` table) is queried using `LIKE` pattern matching.
+3.  **Context Injection**: The resulting verified nutritional data is secretly prepended to the user's prompt as a "System Message", forcing the AI to base its answer on local, hard data.

+ 26 - 0
documentation/Sprint6_Dataset_Decisions.md

@@ -0,0 +1,26 @@
+# Sprint 6: Dataset & Chat History Decisions
+
+## Overview
+This document provides details regarding the choices made during Sprint 6, specifically the transition to a massive nutritional dataset and the implementation of chat history persistence.
+
+## 1. Dataset Expansion (USDA SR Legacy)
+To improve the breadth and accuracy of the application's local search and RAG (Retrieval-Augmented Generation) capabilities, the system transitioned from a small, manually curated dataset (142 items) to the comprehensive **USDA National Nutrient Database for Standard Reference, Legacy Release (SR Legacy)**.
+
+### Specifications
+*   **Original Source:** USDA FoodData Central (SR Legacy Release, April 2018)
+*   **Methodology (The "MyFoodData" Approach):** MyFoodData.com provides a pre-cleaned, spreadsheet-friendly version of the complex USDA database. Because downloading third-party spreadsheets directly to a Linux server is unreliable, we downloaded the raw, official USDA data and built a custom script (`mega_seed_usda.py`) to clean and flatten it locally. This gives us the exact same clean, 7,700-item structure as MyFoodData, but directly from the government source.
+*   **Total Items Ingested:** 7,793 unique food items
+*   **Included Metrics:** Calories, Protein, Total Fat, Carbohydrates, Fiber, Sugar, Sodium.
+
+### Technical Implementation & Search Optimization
+*   **Storage:** Flattened data stored in a high-performance SQLite database (`localfood.db`).
+*   **Performance:** Food names are indexed using `COLLATE NOCASE`. The search algorithm prioritizes exact prefixes, penalizes the "Baby Foods" category (which tends to crowd out general searches like "Chicken"), and sorts by name length to ensure base ingredients appear before obscure variants.
+*   **LLM Context Limit (Performance Fix):** To maintain responsiveness with the local Llama 3.1 8B model running on a CPU, context injection is strictly limited. Only the **top 3 most relevant items** (names truncated to 100 characters, core macros only) are injected into the AI's prompt.
+
+## 2. Chat History Persistence
+To enhance the user experience, chat history was moved from volatile browser memory to permanent local storage.
+
+### Technical Choices
+*   **Database Table:** A new `chat_messages` table was added, linked to the `users` table via `user_id`.
+*   **Transaction Safety:** Encountered `database is locked` errors due to concurrent RAG reads and history writes. Resolved by implementing strict `try...finally` blocks around all database connections to prevent connection leaking during failed operations.
+*   **Context Window Limitation:** The backend now strictly limits the conversational memory sent to the LLM to the **last 6 messages**. This drastically improves initial generation time on the CPU without losing immediate conversational context.

+ 22 - 1
main.py

@@ -4,7 +4,7 @@ import httpx
 import bcrypt
 from contextlib import asynccontextmanager
 from fastapi import FastAPI, HTTPException, Depends, Header
-from database import create_tables, create_user, get_user_by_username, create_session, get_user_from_token, delete_session, search_foods_by_name, save_chat_message, get_user_chat_history
+from database import create_tables, create_user, get_user_by_username, create_session, get_user_from_token, delete_session, search_foods_by_name, save_chat_message, get_user_chat_history, get_user_profile
 from fastapi.responses import HTMLResponse, StreamingResponse
 from fastapi.staticfiles import StaticFiles
 from pydantic import BaseModel
@@ -173,6 +173,27 @@ async def logout(authorization: Optional[str] = Header(None)):
         delete_session(token)
     return {"message": "Logged out successfully"}
 
+@app.get("/api/macros/targets")
+async def get_macro_targets(current_user: dict = Depends(get_current_user)):
+    """API endpoint to securely fetch the user's current macronutrient targets"""
+    profile = get_user_profile(current_user['id'])
+    
+    if not profile:
+        # Fallback to defaults in case database insertion failed
+        return {
+            "calories": 2000,
+            "protein_g": 150,
+            "carbs_g": 200,
+            "fat_g": 65
+        }
+        
+    return {
+        "calories": profile.get("target_calories", 2000),
+        "protein_g": profile.get("target_protein_g", 150),
+        "carbs_g": profile.get("target_carbs_g", 200),
+        "fat_g": profile.get("target_fat_g", 65)
+    }
+
 @app.post("/chat")
 async def chat_endpoint(request: ChatRequest, current_user: dict = Depends(get_current_user)):
     """Proxy chat requests to the local Ollama instance with streaming support.