first commit
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Fitness_core_BE.AI
|
||||
{
|
||||
public class GeminiClient : IAiClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public GeminiClient(HttpClient http, IConfiguration configuration)
|
||||
{
|
||||
_http = http;
|
||||
_config = configuration;
|
||||
}
|
||||
|
||||
public async Task<string> GenerateJsonAsync( string systemPrompt, object payload)
|
||||
{
|
||||
string apiKey = _config["Gemini:ApiKey"];
|
||||
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
throw new Exception("Gemini API key is not configured.");
|
||||
|
||||
var requestBody = new
|
||||
{
|
||||
contents = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
parts = new[]
|
||||
{
|
||||
new { text = systemPrompt },
|
||||
new { text = JsonSerializer.Serialize(payload) }
|
||||
}
|
||||
}
|
||||
},
|
||||
generationConfig = new
|
||||
{
|
||||
temperature = 0.4,
|
||||
maxOutputTokens = 10000
|
||||
}
|
||||
};
|
||||
|
||||
var response = await _http.PostAsJsonAsync(
|
||||
$"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={apiKey}",
|
||||
requestBody
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
throw new Exception($"Gemini API Error: {error}");
|
||||
}
|
||||
|
||||
var jsonString = await response.Content.ReadAsStringAsync();
|
||||
|
||||
using var doc = JsonDocument.Parse(jsonString);
|
||||
|
||||
var text = doc.RootElement
|
||||
.GetProperty("candidates")[0]
|
||||
.GetProperty("content")
|
||||
.GetProperty("parts")[0]
|
||||
.GetProperty("text")
|
||||
.GetString();
|
||||
|
||||
return text ?? throw new Exception("No response text from Gemini.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Fitness_core_BE.AI
|
||||
{
|
||||
public interface IAiClient
|
||||
{
|
||||
Task<string> GenerateJsonAsync(string systemPrompt, object payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
using Fitness_core_BE.Helpers;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Fitness_core_BE.AI
|
||||
{
|
||||
public class MealPlanOrchestrator
|
||||
{
|
||||
private readonly IAiClient _aiClient;
|
||||
private readonly ModificationGuard _guard;
|
||||
|
||||
public MealPlanOrchestrator( IAiClient aiClient, ModificationGuard guard)
|
||||
{
|
||||
_aiClient = aiClient;
|
||||
_guard = guard;
|
||||
}
|
||||
|
||||
public async Task<object> GeneratePlanAsync(object payload)
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
|
||||
decimal bmi = DataExtractor.GetRequiredDecimal(data, "bmi");
|
||||
string country = DataExtractor.GetRequiredString(data, "country");
|
||||
string province = DataExtractor.GetRequiredString(data, "province");
|
||||
string goal = DataExtractor.GetRequiredString(data, "goal");
|
||||
decimal budget = DataExtractor.GetRequiredDecimal(data, "budgetPerWeek");
|
||||
int mealsPerDay = DataExtractor.GetRequiredInt(data, "mealsPerDay");
|
||||
bool isVegetarian = DataExtractor.GetRequiredBool(data, "isVegetarian");
|
||||
|
||||
// Basic validation
|
||||
if (bmi < 10 || bmi > 60)
|
||||
throw new Exception("Invalid BMI");
|
||||
|
||||
var systemPrompt = @"
|
||||
You are a certified nutritionist.
|
||||
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,
|
||||
mealItems: [
|
||||
{
|
||||
name: string,
|
||||
description: string,
|
||||
calories: number,
|
||||
protein: number,
|
||||
carbohydrates: number,
|
||||
fats: number,
|
||||
servingSize: string,
|
||||
mealItems: [
|
||||
{ food: string, quantity: string }
|
||||
],
|
||||
preparationNotes: string
|
||||
}
|
||||
]
|
||||
}
|
||||
";
|
||||
|
||||
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("currentPlan"))
|
||||
throw new Exception("Current plan missing");
|
||||
|
||||
string modification =
|
||||
DataExtractor.GetRequiredString(data, "modification");
|
||||
|
||||
_guard.Validate(modification);
|
||||
|
||||
var systemPrompt = @"
|
||||
You are a professional nutrition AI.
|
||||
|
||||
You will receive:
|
||||
1) The current meal plan in valid JSON format.
|
||||
2) A user modification request in free text.
|
||||
|
||||
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 meals unless requested.
|
||||
- Do NOT modify calories, protein, carbs, fats unless the request requires it.
|
||||
- Do NOT rewrite descriptions unless necessary for the modification.
|
||||
- Do NOT change serving sizes unless required.
|
||||
- Do NOT add or remove meals unless explicitly requested.
|
||||
- Do NOT modify ingredients that are unrelated to the request.
|
||||
- Preserve every unchanged value exactly as provided.
|
||||
|
||||
Scope control:
|
||||
If the request only affects one ingredient, update only that ingredient.
|
||||
If the request does not affect the plan, return it unchanged.
|
||||
|
||||
Preserve the exact internal structure of the meal 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,
|
||||
""mealItems"": [
|
||||
{
|
||||
""name"": string,
|
||||
""description"": string,
|
||||
""calories"": number,
|
||||
""protein"": number,
|
||||
""carbohydrates"": number,
|
||||
""fats"": number,
|
||||
""servingSize"": string,
|
||||
""mealItems"": [
|
||||
{ ""food"": string, ""quantity"": string }
|
||||
],
|
||||
""preparationNotes"": string
|
||||
}
|
||||
]
|
||||
},
|
||||
""updatedPlan"": {
|
||||
""name"": string,
|
||||
""description"": string,
|
||||
""mealItems"": [
|
||||
{
|
||||
""name"": string,
|
||||
""description"": string,
|
||||
""calories"": number,
|
||||
""protein"": number,
|
||||
""carbohydrates"": number,
|
||||
""fats"": number,
|
||||
""servingSize"": string,
|
||||
""mealItems"": [
|
||||
{ ""food"": string, ""quantity"": string }
|
||||
],
|
||||
""preparationNotes"": 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)!;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace Fitness_core_BE.AI
|
||||
{
|
||||
public class ModificationGuard
|
||||
{
|
||||
private static readonly string[] AllowedKeywords =
|
||||
{
|
||||
"replace",
|
||||
"remove",
|
||||
"allergy",
|
||||
"change",
|
||||
"vegetarian",
|
||||
"calorie",
|
||||
"protein",
|
||||
"carb",
|
||||
"fat",
|
||||
"meal",
|
||||
"ingredient"
|
||||
};
|
||||
|
||||
private static readonly string[] BlockedPatterns =
|
||||
{
|
||||
"ignore previous instructions",
|
||||
"system prompt",
|
||||
"act as",
|
||||
"bypass",
|
||||
"developer mode",
|
||||
"jailbreak"
|
||||
};
|
||||
|
||||
public void Validate(string message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
throw new Exception("Modification cannot be empty.");
|
||||
|
||||
if (message.Length > 500)
|
||||
throw new Exception("Modification too long.");
|
||||
|
||||
var lower = message.ToLower();
|
||||
|
||||
if (BlockedPatterns.Any(p => lower.Contains(p)))
|
||||
throw new Exception("Invalid modification request.");
|
||||
|
||||
//if (!AllowedKeywords.Any(k => lower.Contains(k)))
|
||||
// throw new Exception("Only meal plan modifications are allowed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
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)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user