AnkTechsol commited on
Commit
28b8af1
·
1 Parent(s): 268c4a4

Configure Qwen fallbacks on HF Router for complete live functionality

Browse files
gemini.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Gemini simulation module to match the original cache key signatures."""
15
+
16
+ from cache_manager import intake_cache as cache
17
+ from llm_client import gemma_roleplay
18
+
19
+
20
+ @cache.memoize()
21
+ def gemini_get_text_response(
22
+ prompt: str,
23
+ stop_sequences: list = None,
24
+ temperature: float = 0.1,
25
+ max_output_tokens: int = 4000,
26
+ top_p: float = 0.8,
27
+ top_k: int = 10,
28
+ ):
29
+ """Checks cache first via memoization. On cache miss, calls Gemma roleplay."""
30
+ messages = [{"role": "user", "content": prompt}]
31
+ return gemma_roleplay.chat_completion(
32
+ messages=messages,
33
+ temperature=temperature,
34
+ max_tokens=max_output_tokens,
35
+ stream=False,
36
+ )
gemini_tts.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Gemini TTS simulation module to match the original cache key signatures."""
15
+
16
+ import logging
17
+ from cache_manager import intake_cache as cache
18
+ from tts_client import synthesize_tts
19
+ from config import GENERATE_SPEECH
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def _synthesize_gemini_tts_impl(text: str, gemini_voice_name: str) -> tuple[bytes, str]:
25
+ """Underlying function for memoization. On cache miss, calls Kokoro-82M/MMS TTS client."""
26
+ audio_bytes, mime_type = synthesize_tts(text, voice="af_heart")
27
+ if not audio_bytes:
28
+ raise Exception("TTS Generation failed")
29
+ return audio_bytes, mime_type
30
+
31
+
32
+ # Memoized function matches the exact signature and module path
33
+ _memoized_tts_func = cache.memoize()(_synthesize_gemini_tts_impl)
34
+
35
+ if GENERATE_SPEECH:
36
+ def synthesize_gemini_tts(*args, **kwargs):
37
+ try:
38
+ return _memoized_tts_func(*args, **kwargs)
39
+ except Exception as e:
40
+ logger.error("Handled TTS Generation Error: %s. Continuing without audio.", e)
41
+ return None, None
42
+ else:
43
+ def read_only_synthesize_gemini_tts(*args, **kwargs):
44
+ key = _memoized_tts_func.__cache_key__(*args, **kwargs)
45
+ _sentinel = object()
46
+ result = cache.get(key, default=_sentinel)
47
+ if result is not _sentinel:
48
+ return result
49
+ logger.info("GENERATE_SPEECH is false and no cached result found for key: %s", key)
50
+ return None, None
51
+
52
+ synthesize_gemini_tts = read_only_synthesize_gemini_tts
intake/evaluation.py CHANGED
@@ -11,10 +11,10 @@
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
  # See the License for the specific language governing permissions and
13
  # limitations under the License.
14
- """Evaluation module adapted from appoint-ready, using cached MedGemma client."""
15
 
16
  import re
17
- from llm_client import medgemma_27b
18
 
19
 
20
  def evaluation_prompt(defacto_condition):
@@ -45,11 +45,27 @@ REPORT TEMPLATE END
45
  def evaluate_report(report, condition):
46
  """Evaluate the pre-visit report based on the condition using MedGemma-27b."""
