Update providers/llms/groq_llm_provider.py
Browse files- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
try:
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
if patient_info:
|
| 48 |
-
|
| 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')}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
)
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
| 59 |
for m in context:
|
| 60 |
-
messages.append(
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
try:
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
for m in context:
|
| 79 |
-
messages.append(
|
| 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 = [
|
| 103 |
for m in context:
|
| 104 |
-
messages.append(
|
| 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(
|
|
|
|
|
|
|
| 118 |
try:
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
| 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')}
|
| 128 |
)
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
| 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 ""
|