Amogh1221 commited on
Commit
df7cbd5
Β·
verified Β·
1 Parent(s): 63e75cd

Update providers/llms/groq_llm_provider.py

Browse files
Files changed (1) hide show
  1. providers/llms/groq_llm_provider.py +127 -27
providers/llms/groq_llm_provider.py CHANGED
@@ -4,17 +4,65 @@ from groq import Groq
4
  from core.ports import LLM1Output, LLM2Output, LLM3Output
5
  from core.prompts import LLM1_SYSTEM_PROMPT, LLM2_SYSTEM_PROMPT, LLM3_SYSTEM_PROMPT
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  class GroqLLMProvider:
8
  """
9
  Concrete implementation of the LLMProvider protocol using the Groq API.
10
- Provides true asynchronous concurrency (no local GPU threading lock required).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  """
12
  def __init__(self, model1: str, model2: str):
13
  self.model1 = model1
14
  self.model2 = model2
15
- # Relies on the GROQ_API_KEY environment variable being set
16
  self.client = Groq()
17
-
18
  def generate_opening_context(self, profile_recap: str | None) -> list:
19
  if profile_recap:
20
  return [
@@ -40,24 +88,48 @@ class GroqLLMProvider:
40
  ),
41
  }
42
  ]
43
-
44
- def psychiatrist_response(self, context: list, patient_info: dict = None) -> LLM1Output:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  try:
46
- sys_prompt = LLM1_SYSTEM_PROMPT
 
 
 
 
47
  if patient_info:
48
- demographics = (
49
  f"PATIENT DEMOGRAPHICS:\n"
50
  f"- Name: {patient_info.get('name', 'Unknown')}\n"
51
  f"- Age: {patient_info.get('age', 'Unknown')}\n"
52
  f"- Gender: {patient_info.get('gender', 'Unknown')}\n"
53
  f"- Nationality: {patient_info.get('nationality', 'Unknown')}\n"
54
- f"- Primary Concern: {patient_info.get('primary_concern', 'Unknown')}\n\n"
 
 
 
 
 
55
  )
56
- sys_prompt = demographics + sys_prompt
57
-
58
- messages = [{"role": "system", "content": sys_prompt}]
 
59
  for m in context:
60
- messages.append({"role": m["role"], "content": m["content"]})
61
 