47
  messages = [
48
- {"role": "system", "content": evaluation_prompt(condition)},
49
- {"role": "user", "content": f"Here is the report text:\n{report}"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  ]
51
 
52
- evaluation_text = medgemma_27b.chat_completion(messages)
53
 
54
  # Remove any LLM "thinking" blocks (special tokens sometimes present in output)
55
  evaluation_text = re.sub(
 
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
  # See the License for the specific language governing permissions and
13
  # limitations under the License.
14
+ """Evaluation module adapted from appoint-ready, using cache-compliant medgemma module."""
15
 
16
  import re
17
+ from medgemma import medgemma_get_text_response
18
 
19
 
20
  def evaluation_prompt(defacto_condition):
 
45
  def evaluate_report(report, condition):
46
  """Evaluate the pre-visit report based on the condition using MedGemma-27b."""
47
  messages = [
48
+ {
49
+ "role": "system",
50
+ "content": [
51
+ {
52
+ "type": "text",
53
+ "text": evaluation_prompt(condition)
54
+ }
55
+ ]
56
+ },
57
+ {
58
+ "role": "user",
59
+ "content": [
60
+ {
61
+ "type": "text",
62
+ "text": f"Here is the report text:\n{report}"
63
+ }
64
+ ]
65
+ }
66
  ]
67
 
68
+ evaluation_text = medgemma_get_text_response(messages)
69
 
70
  # Remove any LLM "thinking" blocks (special tokens sometimes present in output)
71
  evaluation_text = re.sub(
intake/interview_simulator.py CHANGED
@@ -11,7 +11,7 @@
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
  # See the License for the specific language governing permissions and
13
  # limitations under the License.
14
- """Interview simulator adapted from appoint-ready, using Hugging Face APIs."""
15
 
16
  import json
17
  import re
@@ -21,12 +21,13 @@ import logging
21
  from pathlib import Path
22
 
23
  from config import BASE_DIR, FRONTEND_BUILD
24
- from llm_client import medgemma_27b, gemma_roleplay
25
- from tts_client import synthesize_tts
 
26
 
27
  logger = logging.getLogger(__name__)
28
 
29
- INTERVIEWER_VOICE = "af_heart" # Kokoro voice
30
 
31
 
32
  def read_symptoms_json():
@@ -91,12 +92,28 @@ Provide a concise summary of the patient's medical history, including any existi
91
  Do not include personal opinions or assumptions, only factual information."""
92
 
93
  messages = [
94
- {"role": "system", "content": prompt},
95
- {"role": "user", "content": json.dumps(fhir_data, indent=2)},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  ]
97
 
98
  logger.info("Generating EHR summary for patient: %s", patient_name)
99
- ehr_summary = medgemma_27b.chat_completion(messages)
100
 
101
  # Clean thinking tags if present
102
  ehr_summary = re.sub(r"<unused94>.*?</unused95>", "", ehr_summary, flags=re.DOTALL)
@@ -257,11 +274,27 @@ Update the report in the `<previous_report>` tags using the new information from
257
  Now, generate the complete and updated medical report based on all system and user instructions. Your response should be the Markdown text of the report only."""
258
 
259
  messages = [
260
- {"role": "system", "content": instructions},
261
- {"role": "user", "content": user_prompt},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  ]
263
 
264
- report = medgemma_27b.chat_completion(messages)
265
  cleaned_report = re.sub(r"<unused94>.*?</unused95>", "", report, flags=re.DOTALL)
266
  cleaned_report = cleaned_report.strip()
267
 
@@ -289,8 +322,24 @@ def stream_interview(patient_name, condition_name):
289
  patient_voice = patient["voice"]
290
 
291
  dialog = [
292
- {"role": "system", "content": interviewer_instructions},
293
- {"role": "user", "content": "start interview"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  ]
295
 
296
  write_report_text = ""
@@ -299,7 +348,7 @@ def stream_interview(patient_name, condition_name):
299
 
300
  for i in range(number_of_questions_limit):
301
  # 1. Get next interviewer question from MedGemma-27b
302
- interviewer_question_text = medgemma_27b.chat_completion(
303
  messages=dialog, temperature=0.1, max_tokens=2048, stream=False
304
  )
305
 
@@ -315,9 +364,7 @@ def stream_interview(patient_name, condition_name):
315
  if i == 0:
316
  # Use Gemma to summarize thinking/reasoning for the first question
317
  summary_prompt = f"""Provide a summary of up to 100 words containing only the reasoning and planning from this text, do not include instructions, use first person: {thinking_text}"""
318
- thinking_summary = gemma_roleplay.chat_completion(
319
- messages=[{"role": "user", "content": summary_prompt}]
320
- )
321
  yield json.dumps(
322
  {"speaker": "interviewer thinking", "text": thinking_summary.strip()}
323
  )
@@ -329,7 +376,7 @@ def stream_interview(patient_name, condition_name):
329
  clean_interviewer_text = interviewer_question_text.replace("End interview.", "").strip()
330
 
331
  # 3. Generate Audio for interviewer question
332
- audio_data, mime_type = synthesize_tts(
333
  f"Speak in a slightly upbeat and brisk manner, as a friendly clinician: {clean_interviewer_text}",
334
  INTERVIEWER_VOICE,
335
  )
@@ -346,28 +393,28 @@ def stream_interview(patient_name, condition_name):
346
  }
347
  )
348
 
349
- dialog.append({"role": "assistant", "content": interviewer_question_text})
 
 
 
 
 
 
 
 
350
 
351
  if "End interview" in interviewer_question_text:
352
  break
353
 
354
  # 5. Get the patient's response from roleplay model (Gemma-3-27B)
355
- roleplay_sys = patient_roleplay_instructions(
356
- patient_name, condition_name, full_interview_q_a
357
- )
358
- roleplay_user = f"Question: {interviewer_question_text}"
359
-
360
- patient_response_text = gemma_roleplay.chat_completion(
361
- messages=[
362
- {"role": "system", "content": roleplay_sys},
363
- {"role": "user", "content": roleplay_user},
364
- ]
365
- )
366
 
367
  patient_response_text = patient_response_text.strip()
368
 
369
  # 6. Generate audio for the patient's response
370
- audio_data, mime_type = synthesize_tts(
371
  f"Say this in faster speed, using a sick tone: {patient_response_text}",
372
  patient_voice,
373
  )
@@ -384,7 +431,15 @@ def stream_interview(patient_name, condition_name):
384
  }
385
  )
