215 lines
7.1 KiB
Go
215 lines
7.1 KiB
Go
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)
|
||
}
|