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."