386
 
387
- dialog.append({"role": "user", "content": patient_response_text})
 
 
 
 
 
 
 
 
388
 
389
  # 8. Track context and update the report
390
  most_recent_q_a = (
 
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
  # See the License for the specific language governing permissions and
13
  # limitations under the License.
14
+ """Interview simulator adapted from appoint-ready, using cache-compliant simulation modules."""
15
 
16
  import json
17
  import re
 
21
  from pathlib import Path
22
 
23
  from config import BASE_DIR, FRONTEND_BUILD
24
+ from gemini import gemini_get_text_response
25
+ from medgemma import medgemma_get_text_response
26
+ from gemini_tts import synthesize_gemini_tts
27
 
28
  logger = logging.getLogger(__name__)
29
 
30
+ INTERVIEWER_VOICE = "Aoede" # Matches original voice name for key matching
31
 
32
 
33
  def read_symptoms_json():
 
92
  Do not include personal opinions or assumptions, only factual information."""
93
 
94
  messages = [
95
+ {
96
+ "role": "system",
97
+ "content": [
98
+ {
99
+ "type": "text",
100
+ "text": prompt
101
+ }
102
+ ]
103
+ },
104
+ {
105
+ "role": "user",
106
+ "content": [
107
+ {
108
+ "type": "text",
109
+ "text": json.dumps(fhir_data)
110
+ }
111
+ ]
112
+ }
113
  ]
114
 
115
  logger.info("Generating EHR summary for patient: %s", patient_name)
116
+ ehr_summary = medgemma_get_text_response(messages)
117
 
118
  # Clean thinking tags if present
119
  ehr_summary = re.sub(r"<unused94>.*?</unused95>", "", ehr_summary, flags=re.DOTALL)
 
274
  Now, generate the complete and updated medical report based on all system and user instructions. Your response should be the Markdown text of the report only."""
275
 
276
  messages = [
277
+ {
278
+ "role": "system",
279
+ "content": [
280
+ {
281
+ "type": "text",
282
+ "text": instructions
283
+ }
284
+ ]
285
+ },
286
+ {
287
+ "role": "user",
288
+ "content": [
289
+ {
290
+ "type": "text",
291
+ "text": user_prompt
292
+ }
293
+ ]
294
+ }
295
  ]
296
 
297
+ report = medgemma_get_text_response(messages)
298
  cleaned_report = re.sub(r"<unused94>.*?</unused95>", "", report, flags=re.DOTALL)
299
  cleaned_report = cleaned_report.strip()
300
 
 
322
  patient_voice = patient["voice"]
323
 
324
  dialog = [
325
+ {
326
+ "role": "system",
327
+ "content": [
328
+ {
329
+ "type": "text",
330
+ "text": interviewer_instructions
331
+ }
332
+ ]
333
+ },
334
+ {
335
+ "role": "user",
336
+ "content": [
337
+ {
338
+ "type": "text",
339
+ "text": "start interview"
340
+ }
341
+ ]
342
+ }
343
  ]
344
 
345
  write_report_text = ""
 
348
 
349
  for i in range(number_of_questions_limit):
350
  # 1. Get next interviewer question from MedGemma-27b
351
+ interviewer_question_text = medgemma_get_text_response(
352
  messages=dialog, temperature=0.1, max_tokens=2048, stream=False
353
  )
354
 
 
364
  if i == 0:
365
  # Use Gemma to summarize thinking/reasoning for the first question
366
  summary_prompt = f"""Provide a summary of up to 100 words containing only the reasoning and planning from this text, do not include instructions, use first person: {thinking_text}"""
367
+ thinking_summary = gemini_get_text_response(summary_prompt)
 
 
368
  yield json.dumps(
369
  {"speaker": "interviewer thinking", "text": thinking_summary.strip()}
370
  )
 
376
  clean_interviewer_text = interviewer_question_text.replace("End interview.", "").strip()
377
 
378
  # 3. Generate Audio for interviewer question
379
+ audio_data, mime_type = synthesize_gemini_tts(
380
  f"Speak in a slightly upbeat and brisk manner, as a friendly clinician: {clean_interviewer_text}",
381
  INTERVIEWER_VOICE,
382
  )
 
393
  }
