230 lines
12 KiB
C#
230 lines
12 KiB
C#
using Fitness_core_BE.Helpers;
|
|
using System.Text.Json;
|
|
|
|
namespace Fitness_core_BE.AI
|
|
{
|
|
public class WorkoutPlanOrchestrator
|
|
{
|
|
private readonly IAiClient _aiClient;
|
|
private readonly ModificationGuard _guard;
|
|
|
|
public WorkoutPlanOrchestrator(IAiClient aiClient, ModificationGuard guard)
|
|
{
|
|
_aiClient = aiClient;
|
|
_guard = guard;
|
|
}
|
|
|
|
public async Task<object> GeneratePlanAsync(object payload)
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
|
|
string fitnessLevel = DataExtractor.GetRequiredString(data, "fitnessLevel");
|
|
string goal = DataExtractor.GetRequiredString(data, "goal");
|
|
int daysPerWeek = DataExtractor.GetRequiredInt(data, "daysPerWeek");
|
|
string availableEquipment = DataExtractor.GetRequiredString(data, "availableEquipment");
|
|
int durationMinutes = DataExtractor.GetRequiredInt(data, "durationPerSession");
|
|
|
|
// Optional fields
|
|
string? injuryOrLimitations = data.ContainsKey("injuryOrLimitations")
|
|
? data["injuryOrLimitations"].GetString()
|
|
: null;
|
|
|
|
// Basic validation
|
|
if (daysPerWeek < 1 || daysPerWeek > 7)
|
|
throw new Exception("Days per week must be between 1 and 7");
|
|
|
|
if (durationMinutes < 15 || durationMinutes > 180)
|
|
throw new Exception("Duration must be between 15 and 180 minutes");
|
|
|
|
|
|
//bmi, body shape , target , should check with tartget and body shape matching, time peiod, Continuose-week days,,, whether going to Gym or not, Injuries, any seriose conditions,
|
|
|
|
var systemPrompt = @"
|
|
You are a certified personal trainer and fitness expert.
|
|
Your response must be STRICTLY a JSON object.
|
|
|
|
Output format rules:
|
|
1. The first character of your response MUST be {
|
|
2. The last character MUST be }
|
|
3. Do not include ``` or any markdown formatting
|
|
4. Do not include any explanation text
|
|
5. Do not include comments
|
|
6. Do not prefix with ""Here is""
|
|
7. Do not suffix with any text
|
|
|
|
Return ONLY valid JSON.
|
|
|
|
Structure:
|
|
{
|
|
""name"": string,
|
|
""description"": string,
|
|
""workoutItems"": [
|
|
{
|
|
""exerciseDay"": string,
|
|
""description"": string,
|
|
""exercises"": {
|
|
""Exercise Name"": ""sets x reps or duration""
|
|
},
|
|
""duration"": string (total session time including warmup and cooldown, with units),
|
|
""wormUp"": string (warmup instructions with an explicit time and unit),
|
|
""cooldown"": string (cooldown instructions with an explicit time and unit),
|
|
""howToDo"": string (JSON string with rest periods and notes),
|
|
""doURL"": string (exercise reference URL)
|
|
}
|
|
]
|
|
}
|
|
|
|
Important guidelines:
|
|
- Create a workout plan based on the user's fitness level, goal, and available equipment
|
|
- Include appropriate exercises for the specified number of days per week
|
|
- Each workout must fit within the specified duration
|
|
- Set ""duration"" to the requested total duration per session (for example, ""45 minutes"")
|
|
- ""wormUp"" and ""cooldown"" must each include an explicit duration such as ""5 minutes""
|
|
- The main workout time plus wormUp time plus cooldown time must equal the total ""duration""
|
|
- The ""exercises"" field should be an object with exercise names as keys and sets/reps as values
|
|
- The ""howToDo"" field should be a JSON string containing warmup, rest periods, and notes
|
|
- Consider any injury or limitations mentioned
|
|
- Provide realistic sets, reps, and rest periods based on fitness level
|
|
- Include proper progression and variety
|
|
";
|
|
|
|
var jsonResponse = await _aiClient.GenerateJsonAsync(
|
|
systemPrompt,
|
|
data
|
|
);
|
|
|
|
// Validate JSON before returning
|
|
try
|
|
{
|
|
var parsed = JsonSerializer.Deserialize<object>(jsonResponse);
|
|
if (parsed == null)
|
|
throw new Exception("Invalid AI JSON");
|
|
}
|
|
catch
|
|
{
|
|
throw new Exception("AI returned invalid JSON structure");
|
|
}
|
|
|
|
return JsonSerializer.Deserialize<object>(jsonResponse)!;
|
|
}
|
|
|
|
public async Task<object> UpdatePlanAsync(object payload)
|
|
{
|
|
var data = DeserializePayload.Deserialize(payload);
|
|
|
|
if (!data.ContainsKey("currentWorkoutPlan"))
|
|
throw new Exception("Current plan missing");
|
|
|
|
string modification = DataExtractor.GetRequiredString(data, "modification");
|
|
|
|
_guard.Validate(modification);
|
|
|
|
var systemPrompt = @"
|
|
You are a professional fitness AI.
|
|
|
|
You will receive:
|
|
1) The current workout plan in valid JSON format.
|
|
2) A user modification request in free text.
|
|
3) BMI, body shape, target, time period, continuous workout days, whether going to gym or not, injuries, any seriose conditions.
|
|
|
|
Your task:
|
|
- Carefully analyze the modification request.
|
|
- Modify ONLY the specific elements explicitly requested by the user.
|
|
- Do NOT change anything else.
|
|
|
|
VERY IMPORTANT RULES:
|
|
- Do NOT change the plan name unless the user explicitly asks.
|
|
- Do NOT change the number of workout days unless requested.
|
|
- Do NOT modify exercises that are not mentioned in the request.
|
|
- Do NOT rewrite descriptions unless necessary for the modification.
|
|
- Do NOT change sets, reps, or rest periods unless the request requires it.
|
|
- Do NOT add or remove exercises unless explicitly requested.
|
|
- Do NOT modify workout structure that is unrelated to the request.
|
|
- Preserve every unchanged value exactly as provided.
|
|
- Analyze the user's BMI, body shape, target, time period, continuous workout days, gym access, injuries, and any seriose conditions.
|
|
- Stricly focus if mentioned any seriose health conditions or injuries and do not suggest any exercise that can cause harm to the user.
|
|
- If the modification request is not logical or contradicts fitness principles, do not apply it and explain why in the reason field.
|
|
|
|
|
|
Scope control:
|
|
If the request only affects one exercise, update only that exercise.
|
|
If the request does not affect the plan, return it unchanged.
|
|
|
|
Preserve the exact internal structure of the workout plan.
|
|
Do NOT rename fields.
|
|
Do NOT remove required properties.
|
|
Do NOT change the JSON schema.
|
|
|
|
Return your response strictly in this JSON format:
|
|
|
|
{
|
|
""reason"": string,
|
|
""modified"": boolean,
|
|
""currentPlan"": {
|
|
""name"": string,
|
|
""description"": string,
|
|
""workoutItems"": [
|
|
{
|
|
""exerciseDay"": string,
|
|
""description"": string,
|
|
""exercises"": {
|
|
""Exercise Name"": ""sets x reps or duration""
|
|
},
|
|
""howToDo"": string,
|
|
""doURL"": string
|
|
}
|
|
]
|
|
},
|
|
""updatedPlan"": {
|
|
""name"": string,
|
|
""description"": string,
|
|
""workoutItems"": [
|
|
{
|
|
""exerciseDay"": string,
|
|
""description"": string,
|
|
""exercises"": {
|
|
""Exercise Name"": ""sets x reps or duration""
|
|
},
|
|
""howToDo"": string,
|
|
""doURL"": string
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
STRICT OUTPUT RULES:
|
|
- Return ONLY valid raw JSON.
|
|
- No markdown.
|
|
- No explanations.
|
|
- First character must be {.
|
|
- Last character must be }.
|
|
- No trailing commas.
|
|
- No extra commentary.
|
|
|
|
If modification cannot be applied:
|
|
- Set ""modified"": false
|
|
- Keep currentPlan and updatedPlan identical.
|
|
- Explain reason clearly.
|
|
";
|
|
|
|
var jsonResponse = await _aiClient.GenerateJsonAsync(
|
|
systemPrompt,
|
|
data
|
|
);
|
|
|
|
try
|
|
{
|
|
var parsed = JsonSerializer.Deserialize<object>(jsonResponse);
|
|
if (parsed == null)
|
|
throw new Exception("Invalid JSON");
|
|
}
|
|
catch
|
|
{
|
|
throw new Exception("AI returned invalid JSON");
|
|
}
|
|
|
|
return JsonSerializer.Deserialize<object>(jsonResponse)!;
|
|
}
|
|
}
|
|
}
|