'''
- 该对话树模拟进行就诊挂号，用户通过与医院挂号机器人进行对话，提供相关的就诊信息（如就诊科室、就诊日期、就诊时间等）
- 该对话树的核心是 “#多轮交互#” 节点，特别是其中 “执行函数” 属性指向的 check_registered_info() 函数：该函数会在每轮对话时被执行，根据用户提供的信息进行校验，并给出相应的提示（通过 {_多轮交互动态提示词_} 系统信息项）引导用户提供正确或者更具体的信息，直到满足挂号条件或者出现系统异常等情况需要退出
- 这种以 “#多轮交互#” 节点为核心实现的业务逻辑，仅仅适合于需要采集的信息项比较少的情况（如本对话树中只有 3 个），如果需要采集的信息项比较多，或者信息项之间的逻辑关系比较复杂，则需要使用若干 “#单次交互#” 节点来实现，具体见 “商旅平台.py” / “商旅平台_预定机票.py” 对话树
- 同时在整个过程中，系统可根据用户的输入随时提供 “急救知识、医院就诊与挂号指南” 知识库的相关知识，而不会打断对话流程的逻辑
- 最后，可以在服务器的项目目录下，执行 “python ./ichatdef/firstapp/py_chattree/医院挂号.py” 生成 “医院挂号.html”，下载到本地用浏览器打开，即可看到整个对话树的拓扑结构及相关代码信息
'''

# -------------------------------------------------------------------------------------
# 每个 python 对话树文件的标准代码头部
# -------------------------------------------------------------------------------------

import sys, os

project_root = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../.."))
if project_root not in sys.path:
    sys.path.append(project_root)
from chattree_def import *

chattree = ChatTree()

# -------------------------------------------------------------------------------------
# 对话树节点的定义部分
# -------------------------------------------------------------------------------------

from datetime import datetime, timedelta
import random, re

department_list = ['心血管内科', '呼吸内科', '消化内科', '神经内科', '内分泌科', '普外科', '骨科', '泌尿外科', '妇科', '产科', '儿科', '眼科', '耳鼻喉科', '口腔科', '皮肤科']

# 开始节点
start_node = chattree.create_node( "#开始#", {
    "对话树标题":"医院挂号服务", # 一段简短的文本，描述这个对话树的主题或者用途
    "系统角色":"医院挂号机器人",
    "用户角色":"患者",
    "背景信息":"你是负责为患者的就医进行挂号，并回答相关问题",
    "是否允许转接人工":False,
    "静态参考信息":"急救知识、医院就诊与挂号指南", # 指向知识库文件名（不带扩展名）
})

# 在进入下面的 “#多轮交互#” 节点前，先给用户一个欢迎提示词
def execute_srcipt(ctx):
    ctx["{_多轮交互动态提示词_}"] = "询问患者：“您好，请问您要挂什么时间、哪个科室的号？”"
execute_srcipt_node = chattree.create_node( "#动作#执行脚本#", {
    "函数":execute_srcipt,
})

# 本对话树的核心节点及相关的函数
def register(register_department, register_date, register_time): # 模拟的挂号函数（根据概率返回相关结果），实际应用中这里会调用医院的挂号系统接口
    if random.random() < 0.1:
        return False, "系统异常"
    if random.random() < 0.3:
        return False, "已无号源"
    return True, "JZ123456"