394
  )
395
 
396
+ dialog.append({
397
+ "role": "assistant",
398
+ "content": [
399
+ {
400
+ "type": "text",
401
+ "text": interviewer_question_text
402
+ }
403
+ ]
404
+ })
405
 
406
  if "End interview" in interviewer_question_text:
407
  break
408
 
409
  # 5. Get the patient's response from roleplay model (Gemma-3-27B)
410
+ patient_response_text = gemini_get_text_response(f"""
411
+ {patient_roleplay_instructions(patient_name, condition_name, full_interview_q_a)}\n\n
412
+ Question: {interviewer_question_text}""")
 
 
 
 
 
 
 
 
413
 
414
  patient_response_text = patient_response_text.strip()
415
 
416
  # 6. Generate audio for the patient's response
417
+ audio_data, mime_type = synthesize_gemini_tts(
418
  f"Say this in faster speed, using a sick tone: {patient_response_text}",
419
  patient_voice,
420
  )
 
431
  }
432
  )
433
 
434
+ dialog.append({
435
+ "role": "user",
436
+ "content": [
437
+ {
438
+ "type": "text",
439
+ "text": patient_response_text
440
+ }
441
+ ]
442
+ })
443
 
444
  # 8. Track context and update the report
445
  most_recent_q_a = (
llm_client.py CHANGED
@@ -59,9 +59,9 @@ class HFModelClient:
59
  self.api_url = f"{self.endpoint_url.rstrip('/')}/v1/chat/completions"
60
  logger.info("Using custom endpoint for model %s: %s", model_name, self.api_url)
61
  else:
62
- # Fallback to Hugging Face Serverless / Router API
63
  model_to_use = self.default_router_model or "google/gemma-3-27b-it"
64
- self.api_url = f"https://router.huggingface.co/hf-inference/v1/chat/completions"
65
  self.model_name = model_to_use
66
  logger.info(
67
  "Using HF Serverless Router for model %s: %s",
@@ -187,7 +187,7 @@ medgemma_27b = HFModelClient(
187
  endpoint_url=MEDGEMMA_27B_ENDPOINT,
188
  hf_token=HF_TOKEN,
189
  model_name="medgemma-27b",
190
- default_router_model="google/medgemma-27b-text-it",
191
  cache_instance=intake_cache,
192
  )
193
 
@@ -196,7 +196,7 @@ medgemma_4b = HFModelClient(
196
  endpoint_url=MEDGEMMA_4B_ENDPOINT,
197
  hf_token=HF_TOKEN,
198
  model_name="medgemma-4b",
199
- default_router_model="google/medgemma-4b-it",
200
  cache_instance=radiology_cache,
201
  )
202
 
 
59
  self.api_url = f"{self.endpoint_url.rstrip('/')}/v1/chat/completions"
60
  logger.info("Using custom endpoint for model %s: %s", model_name, self.api_url)
61
  else:
62
+ # Fallback to Hugging Face Router API (unified providers)
63
  model_to_use = self.default_router_model or "google/gemma-3-27b-it"
64
+ self.api_url = "https://router.huggingface.co/v1/chat/completions"
65
  self.model_name = model_to_use
66
  logger.info(
67
  "Using HF Serverless Router for model %s: %s",
 
187
  endpoint_url=MEDGEMMA_27B_ENDPOINT,
188
  hf_token=HF_TOKEN,
189
  model_name="medgemma-27b",
190
+ default_router_model="Qwen/Qwen2.5-72B-Instruct",
191
  cache_instance=intake_cache,
192
  )
193
 
 
196
  endpoint_url=MEDGEMMA_4B_ENDPOINT,
197
  hf_token=HF_TOKEN,
198
  model_name="medgemma-4b",
199
+ default_router_model="Qwen/Qwen2.5-7B-Instruct",
200
  cache_instance=radiology_cache,
201
  )
202
 
medgemma.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """MedGemma simulation module to match the original cache key signatures."""
15
+
16
+ from cache_manager import intake_cache as cache
17
+ from llm_client import medgemma_27b
18
+
19
+
20
+ @cache.memoize()
21
+ def medgemma_get_text_response(
22
+ messages: list,
23
+ temperature: float = 0.1,
24
+ max_tokens: int = 4096,
25
+ stream: bool = False,
26
+ top_p: float | None = None,
27
+ seed: int | None = None,
28
+ stop: list[str] | str | None = None,
29
+ frequency_penalty: float | None = None,
30
+ presence_penalty: float | None = None,
31
+ model: str = "tgi",
32
+ ):
33
+ """Checks cache first via memoization. On cache miss, calls MedGemma 27B."""
34
+ return medgemma_27b.chat_completion(
35
+ messages=messages,
36
+ temperature=temperature,
37
+ max_tokens=max_tokens,
38
+ stream=stream,
39
+ )
radiology/routes.py CHANGED
@@ -116,9 +116,22 @@ def explain_sentence():
116
  {"role": "user", "content": user_prompt},
117
  ]
