Spaces:
Running
Running
File size: 16,628 Bytes
d2c3421 38bd2d4 aa90007 0264951 1365897 aa90007 4a30274 5f89d23 38bd2d4 aa90007 5f89d23 7ae43af f2d2182 86b388d 7e21141 f2d2182 9a97411 d0d6d4d 9a97411 7e21141 001d1ef ab35161 7e21141 9a97411 a49f925 3cd1e30 9a97411 7e21141 9a97411 d1594b3 7b70825 d1594b3 27f7c68 cd60f6d 27f7c68 aa90007 816374b 86b388d 816374b aa90007 816374b aa90007 816374b aa90007 816374b a258273 816374b aa90007 816374b aa90007 816374b 4a30274 816374b aa90007 d1594b3 7ae43af 9a97411 38bd2d4 7ae43af 714f4ae 4fffbcc 7ae43af 714f4ae 9955a0a 79a8b0c e483fb4 e9541b9 79a8b0c 7ae43af 79a8b0c 7ae43af 79a8b0c 7ae43af 79a8b0c 7ae43af 79a8b0c 7ae43af 79a8b0c 0264951 1365897 0264951 1365897 0264951 b87f064 1365897 0264951 7ae43af 0264951 7ae43af 0264951 f98d1cf 38bd2d4 a49f925 f98d1cf 38bd2d4 dad7c88 c6e3c53 dba1c38 c6e3c53 0264951 38bd2d4 f98d1cf 36140bb 9a97411 899134d 36140bb f804d88 f03ab9c 1a187b5 3cd1e30 6c824be 3cd1e30 1ab91ee fd36e75 7fd2cf6 0264951 9705616 0264951 1365897 7ae43af c6e3c53 9a97411 f804d88 9a97411 7ae43af |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
import os
import gradio as gr
from openai import OpenAI
import json
import requests
import datetime
import tempfile
openai_api_key = os.getenv("OPENROUTER_API_KEY")
openai_base_url = os.getenv("OPENAI_BASE_URL")
ai_model = os.getenv("AI_MODEL")
reasoning_ai_model = os.getenv("REASONING_AI_MODEL")
# Configure the OpenAI client with your custom API endpoint and API key.
client = OpenAI(base_url=openai_base_url, api_key=openai_api_key)
medical_recommendations = "MEDICAL RECOMMENDATIONS:\n\n" + "Birth control options sorted by effectiveness (typical-use rates), with brief pros, cons, and side effects:\n\nHighly Effective Methods (failure rate <1%)\n-Sterilization\n- Prevention rate: >99%\n- Pros: Permanent and low maintenance\n- Cons: Irreversible; requires surgery\n- side effects: Surgical risks (infection, pain)\n\n-Intrauterine Devices (IUDs) – Hormonal and Copper\n- Prevention rate: >99%\n- Pros: Long-term (3–10 years), low maintenance, reversible\n- Cons: Requires provider insertion; possible initial discomfort\n- Side effects:\n - Hormonal IUD: Initial irregular bleeding\n - Copper IUD: Heavier periods, cramping; rare risk of expulsion or uterine perforation\n\n-Implant (e.g., Nexplanon)\n- Prevention rate: >99%\n- Pros: Lasts up to 3 years, low maintenance, reversible\n- Cons: Requires minor procedure for insertion and removal; may cause irregular bleeding\n- Side effects: Mood changes, headaches, weight gain, pain at insertion site\n\nModerately Effective Methods (failure rate ~1–9%)\n-Injectable (e.g., Depo-Provera)\n- Prevention rate: ~96%\n- Pros: Injection every 3 months; high efficacy when on schedule\n- Cons: Can cause irregular bleeding; fertility may be delayed after stopping\n- Side effects: Weight gain, mood swings, potential bone density loss, injection site reactions\n\n-Oral Contraceptive Pills (combined or progestin-only)\n- Prevention rate: ~91%\n- Pros: Regulates cycles, may reduce cramps and help with acne; quick return to fertility\n- Cons: Must be taken daily; effectiveness depends on correct use\n- Side effects: Risk of blood clots (especially for smokers or women over 35), nausea, breast tenderness, mood changes, possible increased blood pressure\n- Prescriptions: Yaz, Yasmin, Ortho TriCyclen, Alesse, Loestrin\n- OTC: OPill $20/month, Taken Daily\n\n-Transdermal Patch (e.g., Ortho Evra)\n- Prevention rate: ~91%\n- Pros: Weekly application; steady hormone delivery\n- Cons: May cause skin irritation; visible on skin; less effective if detached\n- Side effects: Similar to pills (blood clots, nausea, breast tenderness, headaches)\n\n-Vaginal Ring (e.g., NuvaRing)\n- Prevention rate: ~91%\n- Pros: Monthly insertion; lower systemic hormone levels\n- Cons: Requires comfort with insertion and removal; possible vaginal discomfort\n- Side effects: Risk of blood clots, mood changes, headaches, vaginal irritation\n\nLess Effective Methods (failure rate 10% or higher)\n-Barrier Methods\n- Male Condoms\n - Prevention rate: ~87%\n - Pros: Also protect against STIs; non-hormonal; widely available\n - Cons: Effectiveness depends on correct use; may break or slip\n - Side effects: Possible latex allergy\n- Female Condoms\n - Prevention rate: ~79%\n - Pros: Offer STI protection; female-controlled\n - Cons: More expensive; less available; may be harder to use\n - Side effects: Possible irritation or allergic reaction\n- Diaphragms and Cervical Caps\n - Prevention rate: ~83–88%\n - Pros: Reusable; non-hormonal\n - Cons: Must be used with spermicide; requires proper fitting and timing\n - Side effects: Potential irritation, allergic reactions, increased risk of UTIs\n\n-Spermicides (used alone)\n- Prevention rate: ~79%\n- Pros: Over-the-counter; can be combined with other methods\n- Cons: Lower effectiveness when used alone; requires application every time\n- Side effects: May cause irritation or allergic reactions\n\n-Fertility Awareness Methods\n- Prevention rate: ~76% (varies widely)\n- Pros: No hormones or devices; increases understanding of your cycle\n- Cons: Requires daily tracking and strict adherence; higher risk of unintended pregnancy if not followed precisely\n- Side effects: None medically\n\n-Withdrawal (Coitus Interruptus)\n- Prevention rate: ~78%\n- Pros: No cost; non-hormonal\n- Cons: Relies heavily on timing; risk from pre-ejaculate\n- Side effects: No direct side effects; may cause stress if it fails\n\nTreatments for Managing HIV:\n- **Integrase Strand Transfer Inhibitor (INSTI)–Based Regimens:** \n - Bictegravir/emtricitabine/tenofovir alafenamide (Biktarvy) \n - Dolutegravir/lamivudine/abacavir (Triumeq)\n\n- **Non-Nucleoside Reverse Transcriptase Inhibitor (NNRTI)–Based Regimen:** \n - Efavirenz/emtricitabine/tenofovir disoproxil fumarate (Atripla)\n\n- **Protease Inhibitor (PI)–Based Regimen:** \n - Darunavir (boosted with ritonavir or cobicistat) plus two NRTIs"
SYSTEM_PROMPT = "You are a medical professional. Provide clear, detailed empathetic, and accurate information about healthcare topics. **You must always use markdown and bullet points.** Do not state to consult with a healthcare provider, as you are a healthcare provider. Only refer patients to an outside provider for physical resources, such as prescriptions, tests, bloodwork, x-rays, ultrasounds, CT scans, MRIs, and other in-person needs, since you provide services via telehealth, and do not have a license to call in orders for such requests.\n\n" # + medical_recommendations
# UI text and styling
DESCRIPTION = '''
<div>
<div style="display: flex; align-items: center;">
<img src="https://aihealthcare.gay/icon96.png" alt="HealthAssistant Logo" width="96" height="96">
<h1 style="margin-left: 20px;">HealthAssistant</h1>
</div>
'''
LICENSE = """
<p>
Disclaimer: This Health Assistant is designed to provide helpful healthcare information; however, it may contain errors and is not intended to replace professional medical care. It does not diagnose any condition or disease. Always consult with a qualified healthcare provider for any medical concerns. Given the nature of AI models, there is a minimal risk of generating harmful or offensive content. Please exercise caution and use common sense.
User Acknowledgment: I hereby confirm that I am at least 18 years of age (or accompanied by a legal guardian who is at least 18 years old), understand that the information provided by this service is for informational purposes only and is not intended to diagnose or treat any medical condition, and acknowledge that I am solely responsible for verifying any information provided.</p>
"""
PLACEHOLDER = """
<div style="padding: 30px; text-align: center; display: flex; flex-direction: column; align-items: center;">
<h1 style="font-size: 28px; margin-bottom: 2px; opacity: 0.55;">The "Doctor" is in.</h1>
<p style="font-size: 18px; margin-bottom: 2px; opacity: 0.65;">Available for free. Always verify responses with outside information.</p>
</div>
"""
css = """
h1 {
text-align: center;
display: block;
}
#duplicate-button {
margin: auto;
color: white;
background: #1565c0;
border-radius: 100vh;
}
"""
# List of (phrase, replacement) pairs.
replacements = [
("a healthcare provider", "me or a healthcare provider"),
("a healthcare professional", "me or a healthcare professional"),
("a doctor", "me or a doctor")
# Add more pairs as needed.
]
# Calculate the maximum length of any phrase.
max_phrase_length = max(len(phrase) for phrase, _ in replacements)
MIN_FLUSH_SIZE = max(50, max_phrase_length * 2)
def think(request):
url = "https://openrouter.ai/api/v1/chat/completions"
headers = {
"Authorization": f"Bearer {openai_api_key}",
"Content-Type": "application/json"
}
def do_req(model, content, include_reasoning=False, reasoning=""):
messages = content
if messages[-1]["role"] == "user":
messages[-1]["content"] += " Please think this through, but don't output an answer."
payload = {
"model": model,
"messages": messages,
"include_reasoning": include_reasoning
}
return requests.post(url, headers=headers, data=json.dumps(payload))
# R1 will reliably return "done" for the content portion of the response
reasoning_response = do_req(reasoning_ai_model, request, True)
reasoning = reasoning_response.json()['choices'][0]['message']['reasoning']
return reasoning
def apply_replacements(text):
"""
Replace all specified phrases in the text.
"""
for phrase, replacement in replacements:
text = text.replace(phrase, replacement)
return text
def chat_with_openai(message: str, history: list, temperature: float, max_new_tokens: int, fast_mode: bool = False):
"""
Call the OpenAI ChatCompletion endpoint using the new client and yield streaming responses.
Implements <think> logic and retries if the full response is blank.
Args:
message (str): The latest user message.
history (list): Conversation history as a list of (user, assistant) tuples.
temperature (float): Sampling temperature.
max_new_tokens (int): Maximum tokens to generate.
Yields:
str: Partial cumulative output from the assistant.
"""
conversation = []
if (not history and message.startswith("Start a talk therapy session with me.")) or \
any(user_msg.startswith("Start a talk therapy session with me.") for user_msg, _ in history):
fast_mode = True
if not history:
# Initialize with system prompt and assistant confirmation.
conversation.append({"role": "system", "content": SYSTEM_PROMPT})
conversation.append({"role": "assistant", "content": "Understood! I will act as the user's healthcare provider..."})
for user_msg, assistant_msg in history:
conversation.append({"role": "user", "content": user_msg})
conversation.append({"role": "assistant", "content": assistant_msg})
conversation.append({"role": "user", "content": message})
if not fast_mode:
# Indicate that the assistant is thinking.
yield "HealthAssistant is Thinking! Please wait, your response will output shortly. This may take 10-30 seconds...\n\n"
think_result = think(conversation)
conversation.append({"role": "assistant", "content": "<think>\n" + think_result + "\n</think> I will now respond to the user's message:\n\n"})
else:
yield "HealthAssistant is Thinking! Please wait, your response will output shortly...\n\n"
attempt = 0
response = None
while attempt < 5:
if attempt == 4 and not fast_mode:
del conversation[-1]
attempt += 1
response = client.chat.completions.create(
model=ai_model,
messages=conversation,
temperature=temperature,
max_tokens=max_new_tokens,
stream=True,
)
# Initialize buffers and state flags.
buffer = ""
pending_buffer = ""
display_text = ""
think_detected = False
full_response = ""
# Process streaming responses.
for chunk in response:
delta = chunk.choices[0].delta
token_text = delta.content or ""
full_response += token_text
# Handle buffering of tokens as in previous logic.
pending_buffer += token_text
if len(pending_buffer) >= MIN_FLUSH_SIZE:
safe_portion = pending_buffer[:-max_phrase_length] if len(pending_buffer) > max_phrase_length else ""
if safe_portion:
display_text += apply_replacements(safe_portion)
yield display_text
pending_buffer = pending_buffer[-max_phrase_length:]
# Flush remaining text.
if pending_buffer:
safe_portion = pending_buffer
display_text += apply_replacements(safe_portion)
yield display_text
# Check if the full response is valid.
if full_response.strip():
break # Exit the loop if the response is not blank.
# If no valid response was generated after 5 attempts
if not full_response.strip():
yield "*The assistant did not provide a response. Please try again.*"
else:
# Apply replacements and append modified response to history.
modified_full_response = apply_replacements(full_response)
history.append((message, modified_full_response))
def export_chat(history):
"""Export chat history as a JSONL file in OpenAI messages format."""
if not history:
return None
messages = []
# Add a blank system message (to maintain format compatibility, but without revealing the actual system prompt)
messages.append({"role": "system", "content": ""})
# Convert history to messages format
for user_msg, assistant_msg in history:
messages.append({"role": "user", "content": user_msg})
messages.append({"role": "assistant", "content": assistant_msg})
# Convert to JSONL format (each line is a JSON object)
jsonl_content = "\n".join([json.dumps(msg) for msg in messages])
# Create a temporary file for download
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".jsonl", mode="w", encoding="utf-8")
temp_file.write(jsonl_content)
temp_file.close()
return temp_file.name # Return the file path
def import_chat(file):
"""Import chat history from a JSONL file."""
try:
# Handle both file-like objects and NamedString objects
if hasattr(file, 'name'): # It's a NamedString from gr.UploadButton
with open(file.name, 'r', encoding='utf-8') as f:
content = f.read()
else: # It's a file-like object
content = file.read()
lines = [line.strip() for line in content.split("\n") if line.strip()]
messages = [json.loads(line) for line in lines]
new_history = []
i = 0
# Skip system message if it's the first one (we don't use imported system prompts)
if messages and messages[0]["role"] == "system":
i = 1
while i < len(messages) - 1:
if messages[i]["role"] == "user" and messages[i+1]["role"] == "assistant":
new_history.append((messages[i]["content"], messages[i+1]["content"]))
i += 2
else:
i += 1
return gr.update(value=new_history) # Update the chatbot state
except Exception as e:
raise gr.Error(f"Error importing chat: {str(e)}")
# Create the Chatbot component.
chatbot = gr.Chatbot(height=450, placeholder=PLACEHOLDER, label='HealthAssistant')
# Build the Gradio interface.
with gr.Blocks(css=css) as demo:
gr.HTML(DESCRIPTION)
# Add the checkbox directly to the layout
fast_mode_checkbox = gr.Checkbox(label="Fast Mode (Skips Reasoning. Provides Immediate, Less Accurate Responses.) RECOMMENDED FOR TALK THERAPY.", value=False)
chat_interface = gr.ChatInterface(
fn=chat_with_openai,
chatbot=chatbot,
fill_height=True,
additional_inputs_accordion=gr.Accordion(label="Settings", open=False, render=False, visible=False),
additional_inputs=[
gr.Slider(minimum=0.6, maximum=0.6, step=0.1, value=0.6, label="Temperature", render=False, visible=False),
gr.Slider(minimum=1024, maximum=4096, step=128, value=2048, label="Max new tokens", render=False, visible=False),
fast_mode_checkbox,
],
examples=[
['What is PrEP, and how do I know if I need it?'],
['What medications help manage being undetectable with HIV?'],
['Start a talk therapy session with me. Begin by asking me what I would like to talk about.'],
['How can I access birth-control in states where it is regulated?'],
],
cache_examples=False,
)
# Add export and import buttons
with gr.Row():
export_btn = gr.Button("Export Chat")
import_btn = gr.UploadButton("Import Chat", file_types=[".jsonl"], visible=False)
# Connect buttons to functions
export_btn.click(fn=export_chat, inputs=chatbot, outputs=gr.File(label="Download Chat History"))
import_btn.upload(fn=import_chat, inputs=[import_btn], outputs=chatbot) # Fixed connection
gr.Markdown(LICENSE)
if __name__ == "__main__":
demo.launch(share=True) |