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 }, ); } }