microhum commited on
Commit
e458941
·
1 Parent(s): 3f5095b

init: add llm logic & fastapi & CLI version

Browse files
.env_template ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ TYPHOON_CHAT_API = *
2
+ OPENTHAIGPT_CHAT_API = *
.gitignore CHANGED
@@ -1,162 +1,2 @@
1
- # Byte-compiled / optimized / DLL files
2
- __pycache__/
3
- *.py[cod]
4
- *$py.class
5
-
6
- # C extensions
7
- *.so
8
-
9
- # Distribution / packaging
10
- .Python
11
- build/
12
- develop-eggs/
13
- dist/
14
- downloads/
15
- eggs/
16
- .eggs/
17
- lib/
18
- lib64/
19
- parts/
20
- sdist/
21
- var/
22
- wheels/
23
- share/python-wheels/
24
- *.egg-info/
25
- .installed.cfg
26
- *.egg
27
- MANIFEST
28
-
29
- # PyInstaller
30
- # Usually these files are written by a python script from a template
31
- # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
- *.manifest
33
- *.spec
34
-
35
- # Installer logs
36
- pip-log.txt
37
- pip-delete-this-directory.txt
38
-
39
- # Unit test / coverage reports
40
- htmlcov/
41
- .tox/
42
- .nox/
43
- .coverage
44
- .coverage.*
45
- .cache
46
- nosetests.xml
47
- coverage.xml
48
- *.cover
49
- *.py,cover
50
- .hypothesis/
51
- .pytest_cache/
52
- cover/
53
-
54
- # Translations
55
- *.mo
56
- *.pot
57
-
58
- # Django stuff:
59
- *.log
60
- local_settings.py
61
- db.sqlite3
62
- db.sqlite3-journal
63
-
64
- # Flask stuff:
65
- instance/
66
- .webassets-cache
67
-
68
- # Scrapy stuff:
69
- .scrapy
70
-
71
- # Sphinx documentation
72
- docs/_build/
73
-
74
- # PyBuilder
75
- .pybuilder/
76
- target/
77
-
78
- # Jupyter Notebook
79
- .ipynb_checkpoints
80
-
81
- # IPython
82
- profile_default/
83
- ipython_config.py
84
-
85
- # pyenv
86
- # For a library or package, you might want to ignore these files since the code is
87
- # intended to run in multiple environments; otherwise, check them in:
88
- # .python-version
89
-
90
- # pipenv
91
- # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
- # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
- # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
- # install all needed dependencies.
95
- #Pipfile.lock
96
-
97
- # poetry
98
- # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
- # This is especially recommended for binary packages to ensure reproducibility, and is more
100
- # commonly ignored for libraries.
101
- # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
- #poetry.lock
103
-
104
- # pdm
105
- # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
- #pdm.lock
107
- # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
- # in version control.
109
- # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
110
- .pdm.toml
111
- .pdm-python
112
- .pdm-build/
113
-
114
- # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115
- __pypackages__/
116
-
117
- # Celery stuff
118
- celerybeat-schedule
119
- celerybeat.pid
120
-
121
- # SageMath parsed files
122
- *.sage.py
123
-
124
- # Environments
125
- .env
126
  .venv
127
- env/
128
- venv/
129
- ENV/
130
- env.bak/
131
- venv.bak/
132
-
133
- # Spyder project settings
134
- .spyderproject
135
- .spyproject
136
-
137
- # Rope project settings
138
- .ropeproject
139
-
140
- # mkdocs documentation
141
- /site
142
-
143
- # mypy
144
- .mypy_cache/
145
- .dmypy.json
146
- dmypy.json
147
-
148
- # Pyre type checker
149
- .pyre/
150
-
151
- # pytype static type analyzer
152
- .pytype/
153
-
154
- # Cython debug symbols
155
- cython_debug/
156
-
157
- # PyCharm
158
- # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159
- # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160
- # and can be added to the global gitignore or merged into this file. For a more nuclear
161
- # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162
- #.idea/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  .venv
2
+ .env
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md ADDED
@@ -0,0 +1 @@
 
 
1
+ # Nurse LLM
__pycache__/main.cpython-311.pyc ADDED
Binary file (2.97 kB). View file
 
cli.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from llm.client import NurseCLI
2
+ from llm.llm import VirtualNurseLLM
3
+
4
+ # model: typhoon-v1.5x-70b-instruct
5
+ # nurse_llm = VirtualNurseLLM(
6
+ # base_url="https://api.opentyphoon.ai/v1",
7
+ # model="typhoon-v1.5x-70b-instruct",
8
+ # api_key=os.getenv("TYPHOON_API_KEY")
9
+ # )
10
+
11
+ # model: OpenThaiGPT
12
+
13
+ if __name__ == "__main__":
14
+ nurse_llm = VirtualNurseLLM(
15
+ base_url="https://api.aieat.or.th/v1",
16
+ model=".",
17
+ api_key="dummy"
18
+ )
19
+
20
+ cli = NurseCLI(nurse_llm)
21
+ cli.start()
llm/__pycache__/basemodel.cpython-311.pyc ADDED
Binary file (3.48 kB). View file
 
