import gradio as gr import langextract as lx import textwrap import os import uuid # 用于生成唯一的文件名 # --- 1. 定义 LangExtract 的提取逻辑 --- # 这部分包含了调用 LangExtract 所需的 Prompt 和范例 # 我们将其封装在一个函数中,以便 Gradio 调用 def get_extraction_config(): """ 准备 LangExtract 所需的 Prompt 和范例数据。 """ # 定义提取指令:告诉模型要做什么 prompt_description = textwrap.dedent(""" 从文本中提取药物及其相关的详细信息,并使用属性对相关信息进行分组: 1. 按照实体在文本中出现的顺序进行提取。 2. 每个被提取的实体都必须有一个 'medication_group' 属性,用来将其与对应的药物关联起来。 3. 关于同一种药物的所有详细信息(如剂量、频率)都应共享相同的 'medication_group' 值。 """) # 提供一个高质量的范例,指导模型如何执行任务 examples = [ lx.data.ExampleData( text="病人每日服用阿司匹林 100mg 以保护心脏健康,并在睡前服用辛伐他汀 20mg。", extractions=[ # 第一个药物组 lx.data.Extraction( extraction_class="药物", extraction_text="阿司匹林", attributes={"medication_group": "阿司匹林"} ), lx.data.Extraction( extraction_class="剂量", extraction_text="100mg", attributes={"medication_group": "阿司匹林"} ), lx.data.Extraction( extraction_class="频率", extraction_text="每日", attributes={"medication_group": "阿司匹林"} ), lx.data.Extraction( extraction_class="病症", extraction_text="心脏健康", attributes={"medication_group": "阿司匹林"} ), # 第二个药物组 lx.data.Extraction( extraction_class="药物", extraction_text="辛伐他汀", attributes={"medication_group": "辛伐他汀"} ), lx.data.Extraction( extraction_class="剂量", extraction_text="20mg", attributes={"medication_group": "辛伐他汀"} ), lx.data.Extraction( extraction_class="频率", extraction_text="睡前", attributes={"medication_group": "辛伐他汀"} ) ] ) ] return prompt_description, examples # --- 2. Gradio 的核心处理函数 --- # 当用户点击按钮时,这个函数会被触发 def process_text_and_visualize(input_text): """ 接收输入文本,调用 LangExtract 进行处理,并返回多种类型的输出给 Gradio。 """ if not input_text.strip(): # 如果输入为空,返回空的初始结果 return None, "请输入文本...", None, None, None # 从环境变量中获取 API Key(在 Hugging Face 中需要设置为 Secret) # api_key = os.environ.get("LANGEXTRACT_API_KEY") # if not api_key: # raise gr.Error("错误:未设置 LANGEXTRACT_API_KEY。请在 Hugging Face Space 的 Secrets 中添加它。") prompt, examples = get_extraction_config() # --- 真实的 LangExtract 调用 --- # 在实际部署时,请取消注释以下代码 # result = lx.extract( # text_or_documents=input_text, # prompt_description=prompt, # examples=examples, # model_id="gemini-2.5-pro", # 推荐使用最新的强大模型 # api_key=api_key # ) # --- 为了演示,我们使用模拟的(Mock)结果 --- # 这部分模拟了 `lx.extract` 的返回结果,以便在没有 API Key 的情况下也能运行和调试 from langextract.data import AnnotatedDocument, Extraction, CharInterval result = AnnotatedDocument( text=input_text, extractions=[ Extraction(extraction_class='药物', extraction_text='Lisinopril', attributes={'medication_group': 'Lisinopril'}, char_interval=CharInterval(start_pos=28, end_pos=38)), Extraction(extraction_class='药物', extraction_text='Metformin', attributes={'medication_group': 'Metformin'}, char_interval=CharInterval(start_pos=43, end_pos=52)), Extraction(extraction_class='药物', extraction_text='Lisinopril', attributes={'medication_group': 'Lisinopril'}, char_interval=CharInterval(start_pos=78, end_pos=88)), Extraction(extraction_class='剂量', extraction_text='10mg', attributes={'medication_group': 'Lisinopril'}, char_interval=CharInterval(start_pos=89, end_pos=93)), Extraction(extraction_class='频率', extraction_text='daily', attributes={'medication_group': 'Lisinopril'}, char_interval=CharInterval(start_pos=94, end_pos=99)), Extraction(extraction_class='病症', extraction_text='hypertension', attributes={'medication_group': 'Lisinopril'}, char_interval=CharInterval(start_pos=104, end_pos=116)), Extraction(extraction_class='药物', extraction_text='Metformin', attributes={'medication_group': 'Metformin'}, char_interval=CharInterval(start_pos=149, end_pos=158)), Extraction(extraction_class='剂量', extraction_text='500mg', attributes={'medication_group': 'Metformin'}, char_interval=CharInterval(start_pos=159, end_pos=164)), Extraction(extraction_class='频率', extraction_text='twice daily', attributes={'medication_group': 'Metformin'}, char_interval=CharInterval(start_pos=192, end_pos=203)), Extraction(extraction_class='病症', extraction_text='diabetes', attributes={'medication_group': 'Metformin'}, char_interval=CharInterval(start_pos=208, end_pos=216)), ] ) # --- 模拟结束 --- # 1. 准备命名实体识别 (NER) 的高亮文本输出 highlighted_text = [] last_pos = 0 # 首先按位置对实体进行排序,确保高亮正确 sorted_extractions = sorted(result.extractions, key=lambda e: e.char_interval.start_pos) for entity in sorted_extractions: start, end = entity.char_interval.start_pos, entity.char_interval.end_pos # 添加实体前面的文本 highlighted_text.append((input_text[last_pos:start], None)) # 添加高亮的实体 highlighted_text.append((entity.extraction_text, entity.extraction_class)) last_pos = end # 添加最后剩余的文本 highlighted_text.append((input_text[last_pos:], None)) # 2. 准备关系提取 (RE) 的结构化 Markdown 输出 medication_groups = {} for extraction in result.extractions: group_name = extraction.attributes.get("medication_group", "未分组") if group_name not in medication_groups: medication_groups[group_name] = [] medication_groups[group_name].append(extraction) structured_output = "### 结构化提取结果\n\n" for med_name, extractions in medication_groups.items(): structured_output += f"#### 药物组: {med_name}\n" for extraction in extractions: pos_info = f" (位置: {extraction.char_interval.start_pos}-{extraction.char_interval.end_pos})" structured_output += f"- **{extraction.extraction_class}**: {extraction.extraction_text}{pos_info}\n" structured_output += "\n" # 3. 生成并保存交互式可视化 HTML 和 JSONL 文件 session_id = str(uuid.uuid4()) output_dir = "/tmp" # 在 Hugging Face 中,使用 /tmp 目录是安全的 os.makedirs(output_dir, exist_ok=True) jsonl_filename = f"extraction_{session_id}.jsonl" jsonl_path = os.path.join(output_dir, jsonl_filename) html_path = os.path.join(output_dir, f"visualization_{session_id}.html") # --- 错误修复 --- # 原代码: lx.io.save_annotated_documents([result], output_path=jsonl_path) # 错误原因: 函数没有 `output_path` 参数。 # 正确用法: 使用 `output_name` 和 `output_dir` 参数。 lx.io.save_annotated_documents( [result], output_name=jsonl_filename, output_dir=output_dir ) html_content = lx.visualize(jsonl_path) with open(html_path, "w", encoding="utf-8") as f: f.write(html_content) # 4. 返回所有结果给 Gradio 界面 return highlighted_text, structured_output, html_path, html_path, jsonl_path # --- 3. 创建 Gradio 应用界面 --- # 使用 gr.Blocks() 来自定义更复杂、更美观的布局 with gr.Blocks(theme=gr.themes.Soft(), title="药物信息提取器") as demo: # 应用标题 gr.Markdown( """ # ⚕️ LangExtract 药物信息提取器 一个基于大型语言模型 (LLM) 的智能工具,可自动从非结构化的临床文本中提取药物、剂量、频率、病症等信息,并将相关信息进行结构化关联。 """ ) with gr.Row(): # 左侧为输入区 with gr.Column(scale=1): input_textbox = gr.Textbox( lines=10, label="临床文本输入", placeholder=textwrap.dedent("""请在此处粘贴临床笔记... 例如: The patient was prescribed Lisinopril and Metformin last month. He takes the Lisinopril 10mg daily for hypertension, but often misses his Metformin 500mg dose which should be taken twice daily for diabetes. """) ) submit_btn = gr.Button("🧠 提取信息", variant="primary") # 右侧为输出区 with gr.Column(scale=2): # 使用 Tab 将不同类型的输出分开 with gr.Tabs(): with gr.TabItem("📊 总览 (NER & RE)"): gr.Markdown("### 命名实体识别 (NER) - 文本高亮") output_highlight = gr.HighlightedText( label="实体高亮显示", color_map={"药物": "#FF6347", "剂量": "#FFA500", "频率": "#32CD32", "病症": "#4169E1"} ) output_structured = gr.Markdown(label="结构化关系") with gr.TabItem("🌐 交互式可视化"): gr.Markdown("### 交互式可视化图表") # 使用 iframe 来确保 HTML 正确渲染 output_html_viewer = gr.HTML(label="交互式图表 (可缩放和筛选)") with gr.TabItem("📁 文件下载"): gr.Markdown("### 下载提取结果") download_html = gr.File(label="下载交互式 HTML 文件") download_jsonl = gr.File(label="下载 JSONL 数据文件") # 设置按钮的点击事件 submit_btn.click( fn=process_text_and_visualize, inputs=input_textbox, outputs=[output_highlight, output_structured, output_html_viewer, download_html, download_jsonl] ) # 添加一些范例,方便用户快速体验 gr.Examples( examples=[ "The patient was prescribed Lisinopril and Metformin last month.\nHe takes the Lisinopril 10mg daily for hypertension, but often misses his Metformin 500mg dose which should be taken twice daily for diabetes.", "Patient took 400 mg PO Ibuprofen q4h for two days for a headache.", "Take one tablet of Amoxicillin 500mg every 8 hours for 7 days." ], inputs=input_textbox, label="示例输入" ) # 启动 Gradio 应用 if __name__ == "__main__": demo.launch()