Mahbodez commited on
Commit
07d7ee6
1 Parent(s): 2f91a49

Upload 5 files

Browse files
Files changed (5) hide show
  1. interface.py +431 -0
  2. knee_template.json +353 -0
  3. log.txt +0 -0
  4. logger.py +13 -0
  5. report_ckecklist_gradio.py +85 -0
interface.py ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import numpy as np
3
+ import treegraph as tg
4
+ import colorama
5
+ from colorama import Fore
6
+ import networkx as nx
7
+ import utils
8
+ import re
9
+ import logger as lg
10
+
11
+ DEBUG = True
12
+ INPUT_COLOR = Fore.LIGHTGREEN_EX
13
+ DEBUG_COLOR = Fore.LIGHTBLACK_EX
14
+ OUTPUT_COLOR = Fore.LIGHTMAGENTA_EX
15
+ INFO_COLOR = Fore.BLUE
16
+ HELP_COLOR = Fore.CYAN
17
+
18
+
19
+ def print_debug(*args, color=DEBUG_COLOR):
20
+ """
21
+ Prints debug messages if DEBUG is set to True.
22
+ """
23
+ if DEBUG:
24
+ for arg in args:
25
+ print(color + str(arg))
26
+
27
+
28
+ class ReportInterface:
29
+ def __init__(
30
+ self,
31
+ llm: utils.LLM,
32
+ system_prompt: str,
33
+ tree_graph: nx.Graph,
34
+ nodes_dict: dict[str, tg.Node],
35
+ api_key: str = None,
36
+ ):
37
+ self.llm = llm
38
+ self.system_prompt = system_prompt
39
+ self.tree_graph = tree_graph
40
+ self.nodes_dict = nodes_dict
41
+ self.api_key = api_key
42
+ self.build()
43
+
44
+ def build(self):
45
+ utils.set_api_key(self.api_key)
46
+ self.system_prompt = utils.make_message("system", self.system_prompt)
47
+ self.visitable_nodes = self._get_visitable_nodes()
48
+ self.report_dict = self._get_report_dict()
49
+
50
+ self.active_node: tg.Node = self.nodes_dict["root"]
51
+ self.unique_visited_nodes = set() # set of nodes visited
52
+ self.node_journey = [] # list of nodes visited
53
+ self.distance_travelled = 0 # number of edges travelled
54
+ self.jumps = 0 # number of jumps
55
+ self.jump_lengths = [] # list of jump lengths
56
+ self.counter = 0 # number of questions asked
57
+
58
+ colorama.init(autoreset=True) # to reset the color after each print statement
59
+
60
+ self.help_message = f"""You are presented with a Knee MRI.
61
+ You are asked to fill out a radiology report.
62
+ Please only report the findings in the MRI.
63
+ Please mention your findings with the corresponding anatomical structures.
64
+ There are {len(self.visitable_nodes.keys())} visitable nodes in the tree.
65
+ You must visit as many nodes as possible, while avoiding too many jumps."""
66
+
67
+ def _get_visitable_nodes(self):
68
+ return dict(
69
+ zip(
70
+ [
71
+ node.name
72
+ for node in self.tree_graph.nodes
73
+ if node.name != "root" and node.has_children() is False
74
+ ],
75
+ [
76
+ node
77
+ for node in self.tree_graph.nodes
78
+ if node.name != "root" and node.has_children() is False
79
+ ],
80
+ )
81
+ )
82
+
83
+ def _get_report_dict(self):
84
+ return {
85
+ node.name: tg.Node(node.name, "", node.children)
86
+ for node in self.visitable_nodes.values()
87
+ }
88
+
89
+ @utils.debug(DEBUG, print_debug)
90
+ def _check_question_validity(
91
+ self,
92
+ question: str,
93
+ ):
94
+ # let's ask the question from the model and check if it's valid
95
+ template_json = json.dumps(
96
+ {key: node.value for key, node in self.visitable_nodes.items()},
97
+ indent=4,
98
+ )
99
+ q = f"""the following is a Knee MRI report "template" in a JSON format with keys and values.
100
+ You are given a "finding" phrase from a radiologist.
101
+ Match as best as possible the "finding" with one of keys in the "template".
102
+ <template>
103
+ {template_json}
104
+ </template>
105
+ <finding>
106
+ {question}
107
+ </finding>
108
+ "available": [Is the "finding" relevant to any key in the "template"? say "yes" or "no".
109
+ Make sure the "finding" is relevant to Knee MRI and knee anatomy otherwise say 'no'.
110
+ Do not answer irrelevant phrases.]
111
+ "node": [if the above answer is 'yes', write only the KEY of the most relevant node to the "finding". otherwise, say 'none'.]
112
+ """
113
+
114
+ keys = ["available", "node"]
115
+ prompt = [self.system_prompt] + [
116
+ utils.make_question(utils.JSON_TEMPLATE, question=q, keys=keys)
117
+ ]
118
+ response = self.llm(prompt)
119
+ print_debug(
120
+ prompt,
121
+ response,
122
+ )
123
+ available = utils.json2dict(response)["available"].strip().lower()
124
+ node = utils.json2dict(response)["node"]
125
+ return available, node
126
+
127
+ def _update_node(self, node_name, findings):
128
+ self.report_dict[node_name].value += str(findings) + "\n"
129
+ response = f"Updated node '{node_name}' with finding '{findings}'"
130
+ print(OUTPUT_COLOR + response)
131
+ return response
132
+
133
+ def save_report(self, filename: str):
134
+ # convert performance metrics to json
135
+ metrics = {
136
+ "distance_travelled": self.distance_travelled,
137
+ "jumps": self.jumps,
138
+ "jump_lengths": self.jump_lengths,
139
+ "unique_visited_nodes": [node.name for node in self.unique_visited_nodes],
140
+ "node_journey": [node.name for node in self.node_journey],
141
+ "report": {
142
+ node_name: node.value for node_name, node in self.report_dict.items()
143
+ },
144
+ }
145
+ # save the report
146
+ with open(filename, "w") as file:
147
+ json.dump(metrics, file, indent=4)
148
+
149
+ def prime_model(self):
150
+ """
151
+ Primes the model with the system prompt.
152
+ """
153
+ q = "Are you ready to begin?\nSay 'yes' or 'no'."
154
+ keys = ["answer"]
155
+ response = self.llm(
156
+ [
157
+ self.system_prompt,
158
+ utils.make_question(utils.JSON_TEMPLATE, question=q, keys=keys),
159
+ ],
160
+ )
161
+ print_debug(q, response)
162
+ if utils.json2dict(response)["answer"].lower() == "yes":
163
+ print(INFO_COLOR + "The model is ready.")
164
+ return True
165
+ else:
166
+ print(INFO_COLOR + "The model is not ready.")
167
+ return False
168
+
169
+ def performance_summary(self):
170
+ # print out the summary info
171
+ print(INFO_COLOR + "Performance Summary:")
172
+ print(
173
+ INFO_COLOR + f"Total distance travelled: {self.distance_travelled} edge(s)"
174
+ )
175
+ print(INFO_COLOR + f"Jump lengths: {self.jump_lengths}")
176
+ print(INFO_COLOR + f"Jump lengths mean: {np.mean(self.jump_lengths):.1f}")
177
+ print(INFO_COLOR + f"Jump lengths SD: {np.std(self.jump_lengths):.1f}")
178
+ print(INFO_COLOR + f"Nodes visited in order: {self.node_journey}")
179
+ print(INFO_COLOR + f"Unique nodes visited: {self.unique_visited_nodes}")
180
+ print(
181
+ INFO_COLOR
182
+ + f"You have explored {len(self.unique_visited_nodes)/len(self.visitable_nodes):.1%} ({len(self.unique_visited_nodes)}/{len(self.visitable_nodes)}) of the tree."
183
+ )
184
+ print_debug("\n")
185
+ print_debug("Report Summary:".rjust(20))
186
+ for name, node in self.report_dict.items():
187
+ if node.value != "":
188
+ print_debug(f"{name}: {node.value}")
189
+ print(INFO_COLOR + f"total cost: ${self.llm.cost:.4f}")
190
+ print(INFO_COLOR + f"total tokens used: {self.llm.token_counter}")
191
+
192
+ def get_stats(self):
193
+ report_string = ""
194
+ for name, node in self.report_dict.items():
195
+ if node.value != "":
196
+ report_string += f"{name}: <{node.value}> \n"
197
+ return {
198
+ "Lengths travelled": self.distance_travelled,
199
+ "Number of jumps": self.jumps,
200
+ "Jump lengths": self.jump_lengths,
201
+ "Unique nodes visited": [node.name for node in self.unique_visited_nodes],
202
+ "Visited Nodes": [node.name for node in self.node_journey],
203
+ "Report": report_string,
204
+ }
205
+
206
+ def visualize_tree(self, **kwargs):
207
+ tg.visualize_graph(tg.from_list(self.node_journey), self.tree_graph, **kwargs)
208
+
209
+ def get_plot(self, **kwargs):
210
+ return tg.get_graph(tg.from_list(self.node_journey), self.tree_graph, **kwargs)
211
+
212
+ def process_input(self, input_text: str):
213
+ res = "n/a"
214
+ try:
215
+ finding = input_text
216
+ if finding.strip().lower() == "quit":
217
+ print(INFO_COLOR + "Exiting...")
218
+ return "quit"
219
+ elif finding.strip().lower() == "help":
220
+ return "help"
221
+
222
+ available, node = self._check_question_validity(finding)
223
+ if available != "yes":
224
+ print(
225
+ OUTPUT_COLOR
226
+ + "Could not find a relevant node.\nWrite more clearly and provide more details."
227
+ )
228
+ return "n/a"
229
+ if node not in self.visitable_nodes.keys():
230
+ print(
231
+ OUTPUT_COLOR
232
+ + "Could not find a relevant node.\nWrite more clearly and provide more details."
233
+ )
234
+ return "n/a"
235
+ else:
236
+ # modify the tree to update the node with findings
237
+ res = self._update_node(node, finding)
238
+
239
+ print(
240
+ INFO_COLOR
241
+ + f"jumping from node '{self.active_node}' to node '{node}'..."
242
+ )
243
+ distance = tg.num_edges_between_nodes(
244
+ self.tree_graph, self.active_node, self.nodes_dict[node]
245
+ )
246
+ print(INFO_COLOR + f"distance travelled: {distance} edge(s)")
247
+
248
+ self.active_node = self.nodes_dict[node]
249
+ self.jumps += 1
250
+ self.jump_lengths.append(distance)
251
+ self.distance_travelled += distance
252
+ if self.active_node.name != "root":
253
+ self.unique_visited_nodes.add(self.active_node)
254
+ self.node_journey.append(self.active_node)
255
+ except Exception as ex:
256
+ print_debug(ex, color=Fore.LIGHTRED_EX)
257
+ return "exception"
258
+
259
+ self.counter += 1
260
+ try:
261
+ self.performance_summary()
262
+ except Exception as ex:
263
+ print_debug(ex, color=Fore.LIGHTRED_EX)
264
+ return res
265
+
266
+
267
+ class ReportChecklistInterface:
268
+ def __init__(
269
+ self,
270
+ llm: utils.LLM,
271
+ system_prompt: str,
272
+ graph: nx.Graph,
273
+ nodes_dict: dict[str, tg.Node],
274
+ api_key: str = None,
275
+ logger: lg.Logger = None,
276
+ username: str = None,
277
+ ):
278
+ self.llm = llm
279
+ self.system_prompt = system_prompt
280
+ self.tree_graph: nx.Graph = graph
281
+ self.nodes_dict = nodes_dict
282
+ self.api_key = api_key
283
+ self.logger = logger
284
+ self.username = username
285
+ self.build()
286
+
287
+ def build(self):
288
+ utils.set_api_key(self.api_key)
289
+ self.system_prompt = utils.make_message("system", self.system_prompt)
290
+ self.visitable_nodes = self._get_visitable_nodes()
291
+
292
+ colorama.init(autoreset=True) # to reset the color after each print statement
293
+
294
+ self.help_message = f"""You are presented with a Knee MRI.
295
+ You are asked to fill out a radiology report.
296
+ Please only report the findings in the MRI.
297
+ Please mention your findings with the corresponding anatomical structures.
298
+ There are {len(self.visitable_nodes.keys())} visitable nodes in the tree."""
299
+
300
+ def _get_visitable_nodes(self):
301
+ return dict(
302
+ zip(
303
+ [
304
+ node.name
305
+ for node in self.tree_graph.nodes
306
+ if node.name != "root" and node.has_children() is False
307
+ ],
308
+ [
309
+ node
310
+ for node in self.tree_graph.nodes
311
+ if node.name != "root" and node.has_children() is False
312
+ ],
313
+ )
314
+ )
315
+
316
+ @utils.debug(DEBUG, print_debug)
317
+ def _check_report(
318
+ self,
319
+ report: str,
320
+ ):
321
+ # let's ask the question from the model and check if it's valid
322
+ checklist_json = json.dumps(
323
+ {key: node.value for key, node in self.visitable_nodes.items()},
324
+ indent=4,
325
+ )
326
+ q = f"""the following is a Knee MRI "checklist" in JSON format with keys as items and values as findings:
327
+ A knee MRI "report" is also provided in raw text format written by a radiologist:
328
+ <checklist>
329
+ {checklist_json}
330
+ </checklist>
331
+ <report>
332
+ {report}
333
+ </report>
334
+ Your task is to find all the corresponding items from the "checklist" in the "report" and fill out a JSON with the same keys as the "checklist" but extract the corresponding values from the "report".
335
+ If a key is not found in the "report", please set the value to "n/a", otherwise set it to the corresponding finding from the "report".
336
+ You must check the "report" phrases one by one and find a corresponding key(s) for EACH phrase in the "report" from the "checklist" and fill out the "report_checked" JSON.
337
+ Try to fill out as many items as possible.
338
+ ALL of the items in the "checklist" must be filled out.
339
+ Don't generate findings that are not present in the "report" (new findings).
340
+ Be comprehensive and don't miss any findings that are present in the "report".
341
+ Watch out for encompassing terms (e.g., "cruciate ligaments" means both "ACL" and "PCL").
342
+ "thought_process": [Think in steps on how you would do this task.]
343
+ "report_ckecked" : [a JSON with the same keys as the "checklist" but take the values from the "report", as described above.]
344
+ """
345
+
346
+ keys = ["thought_process", "report_checked"]
347
+ prompt = [self.system_prompt] + [
348
+ utils.make_question(utils.JSON_TEMPLATE, question=q, keys=keys)
349
+ ]
350
+ response = self.llm(prompt)
351
+ print_debug(
352
+ prompt,
353
+ response,
354
+ )
355
+ if self.logger:
356
+ # set name to class name
357
+ self.logger(
358
+ name=self.__class__.__name__,
359
+ message=f"prompt: {prompt}\nresponse: {response}",
360
+ )
361
+ report_checked = utils.json2dict(response)
362
+ return report_checked["report_checked"]
363
+
364
+ def prime_model(self):
365
+ """
366
+ Primes the model with the system prompt.
367
+ """
368
+ q = "Are you ready to begin?\nSay 'yes' or 'no'."
369
+ keys = ["answer"]
370
+ response = self.llm(
371
+ [
372
+ self.system_prompt,
373
+ utils.make_question(utils.JSON_TEMPLATE, question=q, keys=keys),
374
+ ],
375
+ )
376
+ print_debug(q, response)
377
+ if utils.json2dict(response)["answer"].lower() == "yes":
378
+ print(INFO_COLOR + "The model is ready.")
379
+ return True
380
+ else:
381
+ print(INFO_COLOR + "The model is not ready.")
382
+ return False
383
+
384
+ def process_input(self, input_text: str):
385
+ try:
386
+ report = input_text
387
+ if self.logger:
388
+ self.logger(self.username, f"report: {report}")
389
+
390
+ if report.strip().lower() == "quit":
391
+ print(INFO_COLOR + "Exiting...")
392
+ if self.logger:
393
+ self.logger(self.username, "Exiting...")
394
+ return "quit"
395
+ elif report.strip().lower() == "help":
396
+ if self.logger:
397
+ self.logger(self.username, "Help")
398
+ return "help"
399
+
400
+ checked_report: dict = self._check_report(report)
401
+ # make a string of the report
402
+ # replace true with [checkmark emoji] and false with [cross emoji]
403
+ report_string = ""
404
+ CHECKMARK = "\u2705"
405
+ CROSS = "\u274C"
406
+
407
+ # we need a regex to convert the camelCase keys to a readable format
408
+ def camel2readable(camel: str):
409
+ string = re.sub("([a-z])([A-Z])", r"\1 \2", camel)
410
+ # captialize every word
411
+ string = " ".join([word.capitalize() for word in string.split()])
412
+ return string
413
+
414
+ for key, value in checked_report.items():
415
+ if str(value).lower() == "n/a":
416
+ report_string += f"{camel2readable(key)}: {CROSS}\n"
417
+ else:
418
+ report_string += f"{camel2readable(key)}: <{value}> {CHECKMARK}\n"
419
+
420
+ portion_visited: float = report_string.count(CHECKMARK) / len(
421
+ checked_report.keys()
422
+ )
423
+ report_string += f"Portion of the checklist visited: {portion_visited:.1%}"
424
+ if self.logger:
425
+ self.logger(self.__class__.__name__, report_string)
426
+ return report_string
427
+ except Exception as ex:
428
+ print_debug(ex, color=Fore.LIGHTRED_EX)
429
+ if self.logger:
430
+ self.logger(self.__class__.__name__, "Exception: " + ex)
431
+ return "exception"
knee_template.json ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "root": {
3
+ "value": "root",
4
+ "parent": null,
5
+ "children": [
6
+ "kneeJointEffusion",
7
+ "kneeMeniscus",
8
+ "kneeAclPcl",
9
+ "kneeMcl",
10
+ "kneePosterolateralCorner",
11
+ "kneeExtensorMechanism",
12
+ "kneeCartilage",
13
+ "kneeBone",
14
+ "kneeOther"
15
+ ]
16
+ },
17
+ "kneeJointEffusion": {
18
+ "value": "Presence and/or extent of joint effusion.",
19
+ "parent": "root",
20
+ "children": []
21
+ },
22
+ "kneeMeniscus": {
23
+ "value": "",
24
+ "parent": "root",
25
+ "children": [
26
+ "kneeMeniscusMedialTearing",
27
+ "kneeMeniscusLateralTearing",
28
+ "kneeMeniscusWrisberg",
29
+ "kneeMeniscusRootTearing",
30
+ "kneeMeniscusRampLesion"
31
+ ]
32
+ },
33
+ "kneeMeniscusMedialTearing": {
34
+ "value": "Presence and/or severity of medial meniscus tearing",
35
+ "parent": "kneeMeniscus",
36
+ "children": []
37
+ },
38
+ "kneeMeniscusLateralTearing": {
39
+ "value": "Presence and/or severity of lateral meniscus tearing",
40
+ "parent": "kneeMeniscus",
41
+ "children": []
42
+ },
43
+ "kneeMeniscusWrisberg": {
44
+ "value": "Presence and/or severity of Wrisberg variant",
45
+ "parent": "kneeMeniscus",
46
+ "children": []
47
+ },
48
+ "kneeMeniscusRootTearing": {
49
+ "value": "Presence and/or severity of meniscus root tearing",
50
+ "parent": "kneeMeniscus",
51
+ "children": []
52
+ },
53
+ "kneeMeniscusRampLesion": {
54
+ "value": "Presence and/or severity of ramp lesion",
55
+ "parent": "kneeMeniscus",
56
+ "children": []
57
+ },
58
+ "kneeAclPcl": {
59
+ "value": "",
60
+ "parent": "root",
61
+ "children": [
62
+ "kneeAcl",
63
+ "kneePcl"
64
+ ]
65
+ },
66
+ "kneeAcl": {
67
+ "value": "",
68
+ "parent": "kneeAclPcl",
69
+ "children": [
70
+ "kneeAclTearing",
71
+ "kneeAclDegeneration",
72
+ "kneeAclReconstruction"
73
+ ]
74
+ },
75
+ "kneeAclTearing": {
76
+ "value": "Presence and/or severity of ACL tearing",
77
+ "parent": "kneeAcl",
78
+ "children": []
79
+ },
80
+ "kneeAclDegeneration": {
81
+ "value": "Presence and/or severity of ACL degeneration",
82
+ "parent": "kneeAcl",
83
+ "children": []
84
+ },
85
+ "kneeAclReconstruction": {
86
+ "value": "ACL reconstruction status",
87
+ "parent": "kneeAcl",
88
+ "children": []
89
+ },
90
+ "kneePcl": {
91
+ "value": "",
92
+ "parent": "kneeAclPcl",
93
+ "children": [
94
+ "kneePclTearing",
95
+ "kneePclDegeneration",
96
+ "kneePclReconstruction"
97
+ ]
98
+ },
99
+ "kneePclTearing": {
100
+ "value": "Presence and/or severity of PCL tearing",
101
+ "parent": "kneePcl",
102
+ "children": []
103
+ },
104
+ "kneePclDegeneration": {
105
+ "value": "Presence and/or severity of PCL degeneration",
106
+ "parent": "kneePcl",
107
+ "children": []
108
+ },
109
+ "kneePclReconstruction": {
110
+ "value": "PCL reconstruction status",
111
+ "parent": "kneePcl",
112
+ "children": []
113
+ },
114
+ "kneeMcl": {
115
+ "value": "",
116
+ "parent": "root",
117
+ "children": [
118
+ "kneeMclTearing",
119
+ "kneeMclDeepFibers",
120
+ "kneeMclSuperficialFibers"
121
+ ]
122
+ },
123
+ "kneeMclTearing": {
124
+ "value": "Presence and/or severity of MCL tearing",
125
+ "parent": "kneeMcl",
126
+ "children": []
127
+ },
128
+ "kneeMclDeepFibers": {
129
+ "value": "MCL deep fibers status",
130
+ "parent": "kneeMcl",
131
+ "children": []
132
+ },
133
+ "kneeMclSuperficialFibers": {
134
+ "value": "MCL superficial fibers status",
135
+ "parent": "kneeMcl",
136
+ "children": []
137
+ },
138
+ "kneePosterolateralCorner": {
139
+ "value": "",
140
+ "parent": "root",
141
+ "children": [
142
+ "kneeIlioTibialBand",
143
+ "kneeBicepsFemorisTendon",
144
+ "kneeLateralCollateralLigament"
145
+ ]
146
+ },
147
+ "kneeIlioTibialBand": {
148
+ "value": "Presence and/or severity of ilio-tibial band findings",
149
+ "parent": "kneePosterolateralCorner",
150
+ "children": []
151
+ },
152
+ "kneeBicepsFemorisTendon": {
153
+ "value": "Presence and/or severity of biceps femoris tendon findings",
154
+ "parent": "kneePosterolateralCorner",
155
+ "children": []
156
+ },
157
+ "kneeLateralCollateralLigament": {
158
+ "value": "Presence and/or severity of lateral collateral ligament findings",
159
+ "parent": "kneePosterolateralCorner",
160
+ "children": []
161
+ },
162
+ "kneeExtensorMechanism": {
163
+ "value": "",
164
+ "parent": "root",
165
+ "children": [
166
+ "kneeQuadricepsTendon",
167
+ "kneePatellarTendon"
168
+ ]
169
+ },
170
+ "kneeQuadricepsTendon": {
171
+ "value": "",
172
+ "parent": "kneeExtensorMechanism",
173
+ "children": [
174
+ "kneeQuadricepsTendonTearing",
175
+ "kneeQuadricepsTendinopathy"
176
+ ]
177
+ },
178
+ "kneeQuadricepsTendonTearing": {
179
+ "value": "Presence and/or severity of quadriceps tendon tearing",
180
+ "parent": "kneeQuadricepsTendon",
181
+ "children": []
182
+ },
183
+ "kneeQuadricepsTendinopathy": {
184
+ "value": "Presence and/or severity of quadriceps tendinopathy",
185
+ "parent": "kneeQuadricepsTendon",
186
+ "children": []
187
+ },
188
+ "kneePatellarTendon": {
189
+ "value": "",
190
+ "parent": "kneeExtensorMechanism",
191
+ "children": [
192
+ "kneePatellarTendonTearing",
193
+ "kneePatellarTendinopathy"
194
+ ]
195
+ },
196
+ "kneePatellarTendonTearing": {
197
+ "value": "Presence and/or severity of patellar tendon tearing",
198
+ "parent": "kneePatellarTendon",
199
+ "children": []
200
+ },
201
+ "kneePatellarTendinopathy": {
202
+ "value": "Presence and/or severity of patellar tendinopathy",
203
+ "parent": "kneePatellarTendon",
204
+ "children": []
205
+ },
206
+ "kneeCartilage": {
207
+ "value": "Articular cartilage status",
208
+ "parent": "root",
209
+ "children": [
210
+ "kneeCartilageFemoral",
211
+ "kneeCartilageTibial",
212
+ "kneeCartilagePatellar",
213
+ "kneeOsteochondralLesion"
214
+ ]
215
+ },
216
+ "kneeCartilageFemoral": {
217
+ "value": "",
218
+ "parent": "kneeCartilage",
219
+ "children": [
220
+ "kneeCartilageFemoralMedial",
221
+ "kneeCartilageFemoralLateral"
222
+ ]
223
+ },
224
+ "kneeCartilageFemoralMedial": {
225
+ "value": "Presence and/or severity of knee medial femoral cartilage findings",
226
+ "parent": "kneeCartilageFemoral",
227
+ "children": []
228
+ },
229
+ "kneeCartilageFemoralLateral": {
230
+ "value": "Presence and/or severity of knee lateral femoral cartilage findings",
231
+ "parent": "kneeCartilageFemoral",
232
+ "children": []
233
+ },
234
+ "kneeCartilageTibial": {
235
+ "value": "",
236
+ "parent": "kneeCartilage",
237
+ "children": [
238
+ "kneeCartilageTibialMedial",
239
+ "kneeCartilageTibialLateral"
240
+ ]
241
+ },
242
+ "kneeCartilageTibialMedial": {
243
+ "value": "Presence and/or severity of knee medial tibial cartilage findings",
244
+ "parent": "kneeCartilageTibial",
245
+ "children": []
246
+ },
247
+ "kneeCartilageTibialLateral": {
248
+ "value": "Presence and/or severity of knee lateral tibial cartilage findings",
249
+ "parent": "kneeCartilageTibial",
250
+ "children": []
251
+ },
252
+ "kneeCartilagePatellar": {
253
+ "value": "",
254
+ "parent": "kneeCartilage",
255
+ "children": [
256
+ "kneeCartilagePatellarMedial",
257
+ "kneeCartilagePatellarLateral"
258
+ ]
259
+ },
260
+ "kneeOsteochondralLesion": {
261
+ "value": "Presence and/or severity of knee osteochondral lesions/defects",
262
+ "parent": "kneeCartilage",
263
+ "children": []
264
+ },
265
+ "kneeCartilagePatellarMedial": {
266
+ "value": "Presence and/or severity of knee medial patellar cartilage findings",
267
+ "parent": "kneeCartilagePatellar",
268
+ "children": []
269
+ },
270
+ "kneeCartilagePatellarLateral": {
271
+ "value": "Presence and/or severity of knee lateral patellar cartilage findings",
272
+ "parent": "kneeCartilagePatellar",
273
+ "children": []
274
+ },
275
+ "kneeBone": {
276
+ "value": "",
277
+ "parent": "root",
278
+ "children": [
279
+ "kneeBoneFracture",
280
+ "kneeBoneMarrowEdema",
281
+ "kneeSubchondralFracture",
282
+ "kneeOsteonecrosis",
283
+ "kneeBoneAvn"
284
+ ]
285
+ },
286
+ "kneeBoneFracture": {
287
+ "value": "Presence, severity and/or location and/or type of knee bone fracture",
288
+ "parent": "kneeBone",
289
+ "children": []
290
+ },
291
+ "kneeBoneMarrowEdema": {
292
+ "value": "Presence and/or severity of knee bone marrow edema/contusion",
293
+ "parent": "kneeBone",
294
+ "children": []
295
+ },
296
+ "kneeSubchondralFracture": {
297
+ "value": "Presence and/or severity of knee subchondral fractures",
298
+ "parent": "kneeBone",
299
+ "children": []
300
+ },
301
+ "kneeOsteonecrosis": {
302
+ "value": "Presence and/or severity of knee osteonecrosis",
303
+ "parent": "kneeBone",
304
+ "children": []
305
+ },
306
+ "kneeBoneAvn": {
307
+ "value": "Presence and/or severity of knee avascular necrosis",
308
+ "parent": "kneeBone",
309
+ "children": []
310
+ },
311
+ "kneeOther": {
312
+ "value": "Other knee findings",
313
+ "parent": "root",
314
+ "children": [
315
+ "kneeBursa",
316
+ "kneePoplitealCyst",
317
+ "kneeGanglionCyst",
318
+ "kneeMass",
319
+ "kneeSynovium",
320
+ "other"
321
+ ]
322
+ },
323
+ "kneeBursa": {
324
+ "value": "Presence and/or severity of knee bursa findings, e.g. bursitis",
325
+ "parent": "kneeOther",
326
+ "children": []
327
+ },
328
+ "kneePoplitealCyst": {
329
+ "value": "Presence and/or extent of knee popliteal/Baker's cyst",
330
+ "parent": "kneeOther",
331
+ "children": []
332
+ },
333
+ "kneeGanglionCyst": {
334
+ "value": "Presence and/or extent of knee ganglion cyst",
335
+ "parent": "kneeOther",
336
+ "children": []
337
+ },
338
+ "kneeMass": {
339
+ "value": "Presence and/or extent of knee masses, e.g. lipoma, hemangioma, synovial sarcoma",
340
+ "parent": "kneeOther",
341
+ "children": []
342
+ },
343
+ "kneeSynovium": {
344
+ "value": "Presence and/or extent of knee synovial findings, e.g. synovitis, thickening",
345
+ "parent": "kneeOther",
346
+ "children": []
347
+ },
348
+ "other": {
349
+ "value": "Any other findings not listed above, such as soft tissue findings",
350
+ "parent": "kneeOther",
351
+ "children": []
352
+ }
353
+ }
log.txt ADDED
File without changes
logger.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class Logger:
2
+ def __init__(self, log_file):
3
+ self.log_file = log_file
4
+
5
+ def log(self, name, message):
6
+ try:
7
+ with open(self.log_file, "a", encoding="utf-8") as f:
8
+ f.write(f"[{name}]: {message}\n")
9
+ except OSError as ex:
10
+ print(f"Error logging message: {ex}")
11
+
12
+ def __call__(self, name, message):
13
+ self.log(name, message)
report_ckecklist_gradio.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import interface
3
+ import utils
4
+ import treegraph as tg
5
+ import logger as lg
6
+
7
+ system_prompt = """
8
+ You are a critical AI radiology assistant.
9
+ You are helping a radiologist correctly fill out a radiology report.
10
+ The report is regarding a Knee MRI.
11
+ """
12
+
13
+ graph, nodes_dict = tg.build_tree_from_file("knee_template.json")
14
+ report_interface = interface.ReportChecklistInterface(
15
+ llm=utils.LLM(model="gpt-3.5-turbo"),
16
+ system_prompt=system_prompt,
17
+ graph=graph,
18
+ nodes_dict=nodes_dict,
19
+ )
20
+ logger = None
21
+
22
+ if report_interface.prime_model() is False:
23
+ print("Model priming failed. Please try again.")
24
+ exit()
25
+ else:
26
+ print("Model priming successful.")
27
+
28
+ running = True
29
+
30
+
31
+ def check_report(report, name):
32
+ global logger, report_interface
33
+ if len(name.strip()) < 3:
34
+ return "Please enter a name."
35
+ else:
36
+ logger = lg.Logger(log_file="log.txt")
37
+ report_interface.logger = logger
38
+ report_interface.username = name
39
+ if running:
40
+ results = report_interface.process_input(report)
41
+ if results == "quit":
42
+ quit_fn()
43
+ elif results == "help":
44
+ return report_interface.help_message
45
+ elif results == "exception":
46
+ return "An exception occurred. Please try again."
47
+ else:
48
+ return results
49
+ else:
50
+ return "Model has been stopped."
51
+
52
+
53
+ def quit_fn():
54
+ global running
55
+ running = False
56
+ return "Model has been stopped."
57
+
58
+
59
+ with gr.Blocks(theme="soft") as demo:
60
+ gr.Markdown("## Radiology Report Assistant")
61
+ gr.Markdown(report_interface.help_message)
62
+ name_textbox = gr.Textbox(label="Name")
63
+
64
+ report_textbox = gr.TextArea(label="Report", lines=20, max_lines=50)
65
+ with gr.Row():
66
+ check_btn = gr.Button(
67
+ value="Check Report",
68
+ )
69
+ clear_btn = gr.ClearButton(
70
+ value="Clear Messages",
71
+ )
72
+ quit_btn = gr.Button(
73
+ value="Quit",
74
+ )
75
+ results_textbox = gr.TextArea(label="Results", lines=20, max_lines=50)
76
+ clear_btn.add([results_textbox, report_textbox])
77
+
78
+ check_btn.click(
79
+ fn=check_report,
80
+ inputs=[report_textbox, name_textbox],
81
+ outputs=[results_textbox],
82
+ )
83
+ quit_btn.click(fn=quit_fn, outputs=[results_textbox])
84
+
85
+ demo.launch(auth=("admin", "radiologyreport"))