KevinIsInCoding commited on
Commit
2fd08bb
·
1 Parent(s): 6977736

To support Gradio Visualization and HuggingFace host

Browse files
Files changed (5) hide show
  1. LICENSE +21 -0
  2. app.py +225 -0
  3. pyproject.toml +1 -0
  4. requirements.txt +7 -0
  5. uv.lock +0 -0
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KevinIsInCoding
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
app.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import json
5
+ from typing import Generator
6
+
7
+ import anthropic
8
+ import gradio as gr
9
+ from dotenv import load_dotenv
10
+
11
+ from clinical_trials_guru import (
12
+ INTAKE_MODEL,
13
+ INTAKE_SYSTEM,
14
+ RESEARCH_MODEL,
15
+ RESEARCH_SYSTEM,
16
+ SEARCH_TRIALS_TOOL,
17
+ SUBMIT_PROFILE_TOOL,
18
+ PatientProfile,
19
+ _flatten_and_rank,
20
+ geocode_zip,
21
+ search_trials_api,
22
+ )
23
+
24
+ load_dotenv()
25
+
26
+
27
+ def _intake_turn(
28
+ user_text: str, messages: list
29
+ ) -> tuple[str, list, PatientProfile | None]:
30
+ today = datetime.date.today().strftime("%B %d, %Y")
31
+ messages = messages + [{"role": "user", "content": user_text}]
32
+ client = anthropic.Anthropic()
33
+ response = client.messages.create(
34
+ model=INTAKE_MODEL,
35
+ max_tokens=1024,
36
+ system=f"Today's date is {today}.\n\n" + INTAKE_SYSTEM,
37
+ tools=[SUBMIT_PROFILE_TOOL],
38
+ messages=messages,
39
+ )
40
+ text = next((b.text for b in response.content if b.type == "text"), "")
41
+ tool_block = next(
42
+ (b for b in response.content if b.type == "tool_use" and b.name == "submit_profile"),
43
+ None,
44
+ )
45
+ messages = messages + [{"role": "assistant", "content": response.content}]
46
+
47
+ if tool_block:
48
+ data = tool_block.input
49
+ try:
50
+ lat, lon = geocode_zip(data["zip_code"], data.get("country_code", "US"))
51
+ except Exception:
52
+ lat, lon = 0.0, 0.0
53
+ profile = PatientProfile(
54
+ disease=data["disease"],
55
+ age=data["age"],
56
+ onset_months=data["onset_months"],
57
+ benchmarks=data.get("benchmarks") or {},
58
+ zip_code=data["zip_code"],
59
+ country_code=data.get("country_code", "US"),
60
+ lat=lat,
61
+ lon=lon,
62
+ radius_miles=data.get("radius_miles", 100),
63
+ phases=data.get("phases") or [],
64
+ )
65
+ return text or "Got it — searching for trials now…", messages, profile
66
+
67
+ return text, messages, None
68
+
69
+
70
+ def _run_research(profile: PatientProfile) -> str:
71
+ client = anthropic.Anthropic()
72
+ messages: list[anthropic.types.MessageParam] = [
73
+ {
74
+ "role": "user",
75
+ "content": (
76
+ f"Find clinical trials for this patient:\n\n{profile.summary()}\n\n"
77
+ "Search within the specified radius and rank results by distance."
78
+ ),
79
+ }
80
+ ]
81
+ while True:
82
+ response = client.messages.create(
83
+ model=RESEARCH_MODEL,
84
+ max_tokens=8096,
85
+ system=RESEARCH_SYSTEM,
86
+ tools=[SEARCH_TRIALS_TOOL],
87
+ messages=messages,
88
+ )
89
+ messages.append({"role": "assistant", "content": response.content})
90
+ if response.stop_reason == "end_turn":
91
+ return next(
92
+ (b.text for b in response.content if b.type == "text"),
93
+ "No analysis produced.",
94
+ )
95
+ tool_results: list[anthropic.types.ToolResultBlockParam] = []
96
+ for block in response.content:
97
+ if block.type != "tool_use" or block.name != "search_clinical_trials":
98
+ continue
99
+ args = block.input
100
+ try:
101
+ studies = search_trials_api(
102
+ condition=args["condition"],
103
+ lat=args["lat"],
104
+ lon=args["lon"],
105
+ radius_miles=args.get("radius_miles", profile.radius_miles),
106
+ phases=args.get("phases") or None,
107
+ max_results=args.get("max_results", 20),
108
+ )
109
+ ranked = _flatten_and_rank(studies, profile.lat, profile.lon)
110
+ content = json.dumps(ranked)
111
+ is_error = False
112
+ except Exception as exc:
113
+ content = f"API request failed: {exc}"
114
+ is_error = True
115
+ tool_results.append(
116
+ {
117
+ "type": "tool_result",
118
+ "tool_use_id": block.id,
119
+ "content": content,
120
+ "is_error": is_error,
121
+ }
122
+ )
123
+ messages.append({"role": "user", "content": tool_results})
124
+
125
+
126
+ def initialize():
127
+ text, msgs, _ = _intake_turn("Please begin.", [])
128
+ chat = [{"role": "assistant", "content": text}]
129
+ # Strip the seed "Please begin." turn so subsequent user messages append cleanly.
130
+ # msgs already includes both the seed user turn and the assistant turn; keep it.
131
+ return chat, msgs, None, "intake"
132
+
133
+
134
+ def respond(
135
+ user_msg: str,
136
+ chat_history: list,
137
+ intake_msgs: list,
138
+ profile,
139
+ phase: str,
140
+ ) -> Generator:
141
+ if not user_msg.strip() or phase == "done":
142
+ yield chat_history, intake_msgs, profile, phase, gr.update(), gr.update()
143
+ return
144
+
145
+ chat_history = chat_history + [{"role": "user", "content": user_msg}]
146
+ yield chat_history, intake_msgs, profile, phase, gr.update(value=""), gr.update()
147
+
148
+ assistant_text, updated_msgs, new_profile = _intake_turn(user_msg, intake_msgs)
149
+
150
+ if new_profile:
151
+ status = (assistant_text + "\n\n" if assistant_text else "") + (
152
+ "*Searching ClinicalTrials.gov — this may take a minute…*"
153
+ )
154
+ chat_history = chat_history + [{"role": "assistant", "content": status}]
155
+ yield (
156
+ chat_history,
157
+ updated_msgs,
158
+ new_profile,
159
+ "researching",
160
+ gr.update(interactive=False, placeholder="Searching…"),
161
+ gr.update(visible=False),
162
+ )
163
+
164
+ analysis = _run_research(new_profile)
165
+ chat_history = chat_history + [{"role": "assistant", "content": analysis}]
166
+ yield (
167
+ chat_history,
168
+ updated_msgs,
169
+ new_profile,
170
+ "done",
171
+ gr.update(interactive=False, placeholder="Search complete."),
172
+ gr.update(visible=True),
173
+ )
174
+ else:
175
+ chat_history = chat_history + [{"role": "assistant", "content": assistant_text}]
176
+ yield (
177
+ chat_history,
178
+ updated_msgs,
179
+ profile,
180
+ "intake",
181
+ gr.update(),
182
+ gr.update(),
183
+ )
184
+
185
+
186
+ with gr.Blocks(title="Beacon — Clinical Trial Finder") as demo:
187
+ gr.Markdown("# 🔦 Beacon — Rare Disease Clinical Trial Finder")
188
+
189
+ chatbot = gr.Chatbot(height=550, show_label=False)
190
+ with gr.Row():
191
+ msg_box = gr.Textbox(
192
+ placeholder="Type your message and press Enter…",
193
+ show_label=False,
194
+ scale=9,
195
+ autofocus=True,
196
+ )
197
+ send_btn = gr.Button("Send", scale=1, variant="primary")
198
+ new_search_btn = gr.Button("New Search", visible=False, variant="secondary")
199
+
200
+ # State
201
+ intake_msgs_state = gr.State([])
202
+ profile_state = gr.State(None)
203
+ phase_state = gr.State("intake")
204
+
205
+ outputs = [chatbot, intake_msgs_state, profile_state, phase_state, msg_box, new_search_btn]
206
+
207
+ demo.load(
208
+ initialize,
209
+ outputs=[chatbot, intake_msgs_state, profile_state, phase_state],
210
+ )
211
+
212
+ msg_box.submit(respond, [msg_box, chatbot, intake_msgs_state, profile_state, phase_state], outputs)
213
+ send_btn.click(respond, [msg_box, chatbot, intake_msgs_state, profile_state, phase_state], outputs)
214
+
215
+ new_search_btn.click(
216
+ initialize,
217
+ outputs=[chatbot, intake_msgs_state, profile_state, phase_state],
218
+ ).then(
219
+ lambda: (gr.update(interactive=True, placeholder="Type your message and press Enter…"), gr.update(visible=False)),
220
+ outputs=[msg_box, new_search_btn],
221
+ )
222
+
223
+
224
+ if __name__ == "__main__":
225
+ demo.launch()
pyproject.toml CHANGED
@@ -10,4 +10,5 @@ dependencies = [
10
  "openai>=1.0.0",
11
  "python-dotenv>=1.2.2",
12
  "rich>=13.0.0",
 
13
  ]
 
10
  "openai>=1.0.0",
11
  "python-dotenv>=1.2.2",
12
  "rich>=13.0.0",
13
+ "gradio>=4.0.0",
14
  ]
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ anthropic>=0.50.0
2
+ gradio>=4.0.0
3
+ langgraph>=1.2.0
4
+ httpx
5
+ python-dotenv
6
+ openai>=1.0.0
7
+ rich>=13.0.0
uv.lock CHANGED
The diff for this file is too large to render. See raw diff