arshenoy commited on
Commit
f618d4b
·
verified ·
1 Parent(s): 956cf6f

Implementation of 3 rollbacks to somAI

Browse files
Files changed (1) hide show
  1. services/geminiService.ts +103 -81
services/geminiService.ts CHANGED
@@ -20,9 +20,14 @@ const getApiKey = () => {
20
  const API_KEY = getApiKey();
21
  const ai = new GoogleGenAI({ apiKey: API_KEY });
22
 
23
- // CHANGED: Switched to Flash-Lite for 1,000 Requests/Day (vs 250 for standard Flash)
24
- const MODEL_FAST = 'gemini-2.5-flash-lite';
 
 
 
 
25
  const MODEL_TTS = 'gemini-2.5-flash-preview-tts';
 
26
  const FALLBACK_API_BASE = 'https://arshenoy-somai-backend.hf.space';
27
 
28
  // Cleaning for final blocks
@@ -147,25 +152,21 @@ const parseRiskResponse = (text: string, calculatedScore: number): RiskAnalysisR
147
  }
148
  };
149
 
150
- // --- UPDATED: NAME EXTRACTION & TTS ---
151
-
152
  export const extractClinicalData = async (imageBase64: string): Promise<ExtractionResult> => {
153
  const base64Data = imageBase64.includes('base64,') ? imageBase64.split('base64,')[1] : imageBase64;
154
- // Improved Prompt for Name Extraction
155
  const prompt = `Analyze this medical document.
156
  CRITICAL: Look for the Patient's Name at the top, headers, or labeled 'Patient', 'Name', 'Mr/Mrs'.
157
  Extract JSON: { name, age, condition, history, allergies, systolicBp, glucose, heartRate, weight, temperature, spo2, clinicalNote }.
158
  If name is missing, use "Guest". Return JSON only.`;
159
 
160
- try {
161
- if (!API_KEY) throw new Error("API Key missing");
162
-
163
  const response = await ai.models.generateContent({
164
- model: MODEL_FAST,
165
  contents: [{ role: 'user', parts: [{ text: prompt }, { inlineData: { mimeType: 'image/jpeg', data: base64Data } }] }],
166
  config: { responseMimeType: "application/json", maxOutputTokens: 2000 }
167
  });
168
-
169
  const text = response.text || "{}";
170
  const data = JSON.parse(text);
171
  return {
@@ -173,13 +174,29 @@ export const extractClinicalData = async (imageBase64: string): Promise<Extracti
173
  vitals: { systolicBp: data.systolicBp, glucose: data.glucose, heartRate: data.heartRate, weight: data.weight, temperature: data.temperature, spo2: data.spo2, clinicalNote: data.clinicalNote },
174
  confidence: 0.9
175
  };
 
176
 
 
 
 
 
177
  } catch (e: any) {
 
 
 
 
 
 
 
 
 
 
 
178
  try {
 
179
  const compressedBase64 = await compressImage(imageBase64);
180
  const cleanBase64 = compressedBase64.includes('base64,') ? compressedBase64.split('base64,')[1] : compressedBase64;
181
  const resText = await callFallbackAPI('/vision', { image: cleanBase64, prompt: "Extract patient name and vitals from this document in JSON format." });
182
-
183
  return {
184
  profile: {},
185
  vitals: { clinicalNote: `[Auto-Scanned]: ${resText}` },
@@ -200,14 +217,9 @@ export const generateSpeech = async (text: string): Promise<string | null> => {
200
  contents: [{ parts: [{ text }] }],
201
  config: {
202
  responseModalities: ['AUDIO'],
203
- speechConfig: {
204
- voiceConfig: {
205
- prebuiltVoiceConfig: { voiceName: 'Fenrir' }, // Fenrir = Cool Male Voice
206
- },
207
- },
208
  },
209
  });
210
- // Return Base64 Audio Data
211
  return response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data || null;
212
  } catch (e) {
213
  console.warn("TTS Failed", e);
@@ -229,6 +241,7 @@ export const transcribeAudio = async (audioBlob: Blob): Promise<string> => {
229
  });
230
  };
231
 
 
232
  export const analyzeRisk = async (profile: PatientProfile, vitals: ClinicalVitals, calculatedScore: number): Promise<RiskAnalysisResult> => {
233
  const prompt = `
234
  Act as a Senior Clinical Risk Assessor.
@@ -241,32 +254,44 @@ export const analyzeRisk = async (profile: PatientProfile, vitals: ClinicalVital
241
  Return JSON.
242
  `;
243
 
244
- try {
245
- if (!API_KEY) throw new Error("API Key missing");
246
  const response = await ai.models.generateContent({
247
- model: MODEL_FAST,
248
- contents: prompt,
249
- config: {
250
- responseMimeType: "application/json",
251
- maxOutputTokens: 4000,
252
- responseSchema: {
253
- type: Type.OBJECT,
254
- properties: {
255
- summary: { type: Type.STRING },
256
- actionItems: { type: Type.ARRAY, items: { type: Type.STRING } },
257
- primaryConditionCode: { type: Type.OBJECT, properties: { code: {type: Type.STRING}, description: {type: Type.STRING} } },
258
- historyCodes: { type: Type.ARRAY, items: { type: Type.OBJECT, properties: { code: {type: Type.STRING}, description: {type: Type.STRING} } } },
259
- insuranceNote: { type: Type.STRING }
260
- },
261
- required: ["summary", "actionItems", "primaryConditionCode", "historyCodes", "insuranceNote"]
 
262
  }
263
- }
264
  });
265
- return {
266
- ...parseRiskResponse(response.text || "{}", calculatedScore),
267
- source: 'Gemini 2.5 Flash-Lite'
268
- };
 
 
 
269
  } catch (err: any) {
 
 
 
 
 
 
 
 
 
270
  try {
271
  const payload = { ...profile, ...vitals, riskScore: calculatedScore, prompt };
272
  const fallback = await callFallbackAPI('/analyze', payload);
@@ -282,43 +307,42 @@ export const analyzeRisk = async (profile: PatientProfile, vitals: ClinicalVital
282
 
283
  export const generateHealthInsights = async (profile: PatientProfile, vitals: ClinicalVitals): Promise<HealthInsights> => {
284
  const prompt = `Based on Patient: ${profile.name}, ${profile.age}y, ${profile.condition}. Vitals: BP ${vitals.systolicBp}, SpO2 ${vitals.spo2}%. Generate JSON: { weeklySummary, progress, tips: [] }.`;
285
- try {
286
- if (!API_KEY) throw new Error("No Key");
287
  const response = await ai.models.generateContent({
288
- model: MODEL_FAST,
289
  contents: prompt,
290
  config: { responseMimeType: "application/json", maxOutputTokens: 2000 }
291
  });
292
  return JSON.parse(response.text || "{}");
293
- } catch {
 
 
 
 
 
 
 
 
294
  return { weeklySummary: "Keep tracking your vitals.", progress: "Data accumulated.", tips: ["Maintain a balanced diet.", "Stay hydrated."] };
295
  }
296
  };
297
 
298
  export const generateSessionName = async (userText: string, aiText: string): Promise<string> => {
299
- const prompt = `Generate a very short, specific title (max 4 words) for a medical chat session based on this context.
300
- User: ${userText}
301
- AI: ${aiText}
302
- Title:`;
303
-
304
  try {
305
  if (!API_KEY) return "New Consultation";
306
- const response = await ai.models.generateContent({
307
- model: MODEL_FAST,
308
- contents: prompt,
309
- config: { maxOutputTokens: 20 }
310
- });
311
  return cleanText(response.text || "New Consultation").replace(/^["']|["']$/g, '');
312
  } catch (e) {
313
  try {
314
  const fallbackRes = await callFallbackAPI('/generate', { prompt: prompt });
315
  return cleanText(fallbackRes).replace(/^["']|["']$/g, '');
316
- } catch {
317
- return "New Consultation";
318
- }
319
  }
320
  };
321
 
 
322
  export const generateChatResponse = async (
323
  history: ChatMessage[],
324
  currentMessage: string,
@@ -340,50 +364,48 @@ export const generateChatResponse = async (
340
  const contents = history.map(msg => ({ role: msg.role === 'user' ? 'user' : 'model', parts: [{ text: msg.text }, ...(msg.image ? [{ inlineData: { mimeType: 'image/jpeg', data: msg.image.split('base64,')[1] } }] : [])] }));
341
  contents.push({ role: 'user', parts: [{ text: context + "\nUser: " + currentMessage }, ...(image ? [{ inlineData: { mimeType: 'image/jpeg', data: image.split('base64,')[1] } }] : [])] });
342
 
343
- try {
344
- if (!API_KEY) throw new Error("No Key");
345
-
346
- // 1. Try Gemini
347
- onSource('Gemini 2.5 Flash-Lite');
348
  const response = await ai.models.generateContent({
349
- model: MODEL_FAST,
350
- contents: contents,
351
- config: {
352
- maxOutputTokens: 4000,
353
- temperature: 0.7
354
- }
355
  });
356
-
357
  return cleanText(response.text || "I didn't catch that.");
 
358
 
359
- } catch (e) {
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  try {
361
- // 2. Fallback
362
  onSource('Phi-3 Mini (Fallback)');
363
  const fallbackPrompt = `${context}\n\nChat History:\n${history.slice(-3).map(m => m.text).join('\n')}\nUser: ${currentMessage}`;
364
  const responseText = await callFallbackAPI('/generate', { prompt: fallbackPrompt });
365
  return cleanText(responseText);
366
-
367
  } catch {
368
  return "I'm having trouble connecting. Please check your internet.";
369
  }
370
  }
371
  };
372
 
373
- // --- UPDATED: CONTEXT-AWARE QUICK REPLIES ---
374
  export const generateQuickReplies = async (history: ChatMessage[]) => {
375
  if (!API_KEY || history.length === 0) return [];
376
-
377
- // Use last 3 messages for context
378
  const recentContext = history.slice(-3).map(m => `${m.role}: ${m.text}`).join('\n');
379
  const prompt = `Based on this conversation:\n${recentContext}\n\nSuggest 3 short, relevant follow-up questions the USER might want to ask next. Return ONLY a JSON array of strings.`;
380
-
381
  try {
382
- const res = await ai.models.generateContent({
383
- model: MODEL_FAST,
384
- contents: prompt,
385
- config: { responseMimeType: "application/json" }
386
- });
387
  return JSON.parse(res.text || "[]");
388
  } catch { return []; }
389
  };
@@ -391,7 +413,7 @@ export const generateQuickReplies = async (history: ChatMessage[]) => {
391
  export const summarizeConversation = async (history: ChatMessage[]) => {
392
  if (!API_KEY) return "Summary unavailable.";
393
  try {
394
- const res = await ai.models.generateContent({ model: MODEL_FAST, contents: `Summarize clinical conversation:\n${history.map(m=>m.text).join('\n')}` });
395
  return cleanText(res.text || "");
396
  } catch { return "Could not summarize."; }
397
  };
 
20
  const API_KEY = getApiKey();
21
  const ai = new GoogleGenAI({ apiKey: API_KEY });
22
 
23
+ // --- TIERED MODEL STRATEGY ---
24
+ // Tier 1: 1,000 RPD (Primary)
25
+ const MODEL_TIER_1 = 'gemini-2.5-flash-lite';
26
+ // Tier 2: 250 RPD (Backup High-Speed)
27
+ const MODEL_TIER_2 = 'gemini-2.5-flash';
28
+ // TTS Model
29
  const MODEL_TTS = 'gemini-2.5-flash-preview-tts';
30
+
31
  const FALLBACK_API_BASE = 'https://arshenoy-somai-backend.hf.space';
32
 
33
  // Cleaning for final blocks
 
152
  }
153
  };
154
 
155
+ // --- UPDATED: VISION EXTRACTION (TIER 1 -> TIER 2 -> FALLBACK) ---
 
156
  export const extractClinicalData = async (imageBase64: string): Promise<ExtractionResult> => {
157
  const base64Data = imageBase64.includes('base64,') ? imageBase64.split('base64,')[1] : imageBase64;
 
158
  const prompt = `Analyze this medical document.
159
  CRITICAL: Look for the Patient's Name at the top, headers, or labeled 'Patient', 'Name', 'Mr/Mrs'.
160
  Extract JSON: { name, age, condition, history, allergies, systolicBp, glucose, heartRate, weight, temperature, spo2, clinicalNote }.
161
  If name is missing, use "Guest". Return JSON only.`;
162
 
163
+ // Helper to call Gemini Vision
164
+ const callGeminiVision = async (modelName: string) => {
 
165
  const response = await ai.models.generateContent({
166
+ model: modelName,
167
  contents: [{ role: 'user', parts: [{ text: prompt }, { inlineData: { mimeType: 'image/jpeg', data: base64Data } }] }],
168
  config: { responseMimeType: "application/json", maxOutputTokens: 2000 }
169
  });
 
170
  const text = response.text || "{}";
171
  const data = JSON.parse(text);
172
  return {
 
174
  vitals: { systolicBp: data.systolicBp, glucose: data.glucose, heartRate: data.heartRate, weight: data.weight, temperature: data.temperature, spo2: data.spo2, clinicalNote: data.clinicalNote },
175
  confidence: 0.9
176
  };
177
+ };
178
 
179
+ try {
180
+ if (!API_KEY) throw new Error("API Key missing");
181
+ // 1. Try Tier 1 (Flash Lite)
182
+ return await callGeminiVision(MODEL_TIER_1);
183
  } catch (e: any) {
184
+ // 2. If Quota Error (429), Try Tier 2 (Flash)
185
+ if (e.toString().includes('429') || e.toString().includes('Quota')) {
186
+ try {
187
+ console.warn("Tier 1 Vision Quota Exceeded. Switching to Tier 2 (Flash)...");
188
+ return await callGeminiVision(MODEL_TIER_2);
189
+ } catch (e2) {
190
+ // Tier 2 Failed, proceed to fallback
191
+ }
192
+ }
193
+
194
+ // 3. Fallback (Moondream)
195
  try {
196
+ console.warn("Gemini Vision Failed. Attempting Fallback...");
197
  const compressedBase64 = await compressImage(imageBase64);
198
  const cleanBase64 = compressedBase64.includes('base64,') ? compressedBase64.split('base64,')[1] : compressedBase64;
199
  const resText = await callFallbackAPI('/vision', { image: cleanBase64, prompt: "Extract patient name and vitals from this document in JSON format." });
 
200
  return {
201
  profile: {},
202
  vitals: { clinicalNote: `[Auto-Scanned]: ${resText}` },
 
217
  contents: [{ parts: [{ text }] }],
218
  config: {
219
  responseModalities: ['AUDIO'],
220
+ speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: 'Fenrir' } } },
 
 
 
 
221
  },
222
  });
 
223
  return response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data || null;
224
  } catch (e) {
225
  console.warn("TTS Failed", e);
 
241
  });
242
  };
243
 
244
+ // --- UPDATED: RISK ANALYSIS (TIER 1 -> TIER 2 -> FALLBACK) ---
245
  export const analyzeRisk = async (profile: PatientProfile, vitals: ClinicalVitals, calculatedScore: number): Promise<RiskAnalysisResult> => {
246
  const prompt = `
247
  Act as a Senior Clinical Risk Assessor.
 
254
  Return JSON.
255
  `;
256
 
257
+ // Helper for Gemini Call
258
+ const callGeminiRisk = async (modelName: string) => {
259
  const response = await ai.models.generateContent({
260
+ model: modelName,
261
+ contents: prompt,
262
+ config: {
263
+ responseMimeType: "application/json",
264
+ maxOutputTokens: 4000,
265
+ responseSchema: {
266
+ type: Type.OBJECT,
267
+ properties: {
268
+ summary: { type: Type.STRING },
269
+ actionItems: { type: Type.ARRAY, items: { type: Type.STRING } },
270
+ primaryConditionCode: { type: Type.OBJECT, properties: { code: {type: Type.STRING}, description: {type: Type.STRING} } },
271
+ historyCodes: { type: Type.ARRAY, items: { type: Type.OBJECT, properties: { code: {type: Type.STRING}, description: {type: Type.STRING} } } },
272
+ insuranceNote: { type: Type.STRING }
273
+ },
274
+ required: ["summary", "actionItems", "primaryConditionCode", "historyCodes", "insuranceNote"]
275
+ }
276
  }
 
277
  });
278
+ return { ...parseRiskResponse(response.text || "{}", calculatedScore), source: modelName === MODEL_TIER_1 ? 'Gemini 2.5 Flash-Lite' : 'Gemini 2.5 Flash' };
279
+ };
280
+
281
+ try {
282
+ if (!API_KEY) throw new Error("API Key missing");
283
+ // 1. Try Tier 1
284
+ return await callGeminiRisk(MODEL_TIER_1);
285
  } catch (err: any) {
286
+ // 2. If Quota Error, Try Tier 2
287
+ if (err.toString().includes('429') || err.toString().includes('Quota')) {
288
+ try {
289
+ console.warn("Tier 1 Risk Quota Exceeded. Switching to Tier 2...");
290
+ return await callGeminiRisk(MODEL_TIER_2);
291
+ } catch (e2) {}
292
+ }
293
+
294
+ // 3. Fallback
295
  try {
296
  const payload = { ...profile, ...vitals, riskScore: calculatedScore, prompt };
297
  const fallback = await callFallbackAPI('/analyze', payload);
 
307
 
308
  export const generateHealthInsights = async (profile: PatientProfile, vitals: ClinicalVitals): Promise<HealthInsights> => {
309
  const prompt = `Based on Patient: ${profile.name}, ${profile.age}y, ${profile.condition}. Vitals: BP ${vitals.systolicBp}, SpO2 ${vitals.spo2}%. Generate JSON: { weeklySummary, progress, tips: [] }.`;
310
+
311
+ const callGeminiInsights = async (model: string) => {
312
  const response = await ai.models.generateContent({
313
+ model: model,
314
  contents: prompt,
315
  config: { responseMimeType: "application/json", maxOutputTokens: 2000 }
316
  });
317
  return JSON.parse(response.text || "{}");
318
+ }
319
+
320
+ try {
321
+ if (!API_KEY) throw new Error("No Key");
322
+ return await callGeminiInsights(MODEL_TIER_1);
323
+ } catch (err: any) {
324
+ if (err.toString().includes('429')) {
325
+ try { return await callGeminiInsights(MODEL_TIER_2); } catch (e) {}
326
+ }
327
  return { weeklySummary: "Keep tracking your vitals.", progress: "Data accumulated.", tips: ["Maintain a balanced diet.", "Stay hydrated."] };
328
  }
329
  };
330
 
331
  export const generateSessionName = async (userText: string, aiText: string): Promise<string> => {
332
+ const prompt = `Generate a very short, specific title (max 4 words) for a medical chat session based on this context. User: ${userText}. AI: ${aiText}. Title:`;
 
 
 
 
333
  try {
334
  if (!API_KEY) return "New Consultation";
335
+ const response = await ai.models.generateContent({ model: MODEL_TIER_1, contents: prompt, config: { maxOutputTokens: 20 } });
 
 
 
 
336
  return cleanText(response.text || "New Consultation").replace(/^["']|["']$/g, '');
337
  } catch (e) {
338
  try {
339
  const fallbackRes = await callFallbackAPI('/generate', { prompt: prompt });
340
  return cleanText(fallbackRes).replace(/^["']|["']$/g, '');
341
+ } catch { return "New Consultation"; }
 
 
342
  }
343
  };
344
 
345
+ // --- UPDATED: CHAT (TIER 1 -> TIER 2 -> FALLBACK) ---
346
  export const generateChatResponse = async (
347
  history: ChatMessage[],
348
  currentMessage: string,
 
364
  const contents = history.map(msg => ({ role: msg.role === 'user' ? 'user' : 'model', parts: [{ text: msg.text }, ...(msg.image ? [{ inlineData: { mimeType: 'image/jpeg', data: msg.image.split('base64,')[1] } }] : [])] }));
365
  contents.push({ role: 'user', parts: [{ text: context + "\nUser: " + currentMessage }, ...(image ? [{ inlineData: { mimeType: 'image/jpeg', data: image.split('base64,')[1] } }] : [])] });
366
 
367
+ // Helper for Chat
368
+ const callGeminiChat = async (modelName: string) => {
369
+ onSource(modelName === MODEL_TIER_1 ? 'Gemini 2.5 Flash-Lite' : 'Gemini 2.5 Flash');
 
 
370
  const response = await ai.models.generateContent({
371
+ model: modelName,
372
+ contents: contents,
373
+ config: { maxOutputTokens: 4000, temperature: 0.7 }
 
 
 
374
  });
 
375
  return cleanText(response.text || "I didn't catch that.");
376
+ };
377
 
378
+ try {
379
+ if (!API_KEY) throw new Error("No Key");
380
+ // 1. Try Tier 1
381
+ return await callGeminiChat(MODEL_TIER_1);
382
+ } catch (e: any) {
383
+ // 2. If Quota Error, Try Tier 2
384
+ if (e.toString().includes('429') || e.toString().includes('Quota')) {
385
+ try {
386
+ console.warn("Tier 1 Chat Quota Exceeded. Switching to Tier 2...");
387
+ return await callGeminiChat(MODEL_TIER_2);
388
+ } catch (e2) {}
389
+ }
390
+
391
+ // 3. Fallback
392
  try {
 
393
  onSource('Phi-3 Mini (Fallback)');
394
  const fallbackPrompt = `${context}\n\nChat History:\n${history.slice(-3).map(m => m.text).join('\n')}\nUser: ${currentMessage}`;
395
  const responseText = await callFallbackAPI('/generate', { prompt: fallbackPrompt });
396
  return cleanText(responseText);
 
397
  } catch {
398
  return "I'm having trouble connecting. Please check your internet.";
399
  }
400
  }
401
  };
402
 
 
403
  export const generateQuickReplies = async (history: ChatMessage[]) => {
404
  if (!API_KEY || history.length === 0) return [];
 
 
405
  const recentContext = history.slice(-3).map(m => `${m.role}: ${m.text}`).join('\n');
406
  const prompt = `Based on this conversation:\n${recentContext}\n\nSuggest 3 short, relevant follow-up questions the USER might want to ask next. Return ONLY a JSON array of strings.`;
 
407
  try {
408
+ const res = await ai.models.generateContent({ model: MODEL_TIER_1, contents: prompt, config: { responseMimeType: "application/json" } });
 
 
 
 
409
  return JSON.parse(res.text || "[]");
410
  } catch { return []; }
411
  };
 
413
  export const summarizeConversation = async (history: ChatMessage[]) => {
414
  if (!API_KEY) return "Summary unavailable.";
415
  try {
416
+ const res = await ai.models.generateContent({ model: MODEL_TIER_1, contents: `Summarize clinical conversation:\n${history.map(m=>m.text).join('\n')}` });
417
  return cleanText(res.text || "");
418
  } catch { return "Could not summarize."; }
419
  };