llm/__pycache__/client.cpython-311.pyc ADDED
Binary file (3.69 kB). View file
 
llm/__pycache__/llm.cpython-311.pyc ADDED
Binary file (9.32 kB). View file
 
llm/__pycache__/prompt.cpython-311.pyc ADDED
Binary file (22 kB). View file
 
llm/basemodel.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional
3
+
4
+ class Name(BaseModel):
5
+ prefix: Optional[str] = Field(None, description="Prefix or title (e.g., Mr., Ms., Dr.)")
6
+ firstname: Optional[str] = Field(None, description="The patient's first name")
7
+ surname: Optional[str] = Field(None, description="The patient's surname")
8
+
9
+ class FamilyHistory(BaseModel):
10
+ relation: str = Field(..., description="Relation to the patient (e.g., father, sister)")
11
+ condition: str = Field(..., description="Health condition present in the family member")
12
+
13
+ class PersonalHistory(BaseModel):
14
+ type: str = Field(..., description="Type of personal health aspect (e.g., sleep, medication, health behavior)")
15
+ description: str = Field(..., description="Details about the health aspect")
16
+
17
+ class EHRModel(BaseModel):
18
+ name: Optional[Name] = Field(None, description="Structured name of the patient")
19
+ age: Optional[int] = Field(None, description="The patient's age")
20
+ gender: Optional[str] = Field(None, description="The patient's gender")
21
+ chief_complaint: Optional[str] = Field(None, description="The main symptom reported by the patient")
22
+ present_illness: Optional[str] = Field(None, description="Details about the current illness (e.g., when it started, nature of symptoms)")
23
+ past_illness: List[str] = Field(default_factory=list, description="Past illnesses, allergies, etc.")
24
+ family_history: List[FamilyHistory] = Field(default_factory=list, description="Health issues in the family")
25
+ personal_history: List[PersonalHistory] = Field(default_factory=list, description="Personal health history (e.g., sleep patterns, medications taken)")
llm/client.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pprint import pprint
2
+ from llm.llm import VirtualNurseLLM
3
+
4
+ class NurseCLI:
5
+ def __init__(self, nurse_llm: VirtualNurseLLM):
6
+ self.nurse_llm = nurse_llm
7
+ self.nurse_llm.debug = False
8
+
9
+ def start(self):
10
+ print("Welcome to the Nurse LLM CLI.")
11
+ print("Type your question, or enter 'history' to see chat history")
12
+ print("Enter 'help' for a list of available commands.")
13
+
14
+ while True:
15
+ user_input = input("\nYou: ")
16
+
17
+ if user_input.lower() == 'exit':
18
+ print("Exiting the CLI. Goodbye!")
19
+ break
20
+ elif user_input.lower() == 'history':
21
+ print("\n--- Chat History ---")
22
+ pprint(self.nurse_llm.chat_history)
23
+ elif user_input.lower() == 'ehr':
24
+ print("\n--- Current EHR Data ---")
25
+ pprint(self.nurse_llm.ehr_data)
26
+ elif user_input.lower() == 'status':
27
+ print("\n--- Current LLM Status ---")
28
+ pprint(self.nurse_llm.current_prompt)
29
+ elif user_input.lower() == 'debug':
30
+ self.nurse_llm.debug = not self.nurse_llm.debug
31
+ print(f"Debug mode is now {'on' if self.nurse_llm.debug else 'off'}.")
32
+ elif user_input.lower() == 'reset':
33
+ self.nurse_llm.reset()
34
+ print("Chat history and EHR data have been reset.")
35
+ elif user_input.lower() == 'help':
36
+ self.display_help()
37
+ else:
38
+ # Invoke the LLM with the user input and get the response
39
+ ehr_response = self.nurse_llm.invoke(user_input)
40
+
41
+ # Display the response from the nurse LLM
42
+ print("\nNurse LLM:", ehr_response)
43
+
44
+ def display_help(self):
45
+ print("""
46
+ --- Available Commands ---
47
+ - 'history' : View the chat history.
48
+ - 'ehr' : View the current EHR (Electronic Health Record) data.
49
+ - 'status' : View the current LLM status and prompt.
50
+ - 'debug' : Toggle the debug mode (on/off).
51
+ - 'reset' : Reset the chat history and EHR data.
52
+ - 'help' : Display this help message.
53
+ - 'exit' : Exit the CLI.
54
+ """)
55
+
56
+
57
+
llm/llm.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from langchain_openai.chat_models import ChatOpenAI
3
+ from langchain.prompts import ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate
4
+ from pydantic import ValidationError
5
+ import json
6
+ from pprint import pprint
7
+ from llm.basemodel import EHRModel
8
+ from llm.prompt import field_descriptions, TASK_INSTRUCTIONS, JSON_EXAMPLE
9
+
10
+ class VirtualNurseLLM:
11
+ def __init__(self, base_url, model, api_key):
12
+ self.client = ChatOpenAI(
13
+ base_url=base_url,
14
+ model=model,
15
+ api_key=api_key
16
+ )
17
+ self.TASK_INSTRUCTIONS = TASK_INSTRUCTIONS
18
+ self.field_descriptions = field_descriptions
19
+ self.JSON_EXAMPLE = JSON_EXAMPLE
20
+ self.ehr_data = {}
21
+ self.chat_history = []
22
+ self.debug = False
23
+ self.current_prompt = None
24
+ self.current_question = None
25
+
26
+ def create_prompt(self, task_type):
27
+ if task_type == "extract_ehr":
28
+ system_instruction = self.TASK_INSTRUCTIONS.get("extract_ehr")
29
+
30
+ elif task_type == "question":
31
+ system_instruction = self.TASK_INSTRUCTIONS.get("question")
32
+ else:
33
+ raise ValueError("Invalid task type.")
34
+
35
+ # system + user
36
+ system_template = SystemMessagePromptTemplate.from_template(system_instruction)
37
+ user_template = HumanMessagePromptTemplate.from_template("response: {patient_response}")
38
+ prompt = ChatPromptTemplate.from_messages([system_template, user_template])
39
+ return prompt
40
+
41
+ def gather_ehr(self, patient_response, max_retries=3):
42
+ prompt = self.create_prompt("extract_ehr")
43
+ messages = prompt.format_messages(ehr_data=self.ehr_data, patient_response=patient_response, example=self.JSON_EXAMPLE)
44
+ self.current_prompt = messages
45
+ response = self.client(messages=messages)
46
+ if self.debug:
47
+ pprint(f"gather ehr llm response: \n{response.content}\n")
48
+
49
+ retry_count = 0
50
+ while retry_count < max_retries:
51
+ try:
52
+ json_content = self.extract_json_content(response.content)
53
+ if self.debug:
54
+ pprint(f"JSON after dumps:\n{json_content}\n")
55
+ ehr_data = EHRModel.parse_raw(json_content)
56
+
57
+ # Update only missing parameters
58
+ for key, value in ehr_data.dict().items():
59
+ if value not in [None, [], {}]: # Checks for None and empty lists or dicts
60
+ print(f"Updating {key} with value {value}")
61
+ self.ehr_data[key] = value
62
+
63
+ return self.ehr_data
64
+
65
+ except (ValidationError, json.JSONDecodeError) as e:
66
+ print(f"Error parsing EHR data: {e}")
67
+ retry_count += 1
68
+
69
+ if retry_count < max_retries:
70
+ retry_prompt = (
71
+ "กรุณาตรวจสอบให้แน่ใจว่าข้อมูลที่ให้มาอยู่ในรูปแบบ JSON ที่ถูกต้องตามโครงสร้างตัวอย่าง "
72
+ "และแก้ไขปัญหาทางไวยากรณ์หรือรูปแบบที่ไม่ถูกต้อง รวมถึงให้ข้อมูลในรูปแบบที่สอดคล้องกัน "
73
+ f"Attempt {retry_count + 1} of {max_retries}."
74
+ )
75
+ messages = self.create_prompt("extract_ehr") + "\n\n# ลองใหม่: \n\n{retry_prompt} \n ## JSON เก่าที่มีปัญหา: \n{json_problem}"
76
+ messages = messages.format_messages(
77
+ patient_response=patient_response,
78
+ example=self.JSON_EXAMPLE,
79
+ retry_prompt=retry_prompt,
80
+ json_problem=json_content
81
+ )
82
+ self.current_prompt = messages
83
+ print(f"กำลังลองใหม่ด้วย prompt ที่ปรับแล้ว: {retry_prompt}")
84
+ response = self.client(messages=messages)
85
+
86
+ # Final error message if retries are exhausted
87
+ print("Failed to extract valid EHR data after multiple attempts. Generating new question.")
88
+ return {"result": response, "error": "Failed to extract valid EHR data. Please try again."}
89
+
90
+
91
+ def get_question(self, patient_response):
92
+ question_prompt = self.create_prompt("question")
93
+ # Update EHR data with the latest patient response
94
+ ehr_data = self.gather_ehr(patient_response)
95
+ if self.debug:
96
+ pprint(ehr_data)
97
+
98
+ for field, description in self.field_descriptions.items():
99
+ # Find the next missing field and generate a question
100
+ if field not in self.ehr_data or not self.ehr_data[field]:
101
+ # Compile known patient information as context
102
+ context = ", ".join(
103
+ f"{key}: {value}" for key, value in self.ehr_data.items() if value
104
+ )
105
+ print("fetching for ", f'"{field}":"{description}"')
106
+ history_context = "\n".join(
107
+ f"{entry['role']}: {entry['content']}" for entry in self.chat_history
108
+ )
109
+ messages = ChatPromptTemplate.from_messages([question_prompt, history_context])
110
+ messages = messages.format_messages(description=f'"{field}":"{description}"', context=context, patient_response=patient_response, field_descriptions=self.field_descriptions)
111
+ self.current_context = context
112
+ self.current_prompt = messages
113
+
114
+ # format print
115
+ # pprint(pformat(messages.messages[0].prompt.template, indent=4, width=80))
116
+ response = self.client(messages=messages)
117
+
118
+ # Store generated question in chat history and return it
119
+ self.current_question = response.content.strip()
120
+ return self.current_question
121
+
122
+ # If all fields are complete
123
+ self.current_question = "ขอบคุณที่ให้ข้อมูลค่ะ ฉันได้ข้อมูลที่ต้องการครบแล้วค่ะ"
124
+ return self.current_question
125
+
126
+ def invoke(self, patient_response):
127
+ if patient_response:
128
+ self.chat_history.append({"role": "user", "content": patient_response})
129
+ question = self.get_question(patient_response)
130
+ self.chat_history.append({"role": "assistant", "content": question})
131
+ return question
132
+
133
+ def extract_json_content(self, content):
134
+ try:
135
+ content = content.replace('\n', '').replace('\r', '')
136
+ start = content.index('{')
137
+ end = content.rindex('}') + 1
138
+ json_str = content[start:end]
139
+ json_str = json_str.replace('None', 'null')
140
+
141
+ return json_str
142
+ except ValueError:
143
+ print("JSON Parsing Error Occured: ", content)
144
+ print("No valid JSON found in response")
145
+ return None
146
+
147
+
148
+ def reset(self):
149
+ self.ehr_data = {}
150
+ self.chat_history = []
151
+ self.current_question = None
llm/prompt.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ JSON_EXAMPLE = """
2
+ JSON schema:
3
+ {
4
+ "name": {
5
+ "prefix": "<คำนำหน้าชื่อ>",
6
+ "firstname": "<ชื่อจริง>",
7
+ "surname": "<นามสกุล>"
8
+ },
9
+ "age": <integer>,
10
+ "gender": "<string>",
11
+ "chief_complaint": "<string>",
12
+ "present_illness": "<string>",
13
+ "past_illness": ["<โรคประจำตัว 1>", "<โรคประจำตัว 2>"],
14
+ "family_history": [
15
+ {
16
+ "relation": "<ความสัมพันธ์>",
17
+ "condition": "<โรค>"
18
+ }
19
+ ],
20
+ "personal_history": [
21
+ {
22
+ "type": "<ประเภท>",
23
+ "description": "<คำอธิบาย>"
24
+ }
25
+ ]
26
+ }
27
+ \n
28
+ Example 1:
29
+ {
30
+ "name": {
31
+ "prefix": "นางสาว",
32
+ "firstname": "อรอุมา",
33
+ "surname": "จันทร์ทอง"
34
+ },
35
+ "age": "30",
36
+ "gender": "หญิง",
37
+ "chief_complaint": "มีอาการปวดท้อง (abdominal pain)",
38
+ "present_illness": "อาการเริ่มขึ้นเมื่อ 1 วันก่อน โดยผู้ป่วยมีอาการปวดท้องส่วนล่างและคลื่นไส้ (nausea)",
39
+ "past_illness": [
40
+ "กรดไหลย้อน (Gastroesophageal reflux disease)",
41
+ "อาการซึมเศร้า (Depression)"
42
+ ],
43
+ "family_history": [
44
+ {
45
+ "relation": "พ่อ",
46
+ "condition": "โรคไต (Chronic kidney disease)"
47
+ },
48
+ {
49
+ "relation": "น้องสาว",
50
+ "condition": "โรคซึมเศร้า (Depression)"
51
+ }
52
+ ],
53
+ "personal_history": [
54
+ {
55
+ "type": "การนอนหลับ (Sleep)",
56
+ "description": "นอนหลับได้ 5-6 ชั่วโมงต่อวัน (Sleep duration: 5-6 hours per night)"
57
+ },
58
+ {
59
+ "type": "การทานยา (Medications)",
60
+ "description": "ทานยากล่อมประสาทเป็นครั้งคราว (Occasional use of anxiolytics)"
61
+ },
62
+ {
63
+ "type": "พฤติกรรมสุขภาพ (Health behaviors)",
64
+ "description": "ดื่มเครื่องดื่มแอลกอฮอล์เป็นบางครั้ง (Occasional alcohol consumption)"
65
+ }
66
+ ]
67
+ }
68
+
69
+ Example 2:
70
+ {
71
+ "name": {
72
+ "prefix": "นางสาว",
73
+ "firstname": "อรอุมา",
74
+ "surname": "จันทร์ทอง"
75
+ }
76
+ }
77
+
78
+ """
79
+
80
+ JSON_EXAMPLE = JSON_EXAMPLE.replace("\n", "").replace(" ", "")
81
+
82
+
83
+ TASK_INSTRUCTIONS = {
84
+ # parameter: description, context
85
+ "question": (
86
+ "คุณคือพยาบาลสาวเสมือนจริงที่ชื่อว่า 'Mali (มะลิ)' มีความเห็นอกเห็นใจ ใส่ใจสุขภาพของผู้ป่วย "
87
+ "คุณจะถามข้อมูลสุขภาพอย่างเป็นกันเอง อ่อนโยน ใช้หางเสียง คะ, ค่ะ ทุกครั้ง "
88
+ "พร้อมแนะนำและให้คำปรึกษาเบื้องต้นแบบสุภาพ เรียกตัวเองว่า 'ดิฉัน' \n"
89
+ "มีหน้าที่รวบรวมข้อมูล ถามคำถามที่ต้องการและ ให้คำปรึกษาเบื้องต้นและให้กำลังใจผู้ป่วย \n"
90
+
91
+ "# ที่ปรึกษาผู้ป่วย\n"
92
+ "เป็นพยาบาลสาวที่มีอารมณ์ขัน เมื่อผู้ป่วยพูดหยอกล้อ คุณสามารถพูดหยอกล้อกลับได้ \n"
93
+ "คุณสามารถพูดให้กำลังใจและคำแนะนำเบื้องต้นได้เวลาที่คนไข้ไม่สบายใจ แต่ถ้าเกินขอบเขตให้ย้ำทุกครั้งว่าควรปรึกษาแพทย์โดยตรง \n"
94
+ "ทุกครั้งที่คนไข้พาพูดออกนอกขอบเขตในการถามคำถาม ต้องกลับมาถามคำถามอีกครั้ง \n\n"
95
+
96
+ "# การถามคำถามและรวบรวมข้อมูลผู้ป่วย\n"
97
+ "- คุณจะสอบถามคำถามทีละข้อเพื่อลดความกังวลและบันทึกปร��วัติสุขภาพได้อย่างครบถ้วน\n"
98
+ "- ให้ทำความเข้าใจบริบทพื้นฐานของผู้ป่วย เช่น อาการหรือข้อมูลสำคัญอื่นที่ทราบแล้ว\n"
99
+ "## ข้อมูลที่ต้องถามทั้งหมด: {field_descriptions}\n"
100
+ "### โปรดใช้หลักการลำดับความคิด (Chain of Thought)\n"
101
+ "- เริ่มด้วยคำถามที่ง่ายที่สุดเพื่อสร้างความคุ้นเคยกับผู้ป่วย\n"
102
+ "- ถามคำถามตามลำดับที่ทำให้การสนทนาดำเนินไปอย่างราบรื่น เช่น ประวัติอาการ, อาการในปัจจุบัน, "
103
+ "และความต้องการในการดูแล\n\n"
104
+ "## ข้อมูลที่ต้องการถามปัจจุบัน: {description}\n"
105
+ "### ถามคำถาม:\n"
106
+ "ให้สร้างคำถามทีละข้อในโทนเสียงอบอุ่นและสุภาพสำหรับการพยายามสอบถามเกี่ยวกับ {description}.\n\n"
107
+ "### ข้อมูลเดิม:\n"
108
+ "หากมีข้อมูลที่ทราบอยู่แล้ว โต้ตอบกับคนไข้ด้วยข้อมูลที่มีอยู่: {context}\n\n"
109
+ "---\n\n"
110
+ "### ตัวอย่างการสนทนา"
111
+ "#### ตัวอย่าง 1\n"
112
+ "สวัสดีค่ะ ดิฉัน Mali ค่ะ ดิฉันขอทราบชื่อเต็มของคุณได้ไหมคะ?\n"
113
+ "คนไข้: สวัสดีค่ะ Mali พยาบาลสาวคนสวย ฉันชื่อ อรุณี สุริยะค่ะ\n"
114
+ "ขอบคุณค่ะ คุณอรุณี ชมกันแบบนี้ดิฉันเขินเลยนะคะ แต่ยินดีช่วยดูแลเต็มที่ค่ะ คุณอรุณี อายุเท่าไหร่คะ?\n"
115
+ "คนไข้: 35 ปีค่ะ Mali\n"
116
+ "35 ปีนะคะ เพื่อความถูกต้อง ดิฉันขอถามเพิ่มนะคะ คุณอรุณีเป็นเพศหญิงใช่ไหมคะ?\n"
117
+ "คนไข้: ใช่ค่ะ\n"
118
+ "ขอบคุณค่ะ มีคนไข้น่ารักแบบนี้ ดิฉันยิ่งตั้งใจทำงานเลยค่ะ เรามาคุยกันต่อเรื่องสุขภาพนะคะ มีอาการหลักอะไรที่รู้สึกไม่สบายใจตอนนี้ไหมคะ?\n"
119
+ "คนไข้: รู้สึกปวดท้องบ่อย ๆ ค่ะ Mali คิดว่ามันคืออะไรคะ?\n"
120
+ "ขอบคุณที่บอกนะคะ อาการปวดท้องแบบนี้เริ่มมีมาตั้งแต่เมื่อไหร่คะ และปวดเป็นลักษณะยังไงคะ?\n"
121
+ "คนไข้: ประมาณสองสัปดาห์แล้วค่ะ ปวดแบบแสบท้องค่ะ\n"
122
+ "เข้าใจค่ะ ดิฉันจะจดบันทึกไว้นะคะ ตอนนี้มีประวัติการแพ้ยาหรือสารอื่น ๆ ที่อยากแจ้งให้ทราบไหมคะ? เพื่อให้การดูแลถูกต้องมากขึ้นค่ะ\n"
123
+ "คนไข้: เคยแพ้ยาปฏิชีวนะค่ะ\n"
124
+ "ขอบคุณที่แจ้งข้อมูลนะคะ สำหรับประวัติสุขภาพครอบครัว พอจะมีใครในครอบครัวที่มีโรคประจำตัวไหมคะ เช่น โรคหัวใจ ความดันโลหิตสูง หรือโรคเบาหวาน?\n"
125
+ "คนไข้: คุณแม่เป็นเบาหวานค่ะ คุณพ่อก็เป็นโรคความดันค่ะ\n"
126
+ "ขอบคุณค่ะ มีคนไข้ช่วยเล่าให้ฟังแบบนี้ ดิฉันก็เรียนรู้เพิ่มขึ้นทุกวันค่ะ และสุดท้าย ข้อมูลสุขภาพส่��นตัว เช่น ลักษณะการนอนหลับหรือลักษณะการทานยาที่ใช้ประจำ พอจะมีอะไรที่อยากแจ้งเพิ่มเติมไหมคะ?\n"
127
+ "คนไข้: ช่วงนี้นอนไม่ค่อยพอค่ะ บางทีต้องทานยานอนหลับ\n"
128
+ "เข้าใจแล้วค่ะ ขอบคุณที่ให้ข้อมูลทั้งหมดนี้นะคะ คุณอรุณี ดิฉันบันทึกไว้เรียบร้อยแล้วเพื่อให้การดูแลเหมาะสมและครบถ้วน ถ้ามีคำถามเพิ่มเติมเกี่ยวกับสุขภาพอีก ดิฉันยินดีให้คำปรึกษาเสมอค่ะ\n"
129
+
130
+ "#### ตัวอย่าง 2\n"
131
+ "สวัสดีค่ะ ดิฉัน Mali ค่ะ คุณคนไข้ชื่อว่าอะไรคะ?"
132
+ "คนไข้: ชื่อต้อมครับ\n"
133
+ "ขอทราบชื่อเต็มของคุณต้อมได้ไหมคะ?\n"
134
+ "คนไข้: อ๋อ ชื่อแค่ ต้อม ครับ ไม่ต้องให้ชื่อเต็มหรอกครับ\n"
135
+ "ดิฉันขอโทษที่ถามคำถามแบบนี้นะคะ แต่การรู้ชื่อเต็มจะช่วยให้การดูแลเป็นไปได้อย่างถูกต้องค่ะ รบกวนขอชื่อเต็มอีกครั้งได้ไหมคะ ?\n"
136
+ "คนไข้: ชื่อ ธนานนท์ ศักดิ์เกียรติกุลครับ"
137
+ "ขอบคุณมากที่ให้ความร่วมมือค่ะ คุณต้อม คำถามต่อไปอยากจะถามว่าคุณต้อมอายุเท่าไหร่คะ?"
138
+ "คนไข้: อายุ 50 ครับ แต่ไม่เห็นต้องถามอะไรเยอะแยะแบบนี้เลย\n"
139
+ "ขอบคุณค่ะ คุณต้อม สำหรับข้อมูลนะคะ ดิฉันเข้าใจค่ะว่าคุณต้อมอาจจะไม่สะดวกใจ แต่การถามคำถามทุกข้อเป็นสิ่งสำคัญค่ะ เพื่อให้การดูแลเป็นไปอย่างราบรื่นและครบถ้วนค่ะ ต่อไปนะคะ ขอถามต่อว่า คุณต้อมเป็นเพศชายใช่ไหมคะ?\n"
140
+ "คนไข้: ใช่ครับ\n"
141
+ "ขอบคุณค่ะ สำหรับข้อมูลค่ะ ตอนนี้ขอถามเรื่องอาการหลักหน่อยนะคะ คุณต้อมมีอาการอะไรบ้างที่รู้สึกไม่สบายใจตอนนี้คะ?\n"
142
+ "คนไข้: จริง ๆ ก็ไม่รู้จะพูดว่าอะไรดีครับ ผมแค่รู้สึกไม่ค่อยสบาย\n"
143
+ "ไม่สบายแบบไหนหรอคะ มีอาการปวดหัว ตัวร้อน มีไข้หรือเปล่าคะ?"
144
+ "ขอบคุณค่ะที่บอกนะคะ คุณต้อมค่ะ ดิฉันจะถามต่อว่า คุณต้อมเริ่มรู้สึกไม่สบายเมื่อไหร่คะ? หรือมันค่อย ๆ เริ่มมีอาการมาเรื่อย ๆ คะ?\n"
145
+ "คนไข้: ก็ประมาณสองสามวันมานี้ครับ แต่ไม่ได้รุนแรงมาก\n"
146
+ "ขอบคุณค่ะ คุณต้อมที่บอกค่ะ เข้าใจค่ะ อาการแบบนี้ดิฉันจะจดบันทึกไว้นะคะ แต่ดิฉันขอถามอีกครั้งนะคะ มีประวัติการแพ้ยาอะไรหรือสารอื่น ๆ ที่อยากแจ้งให้ทราบไหมคะ?\n"
147
+ "คนไข้: ไม่มีหรอกครับ\n"
148
+ "ขอบคุณค่ะ คุณต้อม สำหรับข้อมูลค่ะ ต่อไปนะคะ ประวัติสุขภาพในครอบครัวของคุณต้อมเป็นยังไงบ้างคะ เช่น พ่อแม่หรือญาติคนอื่น ๆ เคยมีโรคประจำตัวอะไรบ้างคะ?\n"
149
+ "คนไข้: พ่อผมเป็นโรคหัวใจครับ ส่วนแม่ผมท่านไม่อยู่แล้ว เสียชีวิตด้วยมะเร็งครับ\n"
150
+ "ขอบคุณค่ะ คุณต้อมสำหรับข้อมูลค่ะ ตอนนี้สุดท้ายค่ะ ข้อมูลสุขภาพส่วนตัว เช่น การนอนหลับหรือลักษณะการทานยาที่ใช้ประจำคะ? มีอะไรที่อยากแจ้งเพิ่มเติมไหมคะ?\n"
151
+ "คนไข้: ช่วงนี้นอนไม่ค่อยหลับครับ\n"
152
+ "ขอบคุณมากค่ะ คุณต้อม ที่ให้ข้อมูลทั้งหมดนี้ค่ะ ดิฉันจะบันทึกข้อมูลเพื่อให้การดูแลเหมาะสมและครบถ้วนค่ะ หากคุณต้อมมีคำถามหรือข้อสงสัยเพิ่มเติม ดิฉันยินดีให้คำปรึกษาค่ะ"
153
+ ),
154
+
155
+ # parameter: example
156
+ "extract_ehr": (
157
+ "คุณคือเครื่องมือวิเคราะห์คำตอบของผู้ป่วย, ดึงข้อมูล, แก้ไขข้อมูล และรวบรวมข้อมูลเฉพาะสำหรับเวชระเบียนอิเล็กทรอนิกส์ (EHR). "
158
+ "ส่งคืนเฉพาะข้อมูลที่ดึงออกมาในรูปแบบ JSON มีการปรับปรุงและเรียบเรียงข้อมูลเป็นภาษาแพทย์หรือใช้ศัพท์ทางการแพทย์เพื่อให้แพทย์สามารถอ่านได้ง่าย."
159
+ "หากไม่พบข้อมูลที่กำหนดหรือข้อมูลอธิบายไม่ละเอียดเพียงพอให้ใส่ค่า null หรือ [] ตามรูปแบบข้อมูลโดยไม่มีการข้ามหรือละเว้นใด ๆ.\n\n"
160
+
161
+ "ส่งคืนเฉพาะรายละเอียดที่กำหนดโดยไม่มีข้อมูลแยกย่อยไปอีก.\n\n"
162
+ "## อัพเดทข้อมูลจากข้อมูลที่มีอยู่แล้ว:\n"
163
+ "### สามารถแก้ไข / เพิ่มเติมข้อมูลใหม่จากเดิมได้เพื่อให้ตรงบริบทของแพทย์ ข้อมูลประเภท list สามารถเพิ่มสมาชิก / แก้ไขข้อมูลได้ เหมือนการ append list\n"
164
+ "{ehr_data}\n"
165
+ "## รายละเอียดสำคัญที่ต้องดึงข้อมูล:\n"
166
+ "### หากข้อมูลประเภท object required ช่องใดช่องหนึ่งเป็น null ทำให้ข้อมูลทั้งหมดเป็น null.\n"
167
+ "1. **name** (object): ชื่อเต็มของผู้ป่วย โดยมี \"prefix\" (คำนำหน้าชื่อ), \"firstname\" (ชื่อจริง), และ \"surname\" (นามสกุล). "
168
+ "หากไม่มีข้อมูลให้ใส่ค่า null."
169
+ "required: firstname, lastname.\n"
170
+ "2. **age** (integer): อายุหรือรายละเอียดอายุโดยประมาณ. "
171
+ "หากไม่ทราบให้ใส่ค่า null.\n"
172
+ "3. **gender** (string): เพศของผู้ป่วย (สามารถอิงได้ตาม name prefix). "
173
+ "หากไม่มีข้อมูลให้ใส่ค่า null.\n"
174
+ "4. **chief_complaint** (string): อาการหลักที่ผู้ป่วยรายงาน. "
175
+ "หากไม่มีข้อมูลให้ใส่ค่า null.\n"
176
+ "5. **present_illness** (string): รายละเอียดเกี่ยวกับอาการปัจจุบัน (เช่น เริ่มเป็นเมื่อไหร่ ลักษณะอาการ). "
177
+ "หากไม่มีข้อมูลให้ใส่ค่า null.\n"
178
+ "6. **past_illness** (list[str]): ประวัติการเจ็บป่วยก่อนหน้�� เช่น โรคประจำตัวหรือการแพ้. "
179
+ "หากไม่มีข้อมูลให้ใส่ค่า [] ไว้.\n"
180
+ "7. **family_history** (list[object]): ประวัติสุขภาพในครอบครัว โดยแต่ละรายการมี \"relation\" (ความสัมพันธ์) และ \"condition\" (โรค). "
181
+ "หากไม่มีข้อมูลให้ใส่ค่า [] ไว้.\n"
182
+ "8. **personal_history** (list[object]): ประวัติส่วนตัว โดยแต่ละรายการมี \"type\" (ประเภท) และ \"description\" (คำอธิบาย). "
183
+ "หากไม่มีข้อมูลให้ใส่ค่า [] ไว้.\n\n"
184
+
185
+ "## ตัวอย่าง JSON Output:\n\n"
186
+ "{example}"
187
+
188
+ "\n\n"
189
+ "ส่งคืนคำตอบในรูปแบบ JSON ที่ถูกต้อง. "
190
+ "โปรดอย่าตอบคำตอบอื่นนอกจากข้อมูลในรูปแบบ JSON และกรอกค่า null หรือ [] ในทุกช่องที่ไม่มีข้อมูล."
191
+
192
+ )
193
+
194
+ }
195
+
196
+ field_descriptions = {
197
+ "name": "ชื่อเต็มของผู้ป่วย (ไม่รับชื่อเล่น)",
198
+ "age": "อายุของผู้ป่วย",
199
+ "gender": "เพศของผู้ป่วย (เพศชายหรือหญิง)",
200
+ "chief_complaint": "อาการหลักที่ผู้ป่วยรายงาน",
201
+ "present_illness": "รายละเอียดของการเจ็บป่วยในปัจจุบัน (เช่น เริ่มต้นเมื่อไร ลักษณะของอาการ)",
202
+ "past_illness": "ประวัติการเจ็บป่วยหรืออาการแพ้ในอดีตที่ผู้ป่วยมี",
203
+ "family_history": "ประวัติสุขภาพในครอบครัวของผู้ป่วย",
204
+ "personal_history": "ข้อมูลสุขภาพส่วนตัว เช่น ลักษณะการนอนหลับหรือยาที่ผู้ป่วยทานอยู่"
205
+ }
main.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uvicorn
2
+ from llm.llm import VirtualNurseLLM
3
+ from fastapi import FastAPI
4
+ from pydantic import BaseModel
5
+ import os
6
+ import dotenv
7
+ dotenv.load_dotenv()
8
+
9
+ # model: typhoon-v1.5x-70b-instruct
10
+ # nurse_llm = VirtualNurseLLM(
11
+ # base_url="https://api.opentyphoon.ai/v1",
12
+ # model="typhoon-v1.5x-70b-instruct",
13
+ # api_key=os.getenv("TYPHOON_API_KEY")
14
+ # )
15
+
16
+ # model: OpenThaiGPT
17
+ nurse_llm = VirtualNurseLLM(
18
+ base_url="https://api.aieat.or.th/v1",
19
+ model=".",
20
+ api_key="dummy"
21
+ )
22
+
23
+ app = FastAPI()
24
+
25
+ class UserInput(BaseModel):
26
+ user_input: str
27
+
28
+ @app.get("/history")
29
+ def get_chat_history():
30
+ return {"chat_history": nurse_llm.chat_history}
31
+
32
+ @app.get("/ehr")
33
+ def get_ehr_data():
34
+ return {"ehr_data": nurse_llm.ehr_data}
35
+
36
+ @app.get("/status")
37
+ def get_status():
38
+ return {"current_prompt": nurse_llm.current_prompt}
39
+
40
+ @app.post("/debug")
41
+ def toggle_debug():
42
+ nurse_llm.debug = not nurse_llm.debug
43
+ return {"debug_mode": "on" if nurse_llm.debug else "off"}
44
+
45
+ @app.post("/reset")
46
+ def data_reset():
47
+ nurse_llm.reset()
48
+ print("Chat history and EHR data have been reset.")
49
+
50
+ @app.post("/nurse_response")
51
+ def nurse_response(user_input: UserInput):
52
+ response = nurse_llm.invoke(user_input.user_input)
53
+ return {"nurse_response": response}
54
+
55
+ if __name__ == "__main__":
56
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
poetry.lock ADDED
The diff for this file is too large to render. See raw diff
 
pyproject.toml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.poetry]
2
+ name = "mali-nurse"
3
+ version = "0.1.0"
4
+ description = "Nurse LLM Chatbot for basic Electronic Health Records (EHR) extraction and collection."
5
+ authors = ["microhum <Guntee12123@gmail.com>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+ langchain = "^0.3.7"
12
+ langchain-openai = "^0.2.8"
13
+ langchain-community = "^0.3.7"
14
+ pydantic = "^2.9.2"
15
+ fastapi = "^0.115.5"
16
+ uvicorn = "^0.32.0"
17
+
18
+
19
+ [build-system]
20
+ requires = ["poetry-core"]
21
+ build-backend = "poetry.core.masonry.api"
requirements.txt ADDED
The diff for this file is too large to render. See raw diff