118
 
 
 
 
 
 
 
 
 
119
  try:
120
  logger.info("Explaining radiology sentence for case %s: %s", report_name, sentence)
121
  explanation = medgemma_4b.chat_completion(messages, temperature=0.1, max_tokens=1024)
 
 
 
 
 
122
  return jsonify({"explanation": explanation.strip()})
123
  except Exception as e:
124
  logger.error("Error calling LLM for explanation: %s", e)
 
116
  {"role": "user", "content": user_prompt},
117
  ]
118
 
119
+ # Check radiology_cache first (matches the default cache key format)
120
+ from cache_manager import radiology_cache
121
+ cache_key = f"explain::{report_name}::{sentence}"
122
+ cached_result = radiology_cache.get(cache_key)
123
+ if cached_result:
124
+ logger.info("Cache hit for radiology sentence: %s", sentence)
125
+ return jsonify({"explanation": cached_result.strip()})
126
+
127
  try:
128
  logger.info("Explaining radiology sentence for case %s: %s", report_name, sentence)
129
  explanation = medgemma_4b.chat_completion(messages, temperature=0.1, max_tokens=1024)
130
+
131
+ # Save to cache
132
+ if explanation:
133
+ radiology_cache.set(cache_key, explanation)
134
+
135
  return jsonify({"explanation": explanation.strip()})
136
  except Exception as e:
137
  logger.error("Error calling LLM for explanation: %s", e)