File size: 14,258 Bytes
d2c3421
38bd2d4
 
aa90007
 
 
 
 
 
5f89d23
38bd2d4
aa90007
5f89d23
fe29472
f2d2182
94e995e
7e21141
f2d2182
9a97411
 
d0d6d4d
 
 
9a97411
 
7e21141
 
 
001d1ef
ab35161
7e21141
 
9a97411
 
a49f925
3cd1e30
9a97411
 
7e21141
9a97411
 
 
 
 
 
 
 
 
 
 
 
 
 
d1594b3
 
7b70825
 
 
d1594b3
 
 
 
 
27f7c68
cd60f6d
27f7c68
aa90007
 
816374b
 
aa90007
 
816374b
aa90007
816374b
 
aa90007
816374b
 
 
 
aa90007
816374b
 
 
 
 
 
 
 
 
aa90007
816374b
aa90007
 
816374b
 
 
 
aa90007
d1594b3
 
 
 
 
 
 
 
 
9955a0a
9a97411
38bd2d4
 
 
 
 
 
f03ab9c
38bd2d4
 
 
 
f03ab9c
38bd2d4
 
 
9a97411
5f89d23
38bd2d4
5f89d23
425a8a5
38bd2d4
 
 
9a97411
f98d1cf
9955a0a
 
 
f98d1cf
9955a0a
aa90007
9955a0a
 
 
 
aa90007
38bd2d4
 
aa90007
38bd2d4
d2c3421
38bd2d4
 
d2c3421
f98d1cf
40c24da
 
 
 
d1594b3
40c24da
34d7543
38bd2d4
 
40c24da
38bd2d4
 
 
d1594b3
aa90007
 
 
 
 
 
 
 
 
d1594b3
02fa10d
d1594b3
02fa10d
 
40c24da
d1594b3
02fa10d
40c24da
d1594b3
 
 
f98d1cf
38bd2d4
a49f925
f98d1cf
38bd2d4
 
dad7c88
c6e3c53
 
 
 
f98d1cf
38bd2d4
f98d1cf
 
9a97411
c6e3c53
899134d
 
f804d88
f03ab9c
1a187b5
3cd1e30
6c824be
3cd1e30
1ab91ee
fd36e75
7fd2cf6
c6e3c53
9a97411
f804d88
9a97411
7e21141
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
import os
import gradio as gr
from openai import OpenAI
import json
import requests

openai_api_key = os.getenv("OPENROUTER_API_KEY")
openai_base_url = os.getenv("OPENAI_BASE_URL")
ai_model = os.getenv("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."
      
        #messages = [{"role": "user", "content": content}]

        #if reasoning != "":
        #    messages.append({"role": "assistant", "content": "<think>\n" + reasoning + "</think>\n"})
        print(messages)
        payload = {
            "model": model,
            "messages": messages,
            "include_reasoning": include_reasoning,
            "stop": "</think>"
        }

        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("deepseek/deepseek-r1", 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=false):
    """
    Call the OpenAI ChatCompletion endpoint using the new client and yield streaming responses.
    Implements <think> logic:
      - The assistant is forced to begin its answer with "<think> ".
      - We then wait until a closing "</think>" marker is received.
      - Only text after "</think>" is displayed as the final answer.
    
    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:
        # Add a system prompt and initial assistant confirmation.
        conversation.append({"role": "system", "content": SYSTEM_PROMPT})
        conversation.append({"role": "assistant", "content": "Understood! I will act as the user's healthcare provider, not hesitate with providing detailed responses, or refer them to another healthcare provider unless they ask for one or need a prescription, test, or resource unavailable via telehealth. I will provide detailed responses with markdown in the form of bullet point lists."})
    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:
        # Immediately yield a "thinking" status message.
        yield "HealthAssistant is Thinking! Please wait, your response will output shortly... This may take 30-60 seconds...\n\n"

        think_result = think(conversation)
    
        # Force the model to begin its answer with a "<think>" block.
        conversation.append({"role": "assistant", "content": "<think>\n"+think_result+"\n</think>"})
    else:
        yield "HealthAssistant is Thinking! Please wait, your response will output shortly...\n\n"
    
    # Call the API with streaming enabled.
    response = client.chat.completions.create(
        model=ai_model,  # Replace with your actual model identifier.
        messages=conversation,
        temperature=temperature,
        max_tokens=max_new_tokens,
        stream=True,
    )

    # Initialize buffers and state flags.
    buffer = ""           # Accumulates tokens until the </think> marker is found.
    pending_buffer = ""   # Holds the tail end of text that may contain a partial phrase.
    display_text = ""     # Cumulative text that has been finalized and yielded.
    think_detected = False
    full_response = ""    # Accumulates the full raw response (without replacements applied).
    
    # Process streaming responses.
    for chunk in response:
        # Extract the new token text from the current chunk.
        delta = chunk.choices[0].delta
        token_text = delta.content or ""
        full_response += token_text
    
        
        # After the </think> marker, add tokens to pending_buffer.
        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:]
    
    # After processing all tokens, flush any remaining text.
    if pending_buffer:
        safe_portion = pending_buffer  # flush whatever remains
        display_text += apply_replacements(safe_portion)
        yield display_text
    
    # Append the full (raw) response, including the <think> section, to the conversation history.
    # If you want the history to reflect the replacements, apply them here.
    modified_full_response = apply_replacements(full_response)
    history.append((message, modified_full_response))


# 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 Responses)", value=False)

    gr.ChatInterface(
        fn=chat_with_openai,
        chatbot=chatbot,
        fill_height=True,
        additional_inputs=[
            fast_mode_checkbox,  # Use the checkbox directly here
            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),
        ],
        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,
    )

    gr.Markdown(LICENSE)

if __name__ == "__main__":
    demo.launch()