62
  response = self.client.chat.completions.create(
63
  model=self.model1,
@@ -72,11 +144,33 @@ class GroqLLMProvider:
72
  print(f"[Groq LLM1 ERROR] {e}")
73
  raise e
74
 
75
- def internal_reasoning(self, context: list) -> LLM2Output:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  try:
77
- messages = [{"role": "system", "content": LLM2_SYSTEM_PROMPT}]
 
 
 
 
 
 
 
78
  for m in context:
79
- messages.append({"role": m["role"], "content": m["content"]})
80
 
81
  response = self.client.chat.completions.create(
82
  model=self.model2,
@@ -99,9 +193,9 @@ class GroqLLMProvider:
99
  f"{retrieved_context}\n\n"
100
  "Keep your response empathetic, natural, and under 4 sentences. Do not use markdown."
101
  )
102
- messages = [{"role": "system", "content": sys_prompt}]
103
  for m in context:
104
- messages.append({"role": m["role"], "content": m["content"]})
105
 
106
  response = self.client.chat.completions.create(
107
  model=self.model1,
@@ -114,9 +208,14 @@ class GroqLLMProvider:
114
  print(f"[Groq QUERY ERROR] {e}")
115
  raise e
116
 
117
- def generate_end_of_session_profile(self, old_profile: dict, session_history: list, patient_info: dict = None) -> LLM3Output:
 
 
118
  try:
119
- sys_prompt = LLM3_SYSTEM_PROMPT
 
 
 
120
  if patient_info:
121
  demographics = (
122
  f"PATIENT DEMOGRAPHICS:\n"
@@ -124,20 +223,20 @@ class GroqLLMProvider:
124
  f"- Age: {patient_info.get('age', 'Unknown')}\n"
125
  f"- Gender: {patient_info.get('gender', 'Unknown')}\n"
126
  f"- Nationality: {patient_info.get('nationality', 'Unknown')}\n"
127
- f"- Primary Concern: {patient_info.get('primary_concern', 'Unknown')}\n\n"
128
  )
129
- sys_prompt = demographics + sys_prompt
130
-
131
- formatted_history = "\n".join(f"{t['role'].upper()}: {t['content']}" for t in session_history)
 
 
 
132
  user_prompt = (
133
  f"EXISTING PROFILE:\n{old_profile}\n\n"
134
  f"RECENT SESSION TRANSCRIPT:\n{formatted_history}\n\n"
135
  "Please generate the 100-200 word session_summary and the merged, deduplicated clinical profile."
136
  )
137
- messages = [
138
- {"role": "system", "content": sys_prompt},
139
- {"role": "user", "content": user_prompt}
140
- ]
141
 
142
  response = self.client.chat.completions.create(
143
  model=self.model1,
@@ -152,6 +251,7 @@ class GroqLLMProvider:
152
  print(f"[Groq LLM3 ERROR] {e}")
153
  raise e
154
 
 
155
  def summarize_history(self, turns: list) -> str:
156
  if not turns:
157
  return ""
 
4
  from core.ports import LLM1Output, LLM2Output, LLM3Output
5
  from core.prompts import LLM1_SYSTEM_PROMPT, LLM2_SYSTEM_PROMPT, LLM3_SYSTEM_PROMPT
6
 
7
+ # ---------------------------------------------------------------------------
8
+ # Groq prompt-caching helpers
9
+ #
10
+ # Groq (beta) caches any prefix up to the LAST message that contains a
11
+ # cache_control marker. Multiple markers create cascading cache levels.
12
+ #
13
+ # Cache levels we exploit:
14
+ # Level 1 (all calls): system prompt β€” constant across all patients/sessions
15
+ # Level 2 (LLM1+LLM2): demographics + medium-term memory β€” constant within
16
+ # a session; only changes when LLM2 fires and updates long_term_memory
17
+ # Level 3 (dynamic): conversation history + RAG context β€” changes every turn
18
+ # ---------------------------------------------------------------------------
19
+
20
+ def _cached_msg(role: str, content: str) -> dict:
21
+ """Message with Groq beta cache_control breakpoint."""
22
+ return {
23
+ "role": role,
24
+ "content": [
25
+ {
26
+ "type": "text",
27
+ "text": content,
28
+ "cache_control": {"type": "ephemeral"},
29
+ }
30
+ ],
31
+ }
32
+
33
+
34
+ def _msg(role: str, content: str) -> dict:
35
+ """Plain message (no cache marker)."""
36
+ return {"role": role, "content": content}
37
+
38
+
39
  class GroqLLMProvider:
40
  """
41
  Concrete implementation of the LLMProvider protocol using the Groq API.
42
+
43
+ Prompt caching strategy (Groq beta cache_control):
44
+
45
+ LLM1 psychiatrist_response β€” two cache levels:
46
+ [1] system, cached LLM1_SYSTEM_PROMPT (~700 tokens, never changes)
47
+ [2] system, cached demographics + medium-term (~200-400 tokens, stable per session)
48
+ memory (long_term_memory recap)
49
+ [3] dynamic conversation history (changes every turn)
50
+
51
+ LLM2 internal_reasoning β€” two cache levels:
52
+ [1] system, cached LLM2_SYSTEM_PROMPT (~600 tokens, never changes)
53
+ [2] user, cached demographics + profile_recap (~200-400 tokens, stable between
54
+ (medium-term memory) LLM2 calls within a session)
55
+ [3] dynamic history[-6:] + analysis ctx (changes every turn)
56
+
57
+ Savings estimate vs. baseline (no caching):
58
+ - LLM1: ~1,000-1,100 cached tokens per turn after warm-up
59
+ - LLM2: ~1,200-1,400 cached tokens per ANALYZE call
60
  """
61
  def __init__(self, model1: str, model2: str):
62
  self.model1 = model1
63
  self.model2 = model2
 
64
  self.client = Groq()
65
+
66
  def generate_opening_context(self, profile_recap: str | None) -> list:
67
  if profile_recap:
68
  return [
 
88
  ),
89
  }
90
  ]
91
+
92
+ def psychiatrist_response(
93
+ self,
94
+ context: list,
95
+ patient_info: dict = None,
96
+ medium_term_memory: str | None = None,
97
+ ) -> LLM1Output:
98
+ """
99
+ LLM1 fast path. Message order:
100
+ [1] system, CACHED LLM1_SYSTEM_PROMPT (constant)
101
+ [2] system, CACHED demographics + MTM recap (stable within session)
102
+ [3] dynamic conversation history (changes every turn)
103
+
104
+ `medium_term_memory` is the long_term_memory recap string built by
105
+ the orchestrator from profile_store. It is constant between LLM2 updates.
106
+ """
107
  try:
108
+ # Level 1: constant system prompt β€” cached across all patients
109
+ messages = [_cached_msg("system", LLM1_SYSTEM_PROMPT)]
110
+
111
+ # Level 2: stable patient context β€” cached within a session
112
+ stable_parts = []
113
  if patient_info:
114
+ stable_parts.append(
115
  f"PATIENT DEMOGRAPHICS:\n"
116
  f"- Name: {patient_info.get('name', 'Unknown')}\n"
117
  f"- Age: {patient_info.get('age', 'Unknown')}\n"
118
  f"- Gender: {patient_info.get('gender', 'Unknown')}\n"
119
  f"- Nationality: {patient_info.get('nationality', 'Unknown')}\n"
120
+ f"- Primary Concern: {patient_info.get('primary_concern', 'Unknown')}"
121
+ )
122
+ if medium_term_memory:
123
+ stable_parts.append(
124
+ f"PATIENT MEDIUM-TERM MEMORY (prior sessions & patterns β€” do not quote verbatim):\n"
125
+ f"{medium_term_memory}"
126
  )
127
+ if stable_parts:
128
+ messages.append(_cached_msg("system", "\n\n".join(stable_parts)))
129
+
130
+ # Level 3: dynamic conversation history
131
  for m in context:
132
+ messages.append(_msg(m["role"], m["content"]))
133
 
134
  response = self.client.chat.completions.create(
135
  model=self.model1,
 
144
  print(f"[Groq LLM1 ERROR] {e}")
145
  raise e
146
 
147
+ def internal_reasoning(
148
+ self,
149
+ context: list,
150
+ stable_prefix: str | None = None,
151
+ ) -> LLM2Output:
152
+ """
153
+ LLM2 analysis path. Message order:
154
+ [1] system, CACHED LLM2_SYSTEM_PROMPT (constant)
155
+ [2] user, CACHED stable_prefix (demographics + profile_recap,
156
+ stable between LLM2 calls)
157
+ [3] dynamic history[-6:] + analysis ctx (changes every ANALYZE trigger)
158
+
159
+ `stable_prefix` is built by the orchestrator and contains demographics +
160
+ medium-term memory (profile_recap). It is constant for the lifetime of a
161
+ session between LLM2 updates β€” giving a solid cache hit on the 70b model.
162
+ """
163
  try:
164
+ # Level 1: constant system prompt
165
+ messages = [_cached_msg("system", LLM2_SYSTEM_PROMPT)]
166
+
167
+ # Level 2: stable patient context (demographics + profile recap)
168
+ if stable_prefix:
169
+ messages.append(_cached_msg("user", stable_prefix))
170
+
171
+ # Level 3: dynamic history + analysis context
172
  for m in context:
173
+ messages.append(_msg(m["role"], m["content"]))
174
 
175
  response = self.client.chat.completions.create(
176
  model=self.model2,
 
193
  f"{retrieved_context}\n\n"
194
  "Keep your response empathetic, natural, and under 4 sentences. Do not use markdown."
195
  )
196
+ messages = [_msg("system", sys_prompt)]
197
  for m in context:
198
+ messages.append(_msg(m["role"], m["content"]))
199
 
200
  response = self.client.chat.completions.create(
201
  model=self.model1,
 
208
  print(f"[Groq QUERY ERROR] {e}")
209
  raise e
210
 
211
+ def generate_end_of_session_profile(
212
+ self, old_profile: dict, session_history: list, patient_info: dict = None
213
+ ) -> LLM3Output:
214
  try:
215
+ # Level 1: constant system prompt β€” cached across all patients
216
+ messages = [_cached_msg("system", LLM3_SYSTEM_PROMPT)]
217
+
218
+ # Level 2: patient demographics β€” small, per-patient, no cache needed
219
  if patient_info:
220
  demographics = (
221
  f"PATIENT DEMOGRAPHICS:\n"
 
223
  f"- Age: {patient_info.get('age', 'Unknown')}\n"
224
  f"- Gender: {patient_info.get('gender', 'Unknown')}\n"
225
  f"- Nationality: {patient_info.get('nationality', 'Unknown')}\n"
226
+ f"- Primary Concern: {patient_info.get('primary_concern', 'Unknown')}"
227
  )
228
+ messages.append(_msg("system", demographics))
229
+
230
+ # Level 3: dynamic content β€” old profile + session transcript
231
+ formatted_history = "\n".join(
232
+ f"{t['role'].upper()}: {t['content']}" for t in session_history
233
+ )
234
  user_prompt = (
235
  f"EXISTING PROFILE:\n{old_profile}\n\n"
236
  f"RECENT SESSION TRANSCRIPT:\n{formatted_history}\n\n"
237
  "Please generate the 100-200 word session_summary and the merged, deduplicated clinical profile."
238
  )
239
+ messages.append(_msg("user", user_prompt))
 
 
 
240
 
241
  response = self.client.chat.completions.create(
242
  model=self.model1,
 
251
  print(f"[Groq LLM3 ERROR] {e}")
252
  raise e
253
 
254
+
255
  def summarize_history(self, turns: list) -> str:
256
  if not turns:
257
  return ""