316 lines
10 KiB
Markdown
316 lines
10 KiB
Markdown
|
|
# Phase 10 — LLM Integration & Smart Features
|
||
|
|
|
||
|
|
**Goal**: Wire up the `ILlmProvider` interface (defined in Phase 5) to real LLM backends. Enable all the smart features that have had placeholder endpoints throughout Phases 5-9. Add new natural language interaction capabilities for both medicine and food domains.
|
||
|
|
|
||
|
|
**Depends on**: All previous phases (feature endpoints already exist)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Deliverables
|
||
|
|
|
||
|
|
1. Concrete `ILlmProvider` implementations (OpenAI, Anthropic, Ollama)
|
||
|
|
2. Config-driven provider selection
|
||
|
|
3. LLM request/response logging and cost tracking
|
||
|
|
4. Rate limiting and budget controls
|
||
|
|
5. Smart features fully wired:
|
||
|
|
- Product recognition (photo → nutrition)
|
||
|
|
- Receipt parsing (photo → store + items + prices)
|
||
|
|
- Recipe import (text/URL → structured recipe)
|
||
|
|
- Natural language pantry entry
|
||
|
|
- LLM-powered meal plan suggestions
|
||
|
|
6. Prompt templates and versioning
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
### Provider Selection
|
||
|
|
|
||
|
|
```
|
||
|
|
Environment variable: LLM_PROVIDER_TYPE=openai|anthropic|ollama|noop
|
||
|
|
|
||
|
|
LlmModule registers the provider dynamically:
|
||
|
|
|
||
|
|
@Module({})
|
||
|
|
export class LlmModule {
|
||
|
|
static forRoot(): DynamicModule {
|
||
|
|
return {
|
||
|
|
providers: [{
|
||
|
|
provide: LLM_PROVIDER,
|
||
|
|
useFactory: (config: ConfigService) => {
|
||
|
|
switch (config.get('LLM_PROVIDER_TYPE')) {
|
||
|
|
case 'openai': return new OpenAiProvider(config);
|
||
|
|
case 'anthropic': return new AnthropicProvider(config);
|
||
|
|
case 'ollama': return new OllamaProvider(config);
|
||
|
|
default: return new NoOpLlmProvider();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
inject: [ConfigService],
|
||
|
|
}],
|
||
|
|
exports: [LLM_PROVIDER],
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Provider Implementations
|
||
|
|
|
||
|
|
Each provider implements `ILlmProvider` and handles:
|
||
|
|
|
||
|
|
- API authentication (keys from env)
|
||
|
|
- Model selection (configurable per provider)
|
||
|
|
- Request/response mapping to/from vendor format
|
||
|
|
- Error handling and retries (exponential backoff)
|
||
|
|
- Timeout management
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// packages/api/src/modules/llm/providers/
|
||
|
|
├── noop.provider.ts # Returns null for everything (already exists from Phase 5)
|
||
|
|
├── openai.provider.ts # GPT-4o / GPT-4o-mini
|
||
|
|
├── anthropic.provider.ts # Claude 3.5 Sonnet / Haiku
|
||
|
|
└── ollama.provider.ts # Local models (Llama 3, Mistral, etc.)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Environment Configuration
|
||
|
|
|
||
|
|
```env
|
||
|
|
# Provider selection
|
||
|
|
LLM_PROVIDER_TYPE=openai
|
||
|
|
|
||
|
|
# OpenAI
|
||
|
|
OPENAI_API_KEY=sk-...
|
||
|
|
OPENAI_MODEL=gpt-4o
|
||
|
|
OPENAI_VISION_MODEL=gpt-4o # For image inputs
|
||
|
|
|
||
|
|
# Anthropic
|
||
|
|
ANTHROPIC_API_KEY=sk-ant-...
|
||
|
|
ANTHROPIC_MODEL=claude-sonnet-4-20250514
|
||
|
|
|
||
|
|
# Ollama
|
||
|
|
OLLAMA_BASE_URL=http://ollama:11434
|
||
|
|
OLLAMA_MODEL=llama3
|
||
|
|
OLLAMA_VISION_MODEL=llava
|
||
|
|
|
||
|
|
# Budget
|
||
|
|
LLM_MONTHLY_BUDGET_USD=20.00
|
||
|
|
LLM_RATE_LIMIT_RPM=30 # Requests per minute per household
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Tasks
|
||
|
|
|
||
|
|
### 10.1 — OpenAI Provider
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class OpenAiProvider implements ILlmProvider {
|
||
|
|
// Uses OpenAI Node.js SDK
|
||
|
|
// Text endpoints: chat completions with JSON mode
|
||
|
|
// Vision endpoints: chat completions with image_url or base64 content
|
||
|
|
// Structured output: use function calling or response_format: json_schema
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- `extractNutrition`: Send product text/image → prompt asks for structured nutrition JSON
|
||
|
|
- `parseRecipe`: Send recipe text → prompt extracts name, servings, ingredients[], steps[]
|
||
|
|
- `parseRecipeFromUrl`: Fetch URL content first, then parse as text
|
||
|
|
- `parseReceipt`: Send receipt image → prompt extracts store, date, line items with prices
|
||
|
|
- `suggestMealPlan`: Send pantry summary + targets + preferences → get 7-day plan
|
||
|
|
- `parseNaturalLanguage`: Send user text → extract intent + entities (add product, log purchase, etc.)
|
||
|
|
|
||
|
|
### 10.2 — Anthropic Provider
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class AnthropicProvider implements ILlmProvider {
|
||
|
|
// Uses Anthropic SDK
|
||
|
|
// Similar structure to OpenAI but with Claude-specific API format
|
||
|
|
// Vision: send image as base64 in messages
|
||
|
|
// Structured output: use tool_use for JSON extraction
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 10.3 — Ollama Provider
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class OllamaProvider implements ILlmProvider {
|
||
|
|
// Uses Ollama REST API (http://host:11434/api/generate or /api/chat)
|
||
|
|
// Text: standard chat endpoint
|
||
|
|
// Vision: requires multimodal model (llava, bakllava)
|
||
|
|
// Note: local models may be less accurate — adjust prompts for simpler output
|
||
|
|
// No cost tracking needed (self-hosted)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 10.4 — Prompt Templates
|
||
|
|
|
||
|
|
Create versioned prompt templates in `packages/api/src/modules/llm/prompts/`:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// prompts/extract-nutrition.ts
|
||
|
|
export const EXTRACT_NUTRITION_PROMPT = {
|
||
|
|
version: '1.0',
|
||
|
|
system: `You are a nutrition data extraction assistant. Given a food product description or image, extract nutritional information. Return ONLY valid JSON matching the schema below. If you cannot determine a value, use null. Be conservative with estimates.`,
|
||
|
|
schema: {
|
||
|
|
name: 'string',
|
||
|
|
brand: 'string | null',
|
||
|
|
servingSize: 'number',
|
||
|
|
servingUnit: 'g | ml | oz | piece',
|
||
|
|
nutrition: {
|
||
|
|
calories: 'number',
|
||
|
|
protein: 'number (grams)',
|
||
|
|
carbs: 'number (grams)',
|
||
|
|
fat: 'number (grams)',
|
||
|
|
fiber: 'number | null',
|
||
|
|
sugar: 'number | null',
|
||
|
|
sodium: 'number | null (mg)',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
// prompts/parse-recipe.ts
|
||
|
|
// prompts/parse-receipt.ts
|
||
|
|
// prompts/suggest-meal-plan.ts
|
||
|
|
// prompts/parse-natural-language.ts
|
||
|
|
```
|
||
|
|
|
||
|
|
- Each prompt has a `version` for tracking which prompt produced which results
|
||
|
|
- Prompts are provider-agnostic (providers may wrap them differently)
|
||
|
|
|
||
|
|
### 10.5 — LLM Logging & Cost Tracking
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Schema
|
||
|
|
export interface LlmLog {
|
||
|
|
id: string;
|
||
|
|
householdId: string;
|
||
|
|
userId: string;
|
||
|
|
provider: string; // 'openai' | 'anthropic' | 'ollama'
|
||
|
|
model: string;
|
||
|
|
feature: string; // 'extract_nutrition' | 'parse_recipe' | etc.
|
||
|
|
promptVersion: string;
|
||
|
|
inputTokens: number;
|
||
|
|
outputTokens: number;
|
||
|
|
totalTokens: number;
|
||
|
|
costUsd: number; // Computed from token count + model pricing
|
||
|
|
latencyMs: number;
|
||
|
|
success: boolean;
|
||
|
|
errorMessage?: string;
|
||
|
|
createdAt: Date;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- All provider calls are wrapped in a logging decorator/interceptor
|
||
|
|
- Cost computed from known per-model pricing (configurable)
|
||
|
|
- Monthly cost aggregation endpoint for household admins
|
||
|
|
|
||
|
|
### 10.6 — Rate Limiting & Budget Controls
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
class LlmBudgetGuard {
|
||
|
|
/**
|
||
|
|
* Before each LLM call:
|
||
|
|
* 1. Check per-household rate limit (requests per minute)
|
||
|
|
* 2. Check monthly budget: sum costUsd for current month vs LLM_MONTHLY_BUDGET_USD
|
||
|
|
* 3. If exceeded, throw BudgetExceededException (HTTP 429)
|
||
|
|
*/
|
||
|
|
async canProceed(householdId: string): Promise<boolean>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- Rate limiting: Redis-backed or in-memory (household-level RPM)
|
||
|
|
- Budget: MongoDB aggregation on `LlmLog` collection
|
||
|
|
|
||
|
|
### 10.7 — Wire Up All Feature Endpoints
|
||
|
|
|
||
|
|
Each of these endpoints already exists as a placeholder from earlier phases. Now they get real LLM calls:
|
||
|
|
|
||
|
|
| Feature | Endpoint (existing) | Phase | LLM Method |
|
||
|
|
| --------------------- | ----------------------------------- | ----- | ---------------------- |
|
||
|
|
| Product recognition | `POST /products/smart-add` | 5 | `extractNutrition()` |
|
||
|
|
| Recipe import (text) | `POST /recipes/import-text` | 6 | `parseRecipe()` |
|
||
|
|
| Recipe import (URL) | `POST /recipes/import-url` | 6 | `parseRecipeFromUrl()` |
|
||
|
|
| Receipt parsing | `POST /prices/parse-receipt` | 9 | `parseReceipt()` |
|
||
|
|
| Meal plan suggestions | `POST /meal-plans/suggest-with-llm` | 8 | `suggestMealPlan()` |
|
||
|
|
|
||
|
|
### 10.8 — Natural Language Input (New Feature)
|
||
|
|
|
||
|
|
New universal endpoint:
|
||
|
|
|
||
|
|
```
|
||
|
|
POST /api/v1/nlp/parse
|
||
|
|
Body: { text: string }
|
||
|
|
Response: { intent: string, action: StructuredAction, confidence: number }
|
||
|
|
```
|
||
|
|
|
||
|
|
Supported intents:
|
||
|
|
|
||
|
|
- `add_pantry_item`: "I bought 2 lbs of chicken at Costco for $12" → create pantry item + price record
|
||
|
|
- `add_product`: "Add whole milk, 240ml serving, 150 cal, 8g protein, 12g carbs, 8g fat" → create product
|
||
|
|
- `check_expiry`: "What's expiring this week?" → redirect to pantry query
|
||
|
|
- `find_recipe`: "What can I make with chicken and rice?" → trigger suggestion engine
|
||
|
|
- `add_to_list`: "Add eggs and butter to my shopping list" → add items to active list
|
||
|
|
|
||
|
|
Each recognized intent maps to an existing API operation, executed automatically or returned as a confirmation prompt.
|
||
|
|
|
||
|
|
### 10.9 — Web UI: LLM Settings & Features
|
||
|
|
|
||
|
|
- `/settings/llm` page (admin only):
|
||
|
|
- Provider selection display
|
||
|
|
- Monthly cost usage bar
|
||
|
|
- Rate limit configuration
|
||
|
|
- LLM log viewer (recent calls, success/failure, latency, cost)
|
||
|
|
- Enhance existing UI with LLM-powered features:
|
||
|
|
- Product add modal: "Smart Add" tab with camera/text → LLM pre-fill
|
||
|
|
- Recipe page: "Import from text" and "Import from URL" now functional
|
||
|
|
- Shopping list: "Scan receipt" button with camera
|
||
|
|
- Dashboard: natural language input bar ("What should I cook tonight?")
|
||
|
|
|
||
|
|
### 10.10 — Docker: Ollama Service (Optional)
|
||
|
|
|
||
|
|
If user wants local LLM, add to Docker Compose:
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
ollama:
|
||
|
|
image: ollama/ollama
|
||
|
|
ports: ['11434:11434']
|
||
|
|
volumes: [ollama-models:/root/.ollama]
|
||
|
|
deploy:
|
||
|
|
resources:
|
||
|
|
reservations:
|
||
|
|
devices:
|
||
|
|
- driver: nvidia
|
||
|
|
count: all
|
||
|
|
capabilities: [gpu]
|
||
|
|
profiles: [llm-local]
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Acceptance Criteria
|
||
|
|
|
||
|
|
- [ ] Can switch LLM provider via environment variable
|
||
|
|
- [ ] OpenAI provider successfully extracts nutrition from product photo
|
||
|
|
- [ ] Recipe import from text returns a valid structured recipe
|
||
|
|
- [ ] Receipt parsing extracts store, items, and prices from receipt image
|
||
|
|
- [ ] All LLM calls are logged with token counts and cost
|
||
|
|
- [ ] Rate limiting prevents exceeding configured RPM
|
||
|
|
- [ ] Monthly budget guard blocks calls when budget is exceeded
|
||
|
|
- [ ] Natural language input correctly identifies intents and executes actions
|
||
|
|
- [ ] NoOp provider still works gracefully when no LLM is configured
|
||
|
|
- [ ] LLM settings page shows usage and cost statistics
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Estimated Effort
|
||
|
|
|
||
|
|
Large. Multiple provider implementations, prompt engineering, testing across different models, cost tracking infrastructure, and NLP intent parsing are all significant.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Notes
|
||
|
|
|
||
|
|
- Prompt engineering is iterative — expect to refine prompts based on real-world testing
|
||
|
|
- Different providers/models will have varying accuracy — consider model-specific prompt tuning
|
||
|
|
- Local models (Ollama) will be less accurate but free — document quality trade-offs
|
||
|
|
- Consider caching LLM results for identical inputs (e.g., same barcode photo → same product)
|