Files
rideaware-api/internal/ai/prompt.go
2026-05-17 20:39:47 -05:00

215 lines
7.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ai
import (
"encoding/json"
"fmt"
"strings"
)
// BuildSystemPrompt creates the system message for DeepSeek
func BuildSystemPrompt() string {
return `You are an expert cycling coach with deep knowledge of power-based training, periodization, and workout design.
Your task is to generate structured cycling workouts based on user data and goals. You must respond ONLY with valid JSON.
POWER ZONES (% of FTP):
- Recovery: 30-55% FTP
- Endurance/Zone 2: 56-75% FTP
- Tempo: 76-87% FTP
- Sweet Spot: 88-94% FTP
- Threshold/FTP: 95-105% FTP
- VO2max: 106-120% FTP
- Anaerobic: 121-150% FTP
- Neuromuscular: >150% FTP
WORKOUT STRUCTURE:
- All workouts must have warmup, main intervals, and cooldown
- Duration is in seconds
- Power is expressed as decimal (0.65 = 65% FTP, 1.0 = 100% FTP)
- Segment types: "warmup", "steadystate", "interval", "ramp", "cooldown", "rest", "freeride"
- For steady state efforts, use "power" field
- For variable efforts, use "power_low" and "power_high" fields
TSS CALCULATION:
TSS = (duration_seconds × NP² / FTP²) / 36
Approximate: duration_minutes × intensity_factor²
TRAINING PRINCIPLES:
1. Progressive overload within user's current fitness
2. Balance intensity and volume
3. Include recovery when needed
4. Respect weekly hour constraints
5. Build on recent training load (CTL/ATL/TSB)
6. Vary workouts to prevent monotony
7. Include appropriate warmup and cooldown for all workouts
EVENT-BASED PERIODIZATION (when a target event is provided):
- Structure the plan around the target event date using standard cycling periodization:
- BUILD phase: Progressive volume/intensity increase (furthest from event)
- PEAK phase: Highest intensity, race-specific intervals
- TAPER phase: Reduce volume while maintaining intensity
- RACE DAY: Generate a "Race" type workout on the event date with pre-race notes
- RECOVERY: Easy rides after race day
- A-priority race: 7-10 day taper, reduce volume 30-50%, maintain intensity
- B-priority race: 3-5 day lighter taper
- C-priority race: No taper, treat as training race
- On race day, create a workout with type "Race" and include race distance/event details in notes
- If multiple events exist, avoid scheduling hard workouts within 2 days of any event
NUTRITION GUIDANCE (when nutrition context is provided):
- Include brief fueling notes in the "notes" field for each workout:
- Pre-ride: what to eat 1-2 hours before (e.g., "Pre: oatmeal + banana, 400kcal")
- During: fueling for rides >60min (e.g., "During: 60g carbs/hr, sports drink")
- Post-ride: recovery nutrition within 30min (e.g., "Post: protein shake + rice, 500kcal")
- For easy/recovery rides: lighter fueling notes
- For hard/long rides: emphasize carb loading and during-ride nutrition
- Keep fueling notes concise (one line each)
OUTPUT FORMAT (JSON only - BE CONCISE):
{
"workouts": [
{
"title": "Workout Name",
"description": "Brief description",
"type": "Endurance|Tempo|Threshold|VO2 Max|Recovery|Sprint",
"scheduled_date": "YYYY-MM-DD",
"duration": 3600,
"segments": [
{"type": "warmup", "duration": 600, "power_low": 0.40, "power_high": 0.65},
{"type": "steadystate", "duration": 2400, "power": 0.65, "cadence": 85},
{"type": "cooldown", "duration": 600, "power_low": 0.65, "power_high": 0.40}
],
"estimated_tss": 45.0,
"notes": "Brief coaching note"
}
],
"rationale": "Brief plan overview"
}
IMPORTANT: Keep descriptions and notes VERY SHORT (max 10 words each). Response must be valid, complete JSON.`
}
// BuildUserPrompt creates the user message with context
func BuildUserPrompt(ctx UserContext, req GenerateRequest) string {
contextJSON, _ := json.MarshalIndent(ctx, "", " ")
focusAreasStr := strings.Join(req.FocusAreas, ", ")
// Build event context section
eventSection := ""
if ctx.TargetEvent != nil {
eventSection = fmt.Sprintf(`
TARGET EVENT:
- Name: %s
- Date: %s (%d days away)
- Type: %s
- Distance: %.0f km
- Priority: %s-race
- IMPORTANT: Periodize the plan to peak for this event. Apply appropriate taper based on priority.
On event day (%s), generate a "Race" type workout with event details in notes.
`,
ctx.TargetEvent.Name,
ctx.TargetEvent.Date,
ctx.TargetEvent.DaysAway,
ctx.TargetEvent.EventType,
ctx.TargetEvent.Distance,
ctx.TargetEvent.Priority,
ctx.TargetEvent.Date,
)
}
if len(ctx.UpcomingEvents) > 0 {
eventSection += "\nUPCOMING EVENTS (avoid hard workouts within 2 days of these):\n"
for _, ev := range ctx.UpcomingEvents {
eventSection += fmt.Sprintf("- %s (%s, %s-race, %d days away)\n",
ev.Name, ev.Date, ev.Priority, ev.DaysAway)
}
}
// Build nutrition context section
nutritionSection := ""
if ctx.Nutrition != nil {
nutritionSection = fmt.Sprintf(`
NUTRITION CONTEXT:
- Goal: %s
- Daily Calorie Target: %d kcal (before workout additions)
- Macro Targets: Protein %dg, Carbs %dg, Fat %dg
- Dietary Preference: %s
- IMPORTANT: Include brief fueling notes (pre-ride, during, post-ride) in each workout's "notes" field. Adjust fueling intensity to match workout intensity.
`,
ctx.Nutrition.Goal,
ctx.Nutrition.DailyCalories,
ctx.Nutrition.ProteinG,
ctx.Nutrition.CarbsG,
ctx.Nutrition.FatG,
ctx.Nutrition.DietaryPref,
)
}
return fmt.Sprintf(`Generate a %d-day training plan with the following requirements:
USER CONTEXT:
%s
PLAN PARAMETERS:
- Duration: %d days
- Focus Areas: %s
- Intensity Level: %s
- Weekly Available Hours: %d
- Start Date: %s
- Include Rest Days: %v
%s%s
REQUIREMENTS:
1. Generate workouts that fit within %d weekly hours
2. Focus on: %s
3. Overall intensity: %s
4. Respect user's current fitness (FTP: %d, recent training load)
5. Include variety and progressive adaptation
6. Provide clear workout descriptions and coaching notes
7. Ensure total duration of segments matches workout duration
8. Use appropriate power zones for each workout type
9. Include proper warmup and cooldown for every workout
IMPORTANT:
- Return ONLY valid JSON in the exact format specified in the system prompt
- Do not include any explanatory text before or after the JSON
- Ensure all dates are sequential starting from %s
- Calculate realistic TSS values for each workout
- Segment durations must sum to workout duration (in seconds)
- Power values should be between 0.30 and 2.50 (30%% to 250%% FTP)
Return ONLY valid JSON in the exact format specified in the system prompt.`,
req.PlanDuration,
string(contextJSON),
req.PlanDuration,
focusAreasStr,
req.IntensityLevel,
req.WeeklyHours,
req.StartDate,
req.IncludeRest,
eventSection,
nutritionSection,
req.WeeklyHours,
focusAreasStr,
req.IntensityLevel,
ctx.FTP,
req.StartDate,
)
}
// extractJSON attempts to extract JSON from response that might be wrapped in markdown
func extractJSON(response string) string {
// Remove markdown code blocks if present
if idx := strings.Index(response, "```json"); idx != -1 {
response = response[idx+7:]
} else if idx := strings.Index(response, "```"); idx != -1 {
response = response[idx+3:]
}
if idx := strings.Index(response, "```"); idx != -1 {
response = response[:idx]
}
return strings.TrimSpace(response)
}