@usamir/healthy-meals-core 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -226,7 +226,7 @@
226
226
  { "name": "File lososa", "quantity": 150, "unit": "g" },
227
227
  { "name": "Smeđa riža", "quantity": 75, "unit": "g" },
228
228
  { "name": "Brokoli", "quantity": 100, "unit": "g" },
229
- { "name": "Šargarepa", "quantity": 80, "unit": "g" },
229
+ { "name": "Mrkva", "quantity": 80, "unit": "g" },
230
230
  { "name": "Edamame", "quantity": 50, "unit": "g" },
231
231
  { "name": "Sezamovo ulje", "quantity": 1, "unit": "žličica" },
232
232
  { "name": "Soja sos", "quantity": 1, "unit": "žličica" },
@@ -235,7 +235,7 @@
235
235
  ],
236
236
  "instructions": [
237
237
  "Skuhajte smeđu rižu prema uputstvima na pakovanju",
238
- "Na pari skuhamo brokoli i narezanu šargarepu 5-7 minuta",
238
+ "Na pari skuhamo brokoli i narezanu mrkvu 5-7 minuta",
239
239
  "Začinite lososa đumbirom, so i biberom",
240
240
  "Pržite lososa u sezamovom ulju po 4-5 minuta sa svake strane",
241
241
  "Rasporedite rižu u zdjelu",
@@ -342,7 +342,7 @@
342
342
  "difficulty": "easy",
343
343
  "ingredients": [
344
344
  { "name": "Crvena leća", "quantity": 150, "unit": "g" },
345
- { "name": "Šargarepa", "quantity": 100, "unit": "g" },
345
+ { "name": "Mrkva", "quantity": 100, "unit": "g" },
346
346
  { "name": "Celer", "quantity": 80, "unit": "g" },
347
347
  { "name": "Luk", "quantity": 1, "unit": "srednji" },
348
348
  { "name": "Bijeli luk", "quantity": 3, "unit": "čehnjaka" },
@@ -354,7 +354,7 @@
354
354
  ],
355
355
  "instructions": [
356
356
  "Zagrijte maslinovo ulje u velikoj posudi na srednjoj vatri",
357
- "Pržite narezani luk, šargarepu i celer 5 minuta",
357
+ "Pržite narezani luk, mrkvu i celer 5 minuta",
358
358
  "Dodajte sjeckani bijeli luk i kumin, kuhajte još 1 minutu",
359
359
  "Dodajte leću, narezane paradajze i povrtnu supu",
360
360
  "Zavrijte, zatim smanjite vatru i pustite da se krčka",
package/data/recipes.json CHANGED
@@ -622,7 +622,7 @@
622
622
  },
623
623
  {
624
624
  "id": "r70",
625
- "name": "Piletina sa šargarepom",
625
+ "name": "Piletina sa mrkvom",
626
626
  "calories": 370,
627
627
  "protein": 34,
628
628
  "fat": 9,
@@ -226,7 +226,7 @@
226
226
  { "name": "File lososa", "quantity": 150, "unit": "g" },
227
227
  { "name": "Smeđa riža", "quantity": 75, "unit": "g" },
228
228
  { "name": "Brokoli", "quantity": 100, "unit": "g" },
229
- { "name": "Šargarepa", "quantity": 80, "unit": "g" },
229
+ { "name": "Mrkva", "quantity": 80, "unit": "g" },
230
230
  { "name": "Edamame", "quantity": 50, "unit": "g" },
231
231
  { "name": "Sezamovo ulje", "quantity": 1, "unit": "žličica" },
232
232
  { "name": "Soja sos", "quantity": 1, "unit": "žličica" },
@@ -235,7 +235,7 @@
235
235
  ],
236
236
  "instructions": [
237
237
  "Skuhajte smeđu rižu prema uputstvima na pakovanju",
238
- "Na pari skuhamo brokoli i narezanu šargarepu 5-7 minuta",
238
+ "Na pari skuhamo brokoli i narezanu mrkvu 5-7 minuta",
239
239
  "Začinite lososa đumbirom, so i biberom",
240
240
  "Pržite lososa u sezamovom ulju po 4-5 minuta sa svake strane",
241
241
  "Rasporedite rižu u zdjelu",
@@ -342,7 +342,7 @@
342
342
  "difficulty": "easy",
343
343
  "ingredients": [
344
344
  { "name": "Crvena leća", "quantity": 150, "unit": "g" },
345
- { "name": "Šargarepa", "quantity": 100, "unit": "g" },
345
+ { "name": "Mrkva", "quantity": 100, "unit": "g" },
346
346
  { "name": "Celer", "quantity": 80, "unit": "g" },
347
347
  { "name": "Luk", "quantity": 1, "unit": "srednji" },
348
348
  { "name": "Bijeli luk", "quantity": 3, "unit": "čehnjaka" },
@@ -354,7 +354,7 @@
354
354
  ],
355
355
  "instructions": [
356
356
  "Zagrijte maslinovo ulje u velikoj posudi na srednjoj vatri",
357
- "Pržite narezani luk, šargarepu i celer 5 minuta",
357
+ "Pržite narezani luk, mrkvu i celer 5 minuta",
358
358
  "Dodajte sjeckani bijeli luk i kumin, kuhajte još 1 minutu",
359
359
  "Dodajte leću, narezane paradajze i povrtnu supu",
360
360
  "Zavrijte, zatim smanjite vatru i pustite da se krčka",
@@ -1,7 +1,7 @@
1
1
  import { UserProfile, MealPlan, Meal } from '../types/firestore';
2
2
  import { OllamaConfig } from '../types/ai';
3
3
  export declare class AIMealPlanGenerator {
4
- private ollamaService;
4
+ private aiService;
5
5
  constructor(ollamaConfig: OllamaConfig);
6
6
  private convertUserProfileToAIContext;
7
7
  private convertAIMealToMeal;
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AIMealPlanGenerator = void 0;
4
- const ollamaService_1 = require("./ollamaService");
4
+ const unifiedAIService_1 = require("./unifiedAIService");
5
5
  class AIMealPlanGenerator {
6
6
  constructor(ollamaConfig) {
7
- this.ollamaService = new ollamaService_1.OllamaService(ollamaConfig);
7
+ this.aiService = new unifiedAIService_1.UnifiedAIService(ollamaConfig);
8
8
  }
9
9
  convertUserProfileToAIContext(profile) {
10
10
  return {
@@ -124,7 +124,7 @@ class AIMealPlanGenerator {
124
124
  avoidIngredients: options?.avoidIngredients,
125
125
  focusAreas: options?.focusAreas
126
126
  };
127
- const response = await this.ollamaService.generateMealPlan(request);
127
+ const response = await this.aiService.generateMealPlan(request);
128
128
  if (!response.success || !response.data) {
129
129
  throw new Error(`Failed to generate AI meal plan: ${response.error}`);
130
130
  }
@@ -139,7 +139,7 @@ class AIMealPlanGenerator {
139
139
  avoidIngredients: options?.avoidIngredients,
140
140
  focusAreas: options?.focusAreas
141
141
  };
142
- const response = await this.ollamaService.generateMealPlan(request);
142
+ const response = await this.aiService.generateMealPlan(request);
143
143
  if (!response.success || !response.data) {
144
144
  throw new Error(`Failed to generate AI weekly meal plan: ${response.error}`);
145
145
  }
@@ -155,7 +155,7 @@ class AIMealPlanGenerator {
155
155
  difficulty: options?.difficulty,
156
156
  servings: options?.servings
157
157
  };
158
- const response = await this.ollamaService.generateRecipe(request);
158
+ const response = await this.aiService.generateRecipe(request);
159
159
  if (!response.success || !response.data) {
160
160
  throw new Error(`Failed to generate AI recipe: ${response.error}`);
161
161
  }
@@ -190,7 +190,7 @@ class AIMealPlanGenerator {
190
190
  tags: meal.tags,
191
191
  tips: meal.tips
192
192
  };
193
- const response = await this.ollamaService.suggestMealVariations(aiMeal, context);
193
+ const response = await this.aiService.suggestMealVariations(aiMeal, context);
194
194
  if (!response.success || !response.data) {
195
195
  throw new Error(`Failed to generate meal variations: ${response.error}`);
196
196
  }
@@ -225,7 +225,7 @@ class AIMealPlanGenerator {
225
225
  tags: meal.tags,
226
226
  tips: meal.tips
227
227
  }));
228
- const response = await this.ollamaService.analyzeMealPlan(aiMeals, context);
228
+ const response = await this.aiService.analyzeMealPlan(aiMeals, context);
229
229
  if (!response.success || !response.data) {
230
230
  throw new Error(`Failed to analyze meal plan: ${response.error}`);
231
231
  }
@@ -5,6 +5,7 @@ export declare class OllamaService implements AIServiceInterface {
5
5
  private makeRequest;
6
6
  private buildUserProfileContext;
7
7
  private parseJSONResponse;
8
+ private fixCommonJSONIssues;
8
9
  generateMealPlan(request: AIMealPlanRequest): Promise<AIResponse<AIGeneratedMealPlan>>;
9
10
  generateRecipe(request: AIRecipeRequest): Promise<AIResponse<AIGeneratedMeal>>;
10
11
  suggestMealVariations(meal: AIGeneratedMeal, context: AIPromptContext): Promise<AIResponse<AIGeneratedMeal[]>>;
@@ -87,20 +87,60 @@ class OllamaService {
87
87
  }
88
88
  parseJSONResponse(response) {
89
89
  try {
90
+ // Clean up the response - remove any markdown code blocks
91
+ let cleanedResponse = response.replace(/```json\s*/g, '').replace(/```\s*$/g, '').trim();
90
92
  // Try to find JSON in the response
91
- const jsonMatch = response.match(/\{[\s\S]*\}/);
93
+ const jsonMatch = cleanedResponse.match(/\{[\s\S]*\}/);
92
94
  if (jsonMatch) {
93
- return JSON.parse(jsonMatch[0]);
95
+ const jsonString = jsonMatch[0];
96
+ // Additional validation to ensure proper JSON structure
97
+ try {
98
+ const parsed = JSON.parse(jsonString);
99
+ return parsed;
100
+ }
101
+ catch (parseError) {
102
+ // If direct parsing fails, try to fix common JSON issues
103
+ const fixedJson = this.fixCommonJSONIssues(jsonString);
104
+ return JSON.parse(fixedJson);
105
+ }
94
106
  }
95
107
  // If no JSON found, try parsing the entire response
96
- return JSON.parse(response);
108
+ try {
109
+ return JSON.parse(cleanedResponse);
110
+ }
111
+ catch (parseError) {
112
+ // Try to fix the entire response
113
+ const fixedResponse = this.fixCommonJSONIssues(cleanedResponse);
114
+ return JSON.parse(fixedResponse);
115
+ }
97
116
  }
98
117
  catch (error) {
99
- throw new Error(`Failed to parse AI response as JSON: ${error}`);
118
+ // Log the actual response for debugging
119
+ console.error('Failed to parse AI response. Raw response:', response);
120
+ throw new Error(`Failed to parse AI response as JSON: ${error instanceof Error ? error.message : 'Unknown error'}`);
100
121
  }
101
122
  }
123
+ fixCommonJSONIssues(jsonString) {
124
+ let fixed = jsonString;
125
+ // Fix common issues with AI-generated JSON
126
+ // 1. Fix trailing commas in arrays and objects
127
+ fixed = fixed.replace(/,\s*([}\]])/g, '$1');
128
+ // 2. Fix unquoted property names
129
+ fixed = fixed.replace(/([{\s,])([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
130
+ // 3. Fix single quotes instead of double quotes
131
+ fixed = fixed.replace(/'/g, '"');
132
+ // 4. Fix missing quotes around string values
133
+ fixed = fixed.replace(/:\s*([a-zA-Z_][a-zA-Z0-9_]*)([\s,}])/g, ': "$1"$2');
134
+ // 5. Fix line breaks in strings
135
+ fixed = fixed.replace(/"([^"]*)\n([^"]*)"/g, '"$1\\n$2"');
136
+ // 6. Fix escaped quotes that might be double-escaped
137
+ fixed = fixed.replace(/\\\\"/g, '\\"');
138
+ return fixed;
139
+ }
102
140
  async generateMealPlan(request) {
103
- const systemPrompt = `You are a professional nutritionist and meal planning expert. Generate personalized meal plans based on user profiles, dietary restrictions, and health goals. Always respond with valid JSON format only, no additional text.
141
+ const systemPrompt = `You are a professional nutritionist and meal planning expert. Generate personalized meal plans based on user profiles, dietary restrictions, and health goals.
142
+
143
+ CRITICAL: Respond with ONLY valid JSON. No explanations, no markdown formatting, no additional text. Your entire response must be a single JSON object.
104
144
 
105
145
  The JSON response must follow this exact structure:
106
146
  {
@@ -126,7 +166,9 @@ The JSON response must follow this exact structure:
126
166
  "totalNutrition": {"calories": number, "protein": number, "carbs": number, "fat": number},
127
167
  "recommendations": ["string"],
128
168
  "shoppingList": [{"ingredient": "string", "quantity": number, "unit": "string", "category": "string"}]
129
- }`;
169
+ }
170
+
171
+ IMPORTANT: Ensure all strings are properly double-quoted, numbers are not quoted, and there are no trailing commas.`;
130
172
  const userContext = this.buildUserProfileContext(request.context);
131
173
  const planType = request.type === 'daily' ? 'one day' : 'seven days';
132
174
  let prompt = `Create a ${planType} meal plan for a person with the following profile:\n${userContext}\n\n`;
@@ -148,7 +190,13 @@ The JSON response must follow this exact structure:
148
190
  prompt += `Ensure nutritional balance, variety, and alignment with the user's goals. Include detailed cooking instructions, accurate nutritional information, and practical tips.`;
149
191
  try {
150
192
  const response = await this.makeRequest(prompt, systemPrompt);
193
+ // Log the raw response for debugging
194
+ console.log('Raw AI meal plan response:', response.response);
151
195
  const parsedData = this.parseJSONResponse(response.response);
196
+ // Validate the parsed data has required fields
197
+ if (!parsedData.name || !parsedData.meals || !Array.isArray(parsedData.meals)) {
198
+ throw new Error('Invalid meal plan structure: missing required fields');
199
+ }
152
200
  return {
153
201
  success: true,
154
202
  data: parsedData,
@@ -160,6 +208,7 @@ The JSON response must follow this exact structure:
160
208
  };
161
209
  }
162
210
  catch (error) {
211
+ console.error('Meal plan generation error:', error);
163
212
  return {
164
213
  success: false,
165
214
  error: error instanceof Error ? error.message : 'Unknown error occurred'
@@ -167,7 +216,9 @@ The JSON response must follow this exact structure:
167
216
  }
168
217
  }
169
218
  async generateRecipe(request) {
170
- const systemPrompt = `You are a professional chef and nutritionist. Generate detailed, healthy recipes based on user requirements. Always respond with valid JSON format only, no additional text.
219
+ const systemPrompt = `You are a professional chef and nutritionist. Generate detailed, healthy recipes based on user requirements.
220
+
221
+ CRITICAL: Respond with ONLY valid JSON. No explanations, no markdown formatting, no additional text. Your entire response must be a single JSON object.
171
222
 
172
223
  The JSON response must follow this exact structure:
173
224
  {
@@ -183,7 +234,9 @@ The JSON response must follow this exact structure:
183
234
  "difficulty": "easy" | "medium" | "hard",
184
235
  "tags": ["string"],
185
236
  "tips": ["string"]
186
- }`;
237
+ }
238
+
239
+ IMPORTANT: Ensure all strings are properly double-quoted, numbers are not quoted, and there are no trailing commas.`;
187
240
  const userContext = this.buildUserProfileContext(request.context);
188
241
  let prompt = `Create a ${request.mealType} recipe for a person with the following profile:\n${userContext}\n\n`;
189
242
  if (request.ingredients?.length) {
@@ -201,7 +254,13 @@ The JSON response must follow this exact structure:
201
254
  prompt += `\nGenerate a detailed recipe with accurate nutritional information, clear instructions, and helpful cooking tips.`;
202
255
  try {
203
256
  const response = await this.makeRequest(prompt, systemPrompt);
257
+ // Log the raw response for debugging
258
+ console.log('Raw AI recipe response:', response.response);
204
259
  const parsedData = this.parseJSONResponse(response.response);
260
+ // Validate the parsed data has required fields
261
+ if (!parsedData.name || !parsedData.ingredients || !parsedData.instructions) {
262
+ throw new Error('Invalid recipe structure: missing required fields');
263
+ }
205
264
  return {
206
265
  success: true,
207
266
  data: parsedData,
@@ -213,6 +272,7 @@ The JSON response must follow this exact structure:
213
272
  };
214
273
  }
215
274
  catch (error) {
275
+ console.error('Recipe generation error:', error);
216
276
  return {
217
277
  success: false,
218
278
  error: error instanceof Error ? error.message : 'Unknown error occurred'
@@ -0,0 +1,26 @@
1
+ import { AIServiceInterface, AIMealPlanRequest, AIRecipeRequest, AIGeneratedMeal, AIGeneratedMealPlan, AIResponse, AIPromptContext } from '../types/ai';
2
+ interface AIConfig {
3
+ baseUrl: string;
4
+ model: string;
5
+ apiKey?: string;
6
+ timeout?: number;
7
+ }
8
+ export declare class UnifiedAIService implements AIServiceInterface {
9
+ private config;
10
+ private isGroq;
11
+ constructor(config: AIConfig);
12
+ private makeRequest;
13
+ private extractResponseContent;
14
+ private buildUserProfileContext;
15
+ private parseJSONResponse;
16
+ private fixCommonJSONIssues;
17
+ generateMealPlan(request: AIMealPlanRequest): Promise<AIResponse<AIGeneratedMealPlan>>;
18
+ generateRecipe(request: AIRecipeRequest): Promise<AIResponse<AIGeneratedMeal>>;
19
+ suggestMealVariations(meal: AIGeneratedMeal, context: AIPromptContext): Promise<AIResponse<AIGeneratedMeal[]>>;
20
+ analyzeMealPlan(meals: AIGeneratedMeal[], context: AIPromptContext): Promise<AIResponse<{
21
+ nutritionalAnalysis: string;
22
+ recommendations: string[];
23
+ improvements: string[];
24
+ }>>;
25
+ }
26
+ export {};
@@ -0,0 +1,448 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UnifiedAIService = void 0;
4
+ class UnifiedAIService {
5
+ constructor(config) {
6
+ this.config = {
7
+ timeout: 60000,
8
+ ...config
9
+ };
10
+ this.isGroq = config.baseUrl.includes('groq.com') || config.baseUrl.includes('openai.com');
11
+ }
12
+ async makeRequest(prompt, systemPrompt) {
13
+ let requestBody;
14
+ let endpoint;
15
+ let headers = {
16
+ 'Content-Type': 'application/json',
17
+ };
18
+ if (this.isGroq) {
19
+ // Groq/OpenAI API format
20
+ requestBody = {
21
+ model: this.config.model,
22
+ messages: [
23
+ ...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),
24
+ { role: 'user', content: prompt }
25
+ ],
26
+ temperature: 0.7,
27
+ max_tokens: 4000,
28
+ stream: false
29
+ };
30
+ endpoint = this.isGroq ? '' : '/chat/completions';
31
+ if (this.config.apiKey) {
32
+ headers['Authorization'] = `Bearer ${this.config.apiKey}`;
33
+ }
34
+ }
35
+ else {
36
+ // Ollama API format
37
+ requestBody = {
38
+ model: this.config.model,
39
+ prompt: prompt,
40
+ system: systemPrompt,
41
+ stream: false,
42
+ options: {
43
+ temperature: 0.7,
44
+ top_p: 0.9,
45
+ top_k: 40,
46
+ }
47
+ };
48
+ endpoint = '/api/generate';
49
+ }
50
+ try {
51
+ const response = await fetch(`${this.config.baseUrl}${endpoint}`, {
52
+ method: 'POST',
53
+ headers,
54
+ body: JSON.stringify(requestBody),
55
+ signal: AbortSignal.timeout(this.config.timeout)
56
+ });
57
+ if (!response.ok) {
58
+ throw new Error(`AI API error: ${response.status} ${response.statusText}`);
59
+ }
60
+ const data = await response.json();
61
+ return data;
62
+ }
63
+ catch (error) {
64
+ if (error instanceof Error) {
65
+ throw new Error(`Failed to connect to AI service: ${error.message}`);
66
+ }
67
+ throw new Error('Unknown error occurred while connecting to AI service');
68
+ }
69
+ }
70
+ extractResponseContent(data) {
71
+ if (this.isGroq) {
72
+ // Groq/OpenAI response format
73
+ return data.choices?.[0]?.message?.content || '';
74
+ }
75
+ else {
76
+ // Ollama response format
77
+ return data.response || '';
78
+ }
79
+ }
80
+ buildUserProfileContext(context) {
81
+ const { userProfile, preferences, nutritionalTargets } = context;
82
+ let profileText = '';
83
+ if (userProfile.age)
84
+ profileText += `Age: ${userProfile.age} years old. `;
85
+ if (userProfile.gender)
86
+ profileText += `Gender: ${userProfile.gender}. `;
87
+ if (userProfile.height && userProfile.weight) {
88
+ profileText += `Height: ${userProfile.height}cm, Weight: ${userProfile.weight}kg. `;
89
+ }
90
+ if (userProfile.activityLevel)
91
+ profileText += `Activity level: ${userProfile.activityLevel}. `;
92
+ if (userProfile.healthGoals?.length) {
93
+ profileText += `Health goals: ${userProfile.healthGoals.join(', ')}. `;
94
+ }
95
+ if (userProfile.dietaryRestrictions?.length) {
96
+ profileText += `Dietary restrictions: ${userProfile.dietaryRestrictions.join(', ')}. `;
97
+ }
98
+ if (userProfile.allergies?.length) {
99
+ profileText += `Allergies: ${userProfile.allergies.join(', ')}. `;
100
+ }
101
+ if (userProfile.healthConditions?.length) {
102
+ profileText += `Health conditions: ${userProfile.healthConditions.join(', ')}. `;
103
+ }
104
+ if (preferences?.cuisineTypes?.length) {
105
+ profileText += `Preferred cuisines: ${preferences.cuisineTypes.join(', ')}. `;
106
+ }
107
+ if (preferences?.cookingTime) {
108
+ profileText += `Preferred cooking time: ${preferences.cookingTime} minutes max. `;
109
+ }
110
+ if (nutritionalTargets) {
111
+ profileText += `Nutritional targets: `;
112
+ if (nutritionalTargets.calories)
113
+ profileText += `${nutritionalTargets.calories} calories, `;
114
+ if (nutritionalTargets.protein)
115
+ profileText += `${nutritionalTargets.protein}g protein, `;
116
+ if (nutritionalTargets.carbs)
117
+ profileText += `${nutritionalTargets.carbs}g carbs, `;
118
+ if (nutritionalTargets.fat)
119
+ profileText += `${nutritionalTargets.fat}g fat. `;
120
+ }
121
+ return profileText.trim();
122
+ }
123
+ parseJSONResponse(response) {
124
+ try {
125
+ console.log('Raw AI response for debugging:', response);
126
+ // Clean up the response - remove any markdown code blocks and extra text
127
+ let cleanedResponse = response.trim();
128
+ // Remove markdown code blocks more aggressively
129
+ cleanedResponse = cleanedResponse.replace(/```json\s*/gi, '');
130
+ cleanedResponse = cleanedResponse.replace(/```\s*$/gi, '');
131
+ cleanedResponse = cleanedResponse.replace(/```\s*$/gi, '');
132
+ // Remove any explanatory text before/after JSON
133
+ const jsonStartIndex = cleanedResponse.indexOf('{');
134
+ const jsonEndIndex = cleanedResponse.lastIndexOf('}');
135
+ if (jsonStartIndex !== -1 && jsonEndIndex !== -1 && jsonEndIndex > jsonStartIndex) {
136
+ cleanedResponse = cleanedResponse.substring(jsonStartIndex, jsonEndIndex + 1);
137
+ }
138
+ console.log('Cleaned JSON response:', cleanedResponse);
139
+ // Try parsing the cleaned response
140
+ try {
141
+ const parsed = JSON.parse(cleanedResponse);
142
+ return parsed;
143
+ }
144
+ catch (parseError) {
145
+ console.log('Direct parse failed, trying to fix common issues:', parseError);
146
+ // If direct parsing fails, try to fix common JSON issues
147
+ const fixedJson = this.fixCommonJSONIssues(cleanedResponse);
148
+ console.log('Fixed JSON:', fixedJson);
149
+ return JSON.parse(fixedJson);
150
+ }
151
+ }
152
+ catch (error) {
153
+ // Log the actual response for debugging
154
+ console.error('Failed to parse AI response. Raw response:', response);
155
+ console.error('Parse error:', error);
156
+ throw new Error(`Failed to parse AI response as JSON: ${error instanceof Error ? error.message : 'Unknown error'}`);
157
+ }
158
+ }
159
+ fixCommonJSONIssues(jsonString) {
160
+ let fixed = jsonString;
161
+ // Fix common issues with AI-generated JSON
162
+ // 1. Fix trailing commas in arrays and objects
163
+ fixed = fixed.replace(/,\s*([}\]])/g, '$1');
164
+ // 2. Fix unquoted property names
165
+ fixed = fixed.replace(/([{\s,])([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
166
+ // 3. Fix single quotes instead of double quotes
167
+ fixed = fixed.replace(/'/g, '"');
168
+ // 4. Fix missing quotes around string values
169
+ fixed = fixed.replace(/:\s*([a-zA-Z_][a-zA-Z0-9_]*)([\s,}])/g, ': "$1"$2');
170
+ // 5. Fix line breaks in strings
171
+ fixed = fixed.replace(/"([^"]*)\n([^"]*)"/g, '"$1\\n$2"');
172
+ // 6. Fix escaped quotes that might be double-escaped
173
+ fixed = fixed.replace(/\\\\"/g, '\\"');
174
+ return fixed;
175
+ }
176
+ async generateMealPlan(request) {
177
+ const systemPrompt = `You are a professional nutritionist and meal planning expert. Generate personalized meal plans based on user profiles, dietary restrictions, and health goals.
178
+
179
+ CRITICAL INSTRUCTIONS:
180
+ 1. Respond with ONLY valid JSON - no explanations, no markdown, no extra text
181
+ 2. Your ENTIRE response must be a single JSON object starting with { and ending with }
182
+ 3. Do NOT use triple backticks json code blocks
183
+ 4. Do NOT add any text before or after the JSON
184
+ 5. Ensure all strings are properly double-quoted
185
+ 6. Numbers must NOT be quoted
186
+ 7. No trailing commas allowed
187
+
188
+ The JSON response must follow this exact structure:
189
+ {
190
+ "name": "string",
191
+ "description": "string",
192
+ "type": "daily" | "weekly",
193
+ "meals": [
194
+ {
195
+ "name": "string",
196
+ "category": "breakfast" | "lunch" | "dinner" | "snack",
197
+ "description": "string",
198
+ "ingredients": [{"name": "string", "quantity": number, "unit": "string"}],
199
+ "instructions": ["string"],
200
+ "nutrition": {"calories": number, "protein": number, "carbs": number, "fat": number, "fiber": number, "sugar": number, "sodium": number},
201
+ "prepTime": number,
202
+ "cookTime": number,
203
+ "servings": number,
204
+ "difficulty": "easy" | "medium" | "hard",
205
+ "tags": ["string"],
206
+ "tips": ["string"]
207
+ }
208
+ ],
209
+ "totalNutrition": {"calories": number, "protein": number, "carbs": number, "fat": number},
210
+ "recommendations": ["string"],
211
+ "shoppingList": [{"ingredient": "string", "quantity": number, "unit": "string", "category": "string"}]
212
+ }
213
+
214
+ VIOLATION OF THESE INSTRUCTIONS WILL RESULT IN INVALID OUTPUT.`;
215
+ const userContext = this.buildUserProfileContext(request.context);
216
+ const planType = request.type === 'daily' ? 'one day' : 'seven days';
217
+ let prompt = `Create a ${planType} meal plan for a person with the following profile:\n${userContext}\n\n`;
218
+ if (request.existingMeals?.length) {
219
+ prompt += `Avoid repeating these meals: ${request.existingMeals.join(', ')}\n\n`;
220
+ }
221
+ if (request.avoidIngredients?.length) {
222
+ prompt += `Avoid these ingredients: ${request.avoidIngredients.join(', ')}\n\n`;
223
+ }
224
+ if (request.focusAreas?.length) {
225
+ prompt += `Focus on these areas: ${request.focusAreas.join(', ')}\n\n`;
226
+ }
227
+ if (request.type === 'daily') {
228
+ prompt += `Generate 4 meals: breakfast, lunch, dinner, and one snack. `;
229
+ }
230
+ else {
231
+ prompt += `Generate 28 meals total: 4 meals per day for 7 days (breakfast, lunch, dinner, snack each day). `;
232
+ }
233
+ prompt += `Ensure nutritional balance, variety, and alignment with the user's goals. Include detailed cooking instructions, accurate nutritional information, and practical tips.`;
234
+ try {
235
+ const response = await this.makeRequest(prompt, systemPrompt);
236
+ // Extract the actual response content
237
+ const responseContent = this.extractResponseContent(response);
238
+ // Log the raw response for debugging
239
+ console.log('Raw AI meal plan response:', responseContent);
240
+ const parsedData = this.parseJSONResponse(responseContent);
241
+ // Validate the parsed data has required fields
242
+ if (!parsedData.name || !parsedData.meals || !Array.isArray(parsedData.meals)) {
243
+ throw new Error('Invalid meal plan structure: missing required fields');
244
+ }
245
+ return {
246
+ success: true,
247
+ data: parsedData,
248
+ usage: {
249
+ promptTokens: response.usage?.prompt_tokens || 0,
250
+ completionTokens: response.usage?.completion_tokens || 0,
251
+ totalTokens: response.usage?.total_tokens || 0
252
+ }
253
+ };
254
+ }
255
+ catch (error) {
256
+ console.error('Meal plan generation error:', error);
257
+ return {
258
+ success: false,
259
+ error: error instanceof Error ? error.message : 'Unknown error occurred'
260
+ };
261
+ }
262
+ }
263
+ async generateRecipe(request) {
264
+ const systemPrompt = `You are a professional chef and nutritionist. Generate detailed, healthy recipes based on user requirements.
265
+
266
+ CRITICAL INSTRUCTIONS:
267
+ 1. Respond with ONLY valid JSON - no explanations, no markdown, no extra text
268
+ 2. Your ENTIRE response must be a single JSON object starting with { and ending with }
269
+ 3. Do NOT use triple backticks json code blocks
270
+ 4. Do NOT add any text before or after the JSON
271
+ 5. Ensure all strings are properly double-quoted
272
+ 6. Numbers must NOT be quoted
273
+ 7. No trailing commas allowed
274
+
275
+ The JSON response must follow this exact structure:
276
+ {
277
+ "name": "string",
278
+ "category": "breakfast" | "lunch" | "dinner" | "snack",
279
+ "description": "string",
280
+ "ingredients": [{"name": "string", "quantity": number, "unit": "string"}],
281
+ "instructions": ["string"],
282
+ "nutrition": {"calories": number, "protein": number, "carbs": number, "fat": number, "fiber": number, "sugar": number, "sodium": number},
283
+ "prepTime": number,
284
+ "cookTime": number,
285
+ "servings": number,
286
+ "difficulty": "easy" | "medium" | "hard",
287
+ "tags": ["string"],
288
+ "tips": ["string"]
289
+ }
290
+
291
+ VIOLATION OF THESE INSTRUCTIONS WILL RESULT IN INVALID OUTPUT.`;
292
+ const userContext = this.buildUserProfileContext(request.context);
293
+ let prompt = `Create a ${request.mealType} recipe for a person with the following profile:\n${userContext}\n\n`;
294
+ if (request.ingredients?.length) {
295
+ prompt += `Include these ingredients: ${request.ingredients.join(', ')}\n`;
296
+ }
297
+ if (request.cookingTime) {
298
+ prompt += `Maximum cooking time: ${request.cookingTime} minutes\n`;
299
+ }
300
+ if (request.difficulty) {
301
+ prompt += `Difficulty level: ${request.difficulty}\n`;
302
+ }
303
+ if (request.servings) {
304
+ prompt += `Number of servings: ${request.servings}\n`;
305
+ }
306
+ prompt += `\nGenerate a detailed recipe with accurate nutritional information, clear instructions, and helpful cooking tips.`;
307
+ try {
308
+ const response = await this.makeRequest(prompt, systemPrompt);
309
+ // Extract the actual response content
310
+ const responseContent = this.extractResponseContent(response);
311
+ // Log the raw response for debugging
312
+ console.log('Raw AI recipe response:', responseContent);
313
+ const parsedData = this.parseJSONResponse(responseContent);
314
+ // Validate the parsed data has required fields
315
+ if (!parsedData.name || !parsedData.ingredients || !parsedData.instructions) {
316
+ throw new Error('Invalid recipe structure: missing required fields');
317
+ }
318
+ return {
319
+ success: true,
320
+ data: parsedData,
321
+ usage: {
322
+ promptTokens: response.usage?.prompt_tokens || 0,
323
+ completionTokens: response.usage?.completion_tokens || 0,
324
+ totalTokens: response.usage?.total_tokens || 0
325
+ }
326
+ };
327
+ }
328
+ catch (error) {
329
+ console.error('Recipe generation error:', error);
330
+ return {
331
+ success: false,
332
+ error: error instanceof Error ? error.message : 'Unknown error occurred'
333
+ };
334
+ }
335
+ }
336
+ async suggestMealVariations(meal, context) {
337
+ const systemPrompt = `You are a creative chef. Generate 3-5 variations of the given meal while maintaining similar nutritional profile. Always respond with valid JSON array format only.
338
+
339
+ The JSON response must be an array of meal objects following this structure:
340
+ [
341
+ {
342
+ "name": "string",
343
+ "category": "breakfast" | "lunch" | "dinner" | "snack",
344
+ "description": "string",
345
+ "ingredients": [{"name": "string", "quantity": number, "unit": "string"}],
346
+ "instructions": ["string"],
347
+ "nutrition": {"calories": number, "protein": number, "carbs": number, "fat": number, "fiber": number, "sugar": number, "sodium": number},
348
+ "prepTime": number,
349
+ "cookTime": number,
350
+ "servings": number,
351
+ "difficulty": "easy" | "medium" | "hard",
352
+ "tags": ["string"],
353
+ "tips": ["string"]
354
+ }
355
+ ]`;
356
+ const userContext = this.buildUserProfileContext(context);
357
+ const prompt = `Create 3-5 variations of this meal: "${meal.name}"
358
+
359
+ Original meal details:
360
+ - Category: ${meal.category}
361
+ - Calories: ${meal.nutrition.calories}
362
+ - Protein: ${meal.nutrition.protein}g
363
+ - Main ingredients: ${meal.ingredients.slice(0, 5).map(i => i.name).join(', ')}
364
+
365
+ User profile: ${userContext}
366
+
367
+ Generate variations that:
368
+ 1. Keep similar nutritional values (±10% calories)
369
+ 2. Use different cooking methods or ingredient substitutions
370
+ 3. Maintain the same meal category
371
+ 4. Consider the user's dietary restrictions and preferences`;
372
+ try {
373
+ const response = await this.makeRequest(prompt, systemPrompt);
374
+ const responseContent = this.extractResponseContent(response);
375
+ const parsedData = this.parseJSONResponse(responseContent);
376
+ return {
377
+ success: true,
378
+ data: parsedData,
379
+ usage: {
380
+ promptTokens: response.usage?.prompt_tokens || 0,
381
+ completionTokens: response.usage?.completion_tokens || 0,
382
+ totalTokens: response.usage?.total_tokens || 0
383
+ }
384
+ };
385
+ }
386
+ catch (error) {
387
+ return {
388
+ success: false,
389
+ error: error instanceof Error ? error.message : 'Unknown error occurred'
390
+ };
391
+ }
392
+ }
393
+ async analyzeMealPlan(meals, context) {
394
+ const systemPrompt = `You are a registered dietitian. Analyze meal plans and provide professional nutritional advice. Always respond with valid JSON format only.
395
+
396
+ The JSON response must follow this exact structure:
397
+ {
398
+ "nutritionalAnalysis": "string",
399
+ "recommendations": ["string"],
400
+ "improvements": ["string"]
401
+ }`;
402
+ const userContext = this.buildUserProfileContext(context);
403
+ const totalNutrition = meals.reduce((total, meal) => ({
404
+ calories: total.calories + meal.nutrition.calories,
405
+ protein: total.protein + meal.nutrition.protein,
406
+ carbs: total.carbs + meal.nutrition.carbs,
407
+ fat: total.fat + meal.nutrition.fat
408
+ }), { calories: 0, protein: 0, carbs: 0, fat: 0 });
409
+ const prompt = `Analyze this meal plan for a person with the following profile:
410
+ ${userContext}
411
+
412
+ Meal Plan Summary:
413
+ - Total meals: ${meals.length}
414
+ - Total calories: ${totalNutrition.calories}
415
+ - Total protein: ${totalNutrition.protein}g
416
+ - Total carbs: ${totalNutrition.carbs}g
417
+ - Total fat: ${totalNutrition.fat}g
418
+
419
+ Meals included:
420
+ ${meals.map(meal => `- ${meal.name} (${meal.category}): ${meal.nutrition.calories} cal`).join('\n')}
421
+
422
+ Provide:
423
+ 1. A comprehensive nutritional analysis
424
+ 2. Specific recommendations for this user
425
+ 3. Concrete improvements that could be made`;
426
+ try {
427
+ const response = await this.makeRequest(prompt, systemPrompt);
428
+ const responseContent = this.extractResponseContent(response);
429
+ const parsedData = this.parseJSONResponse(responseContent);
430
+ return {
431
+ success: true,
432
+ data: parsedData,
433
+ usage: {
434
+ promptTokens: response.usage?.prompt_tokens || 0,
435
+ completionTokens: response.usage?.completion_tokens || 0,
436
+ totalTokens: response.usage?.total_tokens || 0
437
+ }
438
+ };
439
+ }
440
+ catch (error) {
441
+ return {
442
+ success: false,
443
+ error: error instanceof Error ? error.message : 'Unknown error occurred'
444
+ };
445
+ }
446
+ }
447
+ }
448
+ exports.UnifiedAIService = UnifiedAIService;
@@ -8,6 +8,7 @@ export interface AIModelConfig {
8
8
  export interface OllamaConfig {
9
9
  baseUrl: string;
10
10
  model: string;
11
+ apiKey?: string;
11
12
  timeout?: number;
12
13
  }
13
14
  export interface AIPromptContext {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usamir/healthy-meals-core",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "main": "dist/src/index.js",
5
5
  "types": "dist/src/index.d.ts",
6
6
  "exports": {
@@ -60,6 +60,10 @@
60
60
  "types": "./dist/src/services/aiMealPlanGenerator.d.ts",
61
61
  "default": "./dist/src/services/aiMealPlanGenerator.js"
62
62
  },
63
+ "./services/unified": {
64
+ "types": "./dist/src/services/unifiedAIService.d.ts",
65
+ "default": "./dist/src/services/unifiedAIService.js"
66
+ },
63
67
  "./services/shoppingListGenerator": {
64
68
  "types": "./dist/src/services/shoppingListGenerator.d.ts",
65
69
  "default": "./dist/src/services/shoppingListGenerator.js"