def check_registered_info(ctx):  # “#多轮交互#” 节点的核心函数，每轮对话时都会执行
    ctx["{_多轮交互动态提示词_}"] = ""
    if ctx["{就诊科室}"].state() == 1:
        if ctx["{就诊科室}"].as_str() not in department_list:
            ctx["{就诊科室}"].clear_info()
            ctx["{_多轮交互动态提示词_}"] = ctx["{_多轮交互动态提示词_}"].as_str() + "提示患者只能挂号如下科室：" + str(department_list) + "\n"
    else:
        ctx["{_多轮交互动态提示词_}"] = ctx["{_多轮交互动态提示词_}"].as_str() + "请患者告知要挂的科室。如果患者不确定哪个科室，可以（询问患者症状后）根据患者提供的症状信息向患者建议相关科室（注意只能建议 " + str(department_list) + " 这些科室），并请患者确认；同时根据患者提供的症状信息，给到患者相关建议\n"
    if ctx["{就诊日期}"].state() == 1:
        if not re.match(r"\d{4}-\d{2}-\d{2}", ctx["{就诊日期}"].as_str()):
            ctx["{就诊日期}"].clear_info()
            ctx["{_多轮交互动态提示词_}"] = ctx["{_多轮交互动态提示词_}"].as_str() + "提示患者提供正确的就诊日期\n"
        else:
            try:
                registered_date_obj = datetime.strptime(ctx["{就诊日期}"].as_str(), "%Y-%m-%d")
                if registered_date_obj < datetime.now() or registered_date_obj > datetime.now() + timedelta(days=10):
                    ctx["{就诊日期}"].clear_info()
                    ctx["{_多轮交互动态提示词_}"] = ctx["{_多轮交互动态提示词_}"].as_str() + "提示患者只能挂 10 天以内的号\n"
            except ValueError:
                ctx["{就诊日期}"].clear_info()
                ctx["{_多轮交互动态提示词_}"] = ctx["{_多轮交互动态提示词_}"].as_str() + "提示患者提供正确的就诊日期\n"
    else:
        ctx["{_多轮交互动态提示词_}"] = ctx["{_多轮交互动态提示词_}"].as_str() + "请患者提供就诊日期\n"
    if ctx["{就诊时间是上午还是下午}"].state() == 1:
        if ctx["{就诊时间是上午还是下午}"].as_str() not in ["下午","上午"]:
            ctx["{就诊时间是上午还是下午}"].clear_info()
            ctx["{_多轮交互动态提示词_}"] = ctx["{_多轮交互动态提示词_}"].as_str() + "提示患者就诊时间只能是上午或下午\n"
    else:
        ctx["{_多轮交互动态提示词_}"] = ctx["{_多轮交互动态提示词_}"].as_str() + "请患者告知就诊时间是上午还是下午\n"
    if ctx["{就诊科室}"].state() == 1 and ctx["{就诊日期}"].state() == 1 and ctx["{就诊时间是上午还是下午}"].state() == 1:
        assert ctx["{_多轮交互动态提示词_}"].as_str() == ""
        if_success, ret_val = register(ctx["{就诊科室}"].as_str(), ctx["{就诊日期}"].as_str(), ctx["{就诊时间是上午还是下午}"].as_str())
        if if_success:
            ctx["{_多轮交互退出原因_}"] = "已成功挂号"
            ctx["{_多轮交互动态提示词_}"] = f"表示已挂号成功，就诊编号为{ret_val}\n"
        else:
            if ret_val == "已无号源":
                ctx["{就诊科室}"].clear_info()
                ctx["{就诊日期}"].clear_info()
                ctx["{就诊时间是上午还是下午}"].clear_info()
                ctx["{_多轮交互动态提示词_}"] = f"提示用户：“已无号源，请选择其它科室和日期”\n"
            elif ret_val == "系统异常":
                ctx["{_多轮交互动态提示词_}"] = f"提示用户：“系统异常，请稍后再试”\n"
                ctx["{_多轮交互退出原因_}"] = "系统异常"
multi_turn_node = chattree.create_node( "#多轮交互#", {
    "抽取信息":[ # 每轮对话均会抽取这些信息项，并将抽取到的信息存储在 ctx 中，供函数 check_registered_info() 使用
        {
            "信息项":"{就诊科室}",
            "信息项约束":"一定是患者提供的科室或经过患者确认的科室",
            "信息项选项":department_list,
            "信息项选项修饰":['开放']
        },
        {"信息项":"{就诊日期}", "信息项约束":"格式是YYYY-MM-DD"},
        {"信息项":"{就诊时间是上午还是下午}", "信息项约束":"只能是‘上午’或‘下午’"},
    ],
    "执行函数":check_registered_info, # 每轮对话均会执行
})

# -------------------------------------------------------------------------------------
# 完整的拓扑结构，“>>” 表示节点之间的连接关系，这里的连接关系也决定了对话的流程走向
# -------------------------------------------------------------------------------------

start_node >> execute_srcipt_node >> multi_turn_node

# -------------------------------------------------------------------------------------
# 每个 python 对话树文件的标准代码结尾部分，即渲染对话树成为 HTML 文件的代码
# -------------------------------------------------------------------------------------

if __name__ == "__main__":
    chattree.render(__file__)