Files
fitaiProto/apps/admin/src/app/api/recommendations/generate/route.ts
T
2026-03-11 06:07:16 +01:00

327 lines
10 KiB
TypeScript

import { NextResponse } from "next/server";
import { getDatabase } from "@/lib/database";
import { buildAIContext } from "@/lib/ai/ai-context";
import { buildEnhancedPrompt, buildBasicPrompt } from "@/lib/ai/prompt-builder";
import log from "@/lib/logger";
export async function POST(req: Request) {
try {
const { userId, useExternalModel, modelProvider } = await req.json();
if (!userId) {
return NextResponse.json(
{ error: "User ID is required" },
{ status: 400 },
);
}
log.debug("Generating recommendation for user", {
userId,
modelProvider,
useExternalModel,
});
const db = await getDatabase();
// Fetch fitness profile
const profile = await db.getFitnessProfileByUserId(userId);
if (!profile) {
log.error("Fitness profile not found", undefined, { userId });
return NextResponse.json(
{ error: "Fitness profile not found for this user" },
{ status: 404 },
);
}
log.debug("Fitness profile found", { profileId: profile.id });
// Build AI context with goals and recommendations
let prompt: string;
try {
const context = await buildAIContext(userId);
prompt = buildEnhancedPrompt(context);
log.debug("Using enhanced AI context with goals and history");
} catch (error) {
// Fallback to basic prompt if context building fails
log.warn("Failed to build AI context, using basic prompt", {
error: error instanceof Error ? error.message : String(error),
});
prompt = buildBasicPrompt(profile);
}
let parsedResponse;
// Determine which AI provider to use
const provider =
modelProvider || (useExternalModel ? "deepseek" : "ollama");
if (provider === "openai") {
// Use OpenAI
const openaiApiKey = process.env.OPENAI_API_KEY;
if (!openaiApiKey) {
return NextResponse.json(
{ error: "OpenAI API key not configured" },
{ status: 500 },
);
}
log.debug("Using OpenAI model", { userId });
const openaiResponse = await fetch(
"https://api.openai.com/v1/chat/completions",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${openaiApiKey}`,
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [
{
role: "system",
content:
'You are a professional fitness trainer and nutritionist. Always respond with valid JSON only, no markdown or code blocks. The response must have this exact structure: {"recommendationText": "string", "activityPlan": "string", "dietPlan": "string"}',
},
{
role: "user",
content: prompt,
},
],
temperature: 0.7,
max_tokens: 1500,
response_format: { type: "json_object" },
}),
},
);
if (!openaiResponse.ok) {
const errorText = await openaiResponse.text();
log.error("OpenAI API request failed", new Error(errorText), {
status: openaiResponse.status,
});
return NextResponse.json(
{ error: "Failed to generate recommendation from OpenAI" },
{ status: 500 },
);
}
const openaiData = await openaiResponse.json();
log.debug("Received OpenAI response", { openaiData });
try {
const content = openaiData.choices[0].message.content;
parsedResponse = JSON.parse(content);
} catch (e) {
log.error("Failed to parse OpenAI response", e, {
response: openaiData,
});
return NextResponse.json(
{ error: "Invalid response format from OpenAI" },
{ status: 500 },
);
}
} else if (provider === "deepseek") {
// Use DeepSeek AI
const deepseekApiKey = process.env.DEEPSEEK_API_KEY;
if (!deepseekApiKey) {
return NextResponse.json(
{ error: "DeepSeek API key not configured" },
{ status: 500 },
);
}
log.debug("Using DeepSeek AI model", { userId });
const deepseekResponse = await fetch(
"https://api.deepseek.com/v1/chat/completions",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${deepseekApiKey}`,
},
body: JSON.stringify({
model: "deepseek-chat",
messages: [
{
role: "system",
content:
"You are a professional fitness trainer and nutritionist. Always respond with valid JSON only, no markdown or code blocks.",
},
{
role: "user",
content: prompt,
},
],
temperature: 0.7,
max_tokens: 1000,
}),
},
);
if (!deepseekResponse.ok) {
const errorText = await deepseekResponse.text();
log.error("DeepSeek API request failed", new Error(errorText), {
status: deepseekResponse.status,
});
return NextResponse.json(
{ error: "Failed to generate recommendation from DeepSeek AI" },
{ status: 500 },
);
}
const deepseekData = await deepseekResponse.json();
log.debug("Received DeepSeek response", { deepseekData });
try {
const content = deepseekData.choices[0].message.content;
let cleanResponse = content.trim();
// Remove markdown code blocks if present
if (cleanResponse.startsWith("```json")) {
cleanResponse = cleanResponse
.replace(/^```json\s*/, "")
.replace(/\s*```$/, "");
} else if (cleanResponse.startsWith("```")) {
cleanResponse = cleanResponse
.replace(/^```\s*/, "")
.replace(/\s*```$/, "");
}
// Find the first '{' and last '}' to extract the JSON object
const firstBrace = cleanResponse.indexOf("{");
const lastBrace = cleanResponse.lastIndexOf("}");
if (firstBrace !== -1 && lastBrace !== -1) {
cleanResponse = cleanResponse.substring(firstBrace, lastBrace + 1);
}
parsedResponse = JSON.parse(cleanResponse);
} catch (e) {
log.error("Failed to parse DeepSeek response", e, {
response: deepseekData,
});
return NextResponse.json(
{ error: "Invalid response format from DeepSeek AI" },
{ status: 500 },
);
}
} else {
// Use local Ollama
log.debug("Using local Ollama model", { userId });
const ollamaResponse = await fetch(
"http://localhost:11434/api/generate",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gemma3:latest",
prompt: prompt,
stream: false,
format: "json",
}),
},
);
if (!ollamaResponse.ok) {
const errorText = await ollamaResponse.text();
log.error("Ollama API request failed", new Error(errorText), {
status: ollamaResponse.status,
});
return NextResponse.json(
{ error: "Failed to generate recommendation from Ollama" },
{ status: 500 },
);
}
const aiData = await ollamaResponse.json();
log.debug("Received Ollama response", { response: aiData.response });
try {
let cleanResponse = aiData.response.trim();
// Remove markdown code blocks if present
if (cleanResponse.startsWith("```json")) {
cleanResponse = cleanResponse
.replace(/^```json\s*/, "")
.replace(/\s*```$/, "");
} else if (cleanResponse.startsWith("```")) {
cleanResponse = cleanResponse
.replace(/^```\s*/, "")
.replace(/\s*```$/, "");
}
// Find the first '{' and last '}' to extract the JSON object
const firstBrace = cleanResponse.indexOf("{");
const lastBrace = cleanResponse.lastIndexOf("}");
if (firstBrace !== -1 && lastBrace !== -1) {
cleanResponse = cleanResponse.substring(firstBrace, lastBrace + 1);
}
parsedResponse = JSON.parse(cleanResponse);
} catch (e) {
log.error("Failed to parse Ollama response", e, {
response: aiData.response,
});
return NextResponse.json(
{ error: "Invalid response format from Ollama" },
{ status: 500 },
);
}
}
// Save to database
log.debug("Saving recommendation to database", {
userId,
profileId: profile.id,
hasRecommendationText: !!parsedResponse.recommendationText,
hasActivityPlan: !!parsedResponse.activityPlan,
hasDietPlan: !!parsedResponse.dietPlan,
});
const recommendation = await db.createRecommendation({
id: crypto.randomUUID(),
userId,
fitnessProfileId: profile.id,
recommendationText: parsedResponse.recommendationText || "",
activityPlan: parsedResponse.activityPlan || "",
dietPlan: parsedResponse.dietPlan || "",
status: "pending",
generatedAt: new Date(),
updatedAt: new Date(),
});
log.info("Recommendation generated successfully", {
recommendationId: recommendation.id,
userId,
});
// Return in standardized format
return NextResponse.json({
success: true,
data: recommendation,
meta: {
timestamp: new Date().toISOString(),
requestId: `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
},
});
} catch (error) {
log.error("Failed to generate recommendation", error, {
errorMessage: error instanceof Error ? error.message : String(error),
errorStack: error instanceof Error ? error.stack : undefined,
});
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}