Driftline / engine /responder.py.complex
jmisak's picture
Rename engine/responder.py to engine/responder.py.complex
385865d verified
import yaml
import os
from engine.utils import safe_log
# HF Inference API configuration - optimized for HF Spaces
HF_TOKEN = os.getenv("HF_TOKEN", None)
MODEL_NAME = os.getenv("MODEL_NAME", "mistralai/Mistral-7B-Instruct-v0.2")
MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "250"))
USE_API = os.getenv("USE_API", "true").lower() == "true"
# Initialize inference client
inference_client = None
def get_inference_client():
"""Get HF Inference API client (fast, runs on HF infrastructure)"""
global inference_client
if inference_client is not None:
return inference_client
if not USE_API:
safe_log("Inference mode", "API disabled, will use local fallback if available")
return None
try:
from huggingface_hub import InferenceClient
if not HF_TOKEN:
safe_log("HF Token", "No HF_TOKEN found, trying without authentication")
inference_client = InferenceClient()
else:
inference_client = InferenceClient(token=HF_TOKEN)
safe_log("Inference API", f"Connected to HF Inference API with model {MODEL_NAME}")
return inference_client
except ImportError:
safe_log("Inference API", "huggingface_hub not installed, install with: pip install huggingface_hub")
return None
except Exception as e:
safe_log("Inference API error", str(e))
return None
# Initialize on module import
inference_client = get_inference_client()
def format_traits(traits):
descriptors = []
for trait, value in traits.items():
if not isinstance(value, (int, float)):
continue
if value >= 0.85:
descriptors.append(f"Extremely {trait.replace('_', ' ')}")
elif value >= 0.65:
descriptors.append(f"Highly {trait.replace('_', ' ')}")
elif value >= 0.5:
descriptors.append(f"Moderately {trait.replace('_', ' ')}")
return descriptors
def generate_response(prompt, persona, event=None):
try:
# Extract persona fields
name = persona.get("name", "The HCP")
style = persona.get("communication_style", "neutral")
current_traits = persona.get("dynamic_state", {})
baseline_traits = persona.get("baseline_traits", {})
voice_instructions = persona.get("voice_instructions", "")
examples = persona.get("response_examples", [])
values = persona.get("values", [])
# Calculate trait changes (baseline vs current)
trait_changes = {}
key_traits = ["innovation", "openness", "risk_tolerance", "peer_influence"]
for trait in key_traits:
baseline = baseline_traits.get(trait, current_traits.get(trait, 0.5))
current = current_traits.get(trait, baseline)
change = current - baseline
if abs(change) > 0.05: # Only note significant changes
trait_changes[trait] = {"baseline": baseline, "current": current, "change": change}
# Extract event context
event_context = ""
if event:
event_name = event.get("event", "")
event_desc = event.get("description", "")
if event_name and event_name != "baseline":
event_context = f"Recent market event: {event_desc}"
# Voice style mapping
voice_map = {
"Confident, direct, data-driven": "Speak with clarity and conviction. Use clinical language.",
"Cautious, collaborative, practical": "Speak gently and with concern. Use simple, patient-centered language.",
"Analytical, pragmatic, structured": "Speak logically and reference evidence.",
"Gentle, emotionally attuned, cautious": "Speak with warmth and emotional sensitivity.",
"Bold, expressive, future-focused": "Speak with energy and optimism. Use visionary language."
}
voice_style = voice_map.get(style, "Speak neutrally.")
# Determine persona type from segment for personality enforcement
segment = persona.get("segment", "").lower()
# Get current trait values for dynamic personality
risk_tolerance = current_traits.get("risk_tolerance", 0.5)
innovation = current_traits.get("innovation", 0.5)
openness = current_traits.get("openness", 0.5)
peer_influence = current_traits.get("peer_influence", 0.5)
# Build personality-specific instructions dynamically based on traits
if "maverick" in segment or "visionary" in segment or "innovator" in segment:
if risk_tolerance >= 0.70:
boldness = "VERY BOLD and DECISIVE"
tone = """Use aggressive, confident language:
βœ“ DO SAY: "I prescribe", "I'm already using", "This works", "That's outdated", "I don't wait for"
βœ— DON'T SAY: "I meticulously evaluate", "carefully weighing", "I take into account", "I aim to optimize"
Be provocative and opinionated. Challenge conservative approaches."""
elif risk_tolerance >= 0.50:
boldness = "BOLD but MEASURED"
tone = """Use confident language but acknowledge concerns:
βœ“ DO SAY: "I'm still using it", "I monitor closely", "The benefit outweighs", "I adjust quickly"
βœ— DON'T SAY: "carefully weighing", "meticulously evaluate", "prioritizing evidence"
Stay assertive but show awareness of risks."""
else:
boldness = "CAUTIOUS MAVERICK"
tone = """You're normally bold, but recent events tempered you:
βœ“ DO SAY: "I'm pausing", "I need clearer data", "That safety signal concerns me"
βœ— DON'T SAY: Generic cautious medical speak. Show your maverick nature is still there but restrained."""
personality_note = f"""YOUR PERSONALITY: You are a {boldness} MAVERICK.
{tone}
CRITICAL: You are NOT an academic. You are NOT methodical. You are a clinical pioneer who takes calculated risks."""
elif "pragmatic" in segment or "adopter" in segment:
if openness >= 0.70:
stance = "OPEN and WILLING"
tone = """You're ready to adopt with solid evidence:
βœ“ DO SAY: "The data is solid", "I'm comfortable using this", "The outcomes justify", "In my practice"
βœ— DON'T SAY: "I meticulously evaluate", "I aim to optimize" (too academic)
You're pragmatic, not overcautious."""
elif openness >= 0.50:
stance = "BALANCED and ANALYTICAL"
tone = """Weigh pros/cons based on real evidence:
βœ“ DO SAY: "The evidence is promising but", "I want more real-world data", "The risk-benefit ratio"
βœ— DON'T SAY: Wishy-washy hedging. Be clear about what you need to see.
You're thoughtful, not indecisive."""
else:
stance = "CAUTIOUS and HESITANT"
tone = """Recent events made you more conservative:
βœ“ DO SAY: "I'm waiting for more clarity", "The recent concerns pause me", "I need stronger evidence"
βœ— DON'T SAY: Generic academic language.
Show your practical concerns, not theoretical ones."""
personality_note = f"""YOUR PERSONALITY: You are {stance} PRAGMATIST.
{tone}
CRITICAL: You're a practicing clinician focused on real-world outcomes, not a researcher."""
elif "moderate" in segment or "middle" in segment or "conservative" in segment:
if peer_influence >= 0.70 and openness >= 0.60:
stance = "READY TO ADOPT"
tone = """With guidelines and peer adoption, you're comfortable:
βœ“ DO SAY: "Now that it's in the guidelines", "My colleagues are using it successfully", "I'm ready to start"
βœ— DON'T SAY: Still hedging or being overly academic now that validation exists.
You waited for validation - now you have it."""
elif peer_influence >= 0.60:
stance = "CAUTIOUSLY OPEN"
tone = """You're warming up with peer validation:
βœ“ DO SAY: "I'm seeing colleagues succeed with it", "Once it's in guidelines", "I'm watching closely"
βœ— DON'T SAY: Academic language like "meticulously evaluate".
Reference your peers and guidelines specifically."""
else:
stance = "VERY CAUTIOUS"
tone = """You need stronger validation before moving:
βœ“ DO SAY: "I'm waiting for guidelines", "I need to see broader adoption", "Most colleagues haven't adopted"
βœ— DON'T SAY: Generic conservative language.
Be specific about what you're waiting for."""
personality_note = f"""YOUR PERSONALITY: You are {stance} and PEER-CONSCIOUS.
{tone}
CRITICAL: You follow the community standard. Reference guidelines, colleagues, and consensus - not just "evidence"."""
else:
personality_note = f"""YOUR PERSONALITY: {voice_instructions}"""
# Build trait change context
trait_context = ""
if trait_changes:
changes_desc = []
for trait, data in trait_changes.items():
change_val = data["change"]
if change_val > 0:
changes_desc.append(f"{trait.replace('_', ' ')} increased (+{abs(change_val):.2f})")
else:
changes_desc.append(f"{trait.replace('_', ' ')} decreased ({change_val:.2f})")
if changes_desc:
trait_context = f"Your mindset shifted: {', '.join(changes_desc)}. Reflect this subtle change in your response tone."
# Build concise system prompt with event and trait awareness
system_prompt = f"""You are {name}, {persona.get('role', 'physician')} in a real interview. Respond AS THIS PERSON, not as a textbook.
⚠️ CRITICAL ANTI-PATTERN TO AVOID ⚠️
DO NOT sound like a medical textbook or academic paper. DO NOT use phrases like:
❌ "I meticulously evaluate"
❌ "I carefully weigh"
❌ "I take into account"
❌ "I aim to optimize"
❌ "prioritizing evidence-based"
These are GENERIC ACADEMIC phrases. You are a REAL PRACTICING PHYSICIAN with personality.
{personality_note}
{voice_instructions}"""
# Add event context if present
if event_context:
system_prompt += f"\n\n{event_context}"
system_prompt += f"\nReference this event naturally in your response if relevant."
# Add trait change context if present
if trait_context:
system_prompt += f"\n\n{trait_context}"
system_prompt += """
Critical instructions:
- ONE focused response to the specific question (3-5 sentences)
- NO introductions, trait descriptions, or stating your name
- NO listing multiple diseases (MS, Alzheimer's, Parkinson's...) - answer about what was asked
- Show personality through strong opinions and specific clinical views
- Complete your thought - don't trail off mid-sentence"""
# Add examples if available
if examples:
system_prompt += f"\n\nYour actual speaking style:"
for ex in examples[:2]:
system_prompt += f"\nβ€’ {ex}"
# Use HF Inference API for fast responses (2-5 seconds)
if inference_client:
try:
safe_log("Generating", f"Using HF Inference API with {MODEL_NAME}")
# Build chat messages format with brevity and personality reinforcement
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"{prompt}\n\n(Answer directly in YOUR authentic voice - not textbook language. 3-5 complete sentences. NO phrases like 'I meticulously evaluate' or 'I carefully weigh'.)"}
]
# Call HF Inference API
response = inference_client.chat_completion(
messages=messages,
model=MODEL_NAME,
max_tokens=MAX_NEW_TOKENS,
temperature=0.7,
top_p=0.9,
)
reply = response.choices[0].message.content.strip()
if not reply:
raise ValueError("Empty API response")
return reply
except Exception as api_error:
safe_log("API error", f"HF Inference API failed: {api_error}, falling back to simple response")
# Fallback to a simple templated response based on persona
return f"As {name}, I'd like to discuss {prompt[:50]}... [API temporarily unavailable]"
else:
# Fallback: Simple response if API not available
safe_log("No API", "HF Inference API not configured, returning templated response")
return f"[API not configured] Please set HF_TOKEN environment variable to enable AI responses."
except Exception as e:
safe_log("Zephyr model error", str(e))
return "I’m having trouble formulating a response right now."