Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 68 additions & 44 deletions controller/healthPlanController.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,48 @@
// controllers/healthPlanController.js

// Node 18+ has global fetch; if you're on Node 16, uncomment:
// const fetch = require("node-fetch");
const Groq = require('groq-sdk');

// [TEMP-DB-OFF] keep import for easy revert; safe to leave unused
const supabase = require("../dbConnection.js");

const AI_BASE =
process.env.AI_BASE_URL || "http://localhost:8000/ai-model/medical-report";
const HEALTH_PLAN_PROMPT = `You are a certified fitness coach and clinical dietitian.
Generate a personalised 4-week health and wellness plan based on the user's medical data and goals.
Respond with ONLY valid JSON — no markdown, no backticks, no explanation.

Return exactly this JSON shape:
{
"weekly_plan": [
{
"week": 1,
"target_calories_per_day": 1800,
"focus": "Building baseline activity",
"workouts": ["20 min walk", "10 min stretching", "Light bodyweight squats x10"],
"meal_notes": "Prioritise lean protein and vegetables; avoid processed foods.",
"reminders": ["Drink 8 glasses of water", "Sleep 7–8 hours"]
}
],
"suggestion": "One paragraph of personalised advice based on the medical profile.",
"progress_analysis": "Brief note on expected progress over 4 weeks."
}`;

function buildHealthPlanPrompt(medicalReport, healthGoal, healthSurvey) {
const report = Array.isArray(medicalReport) ? medicalReport[0] : medicalReport;
const obesity = report?.obesity_prediction?.obesity_level || "Unknown";
const diabetes = report?.diabetes_prediction?.diabetes ? "Yes" : "No";
const info = report?.health_info || healthSurvey || {};

return `User profile:
- Gender: ${info.gender || "Not specified"}
- Age: ${info.age || "Not specified"}
- Height: ${info.height ? `${info.height}m` : "Not specified"}
- Weight: ${info.weight ? `${info.weight}kg` : "Not specified"}
- Obesity level: ${obesity}
- Diabetes: ${diabetes}
- Workout days per week: ${healthGoal.days_per_week}
- Preferred workout place: ${healthGoal.workout_place || "any"}
- Target weight: ${healthGoal.target_weight ? `${healthGoal.target_weight}kg` : "Not specified"}

Generate a 4-week health plan for this person.`;
}

// ---------- helpers ----------
const toNum = (x) => {
Expand Down Expand Up @@ -197,54 +232,43 @@ const generateWeeklyPlan = async (req, res) => {
// survey (optional for AI payload)
const health_survey = buildHealthSurvey(body.survey_data);

const payload = {
medical_report: Array.isArray(body.medical_report)
? body.medical_report.map(normalizeMedicalReportForPlan)
: [normalizeMedicalReportForPlan(body.medical_report)],
survey_data: health_survey || undefined,
health_goal,
followup_qa: null,
};
Object.keys(payload).forEach((k) => payload[k] === undefined && delete payload[k]);

// call AI
const aiResponse = await fetch(`${AI_BASE}/plan/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const medicalReportNormalized = Array.isArray(body.medical_report)
? body.medical_report.map(normalizeMedicalReportForPlan)
: [normalizeMedicalReportForPlan(body.medical_report)];

const text = await aiResponse.text();
let result;
try {
result = JSON.parse(text);
} catch {
result = text;
}

if (!aiResponse.ok) {
const weeklyPlan = buildFallbackWeeklyPlan({
medicalReport: payload.medical_report[0],
healthGoal: health_goal
});
return res.status(200).json({
plan_id: null,
suggestion: "Generated a local roadmap because the AI plan service could not process the request.",
weekly_plan: weeklyPlan,
progress_analysis: null,
goal: derivePlanGoal(weeklyPlan) ?? null,
length: weeklyPlan.length,
});
if (process.env.GROQ_API_KEY) {
const userMessage = buildHealthPlanPrompt(medicalReportNormalized, health_goal, health_survey);
const groq = new Groq({ apiKey: process.env.GROQ_API_KEY, timeout: 30000 });

try {
const completion = await groq.chat.completions.create({
model: "llama-3.3-70b-versatile",
messages: [
{ role: "system", content: HEALTH_PLAN_PROMPT },
{ role: "user", content: userMessage },
],
max_tokens: 2000,
});

const rawText = completion.choices[0]?.message?.content || "";
const cleaned = rawText.replace(/```json\s*/gi, "").replace(/```/g, "").trim();
const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
result = JSON.parse(jsonMatch ? jsonMatch[0] : cleaned);
} catch (groqErr) {
console.warn("[healthPlanController] Groq failed, using fallback:", groqErr.message);
}
}

if (!result.weekly_plan) {
if (!result?.weekly_plan) {
const weeklyPlan = buildFallbackWeeklyPlan({
medicalReport: payload.medical_report[0],
healthGoal: health_goal
medicalReport: medicalReportNormalized[0],
healthGoal: health_goal,
});
return res.status(200).json({
plan_id: null,
suggestion: "Generated a local roadmap because the AI plan service did not return a weekly plan.",
suggestion: "Generated a local roadmap based on your health profile.",
weekly_plan: weeklyPlan,
progress_analysis: null,
goal: derivePlanGoal(weeklyPlan) ?? null,
Expand Down
6 changes: 3 additions & 3 deletions controller/mealPlanAIController.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ const { saveMealPlan } = require('../model/aiMealPlanModel');

const generateAIMealPlan = async (req, res) => {
try {
const dietType = typeof req.body.dietType === 'string' ? req.body.dietType : 'balanced';
const goal = typeof req.body.goal === 'string' ? req.body.goal : 'maintain weight';
const dietType = req.body.dietType || req.body.dietary_preference || 'balanced';
const goal = req.body.goal || 'maintain weight';
const allergies = Array.isArray(req.body.allergies) ? req.body.allergies : [];
const calorieTarget = typeof req.body.calorieTarget === 'number' ? req.body.calorieTarget : 1800;
const calorieTarget = Number(req.body.calorieTarget || req.body.calorie_target) || 1800;
const cuisine = typeof req.body.cuisine === 'string' ? req.body.cuisine : 'any';
const healthConditions = Array.isArray(req.body.healthConditions) ? req.body.healthConditions : [];
const mealTexture = ['regular', 'soft', 'pureed'].includes(req.body.mealTexture) ? req.body.mealTexture : 'regular';
Expand Down
168 changes: 168 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"dev:all": "concurrently --names \"API,AI\" --prefix-colors \"blue,green\" \"npm run dev\" \"cd ../NutriHelp-AI && python3 run.py\"",
"start:all": "concurrently --names \"API,AI\" --prefix-colors \"blue,green\" \"npm run start\" \"cd ../NutriHelp-AI && python3 run.py\"",
"test": "mocha ./test/**/*.test.js --timeout 5000 --exit --ignore ./test/contractTests/**",
"test:unit": "mocha ./test/unit/**/*.test.js --timeout 5000 --exit",
"test:contract": "jest ./test/contractTests/ --testTimeout=10000 --forceExit",
Expand All @@ -21,6 +23,7 @@
},
"devDependencies": {
"chai": "6.0.1",
"concurrently": "^9.2.1",
"eslint": "8.57.0",
"jest": "^29.7.0",
"mocha": "11.7.2",
Expand All @@ -37,6 +40,7 @@
"base64-arraybuffer": "^1.0.2",
"bcrypt": "5.1.1",
"bcryptjs": "2.4.3",
"chromadb": "^3.4.3",
"cors": "2.8.5",
"crypto": "1.0.1",
"dotenv": "16.6.1",
Expand Down
Loading
Loading