JDFPalladium commited on
Commit
5486ae5
·
1 Parent(s): 6e5b890

adding main.py with multiple tools to invoke

Browse files
Files changed (6) hide show
  1. README.md +6 -1
  2. chat.ipynb +196 -15
  3. chatlib/patient_sql_agent.py +45 -9
  4. chatlib/state_types.py +2 -1
  5. main.py +40 -7
  6. sql_agent.ipynb +34 -22
README.md CHANGED
@@ -1 +1,6 @@
1
- # clinician-assistant-lg
 
 
 
 
 
 
1
+ # clinician-assistant-lg
2
+
3
+ curl -fsSL https://ollama.com/install.sh | sh
4
+ ollama pull llama3.1:8b
5
+ ollama serve
6
+ ollama run llama3
chat.ipynb CHANGED
@@ -20,7 +20,7 @@
20
  },
21
  {
22
  "cell_type": "code",
23
- "execution_count": null,
24
  "id": "73bd3df7",
25
  "metadata": {},
26
  "outputs": [
@@ -56,13 +56,15 @@
56
  " return \"RAG search results for: \" + retrieved_text\n",
57
  "\n",
58
  "tools = [rag_retrieve]\n",
59
- "llm_lc = ChatOpenAI(temperature = 0.0, model=\"gpt-4o\")\n",
 
 
60
  "llm_with_tools = llm_lc.bind_tools([rag_retrieve])"
61
  ]
62
  },
63
  {
64
  "cell_type": "code",
65
- "execution_count": 11,
66
  "id": "2cb76d17",
67
  "metadata": {},
68
  "outputs": [],
@@ -84,7 +86,7 @@
84
  },
85
  {
86
  "cell_type": "code",
87
- "execution_count": 14,
88
  "id": "e561b005",
89
  "metadata": {},
90
  "outputs": [
@@ -131,27 +133,206 @@
131
  },
132
  {
133
  "cell_type": "code",
134
- "execution_count": 1,
135
  "id": "01fd23c5",
136
  "metadata": {},
137
  "outputs": [
138
  {
139
- "ename": "NameError",
140
- "evalue": "name 'HumanMessage' is not defined",
141
- "output_type": "error",
142
- "traceback": [
143
- "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
144
- "\u001b[31mNameError\u001b[39m Traceback (most recent call last)",
145
- "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 4\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# Specify a thread\u001b[39;00m\n\u001b[32m 2\u001b[39m config = {\u001b[33m\"\u001b[39m\u001b[33mconfigurable\u001b[39m\u001b[33m\"\u001b[39m: {\u001b[33m\"\u001b[39m\u001b[33mthread_id\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33m1\u001b[39m\u001b[33m\"\u001b[39m}}\n\u001b[32m----> \u001b[39m\u001b[32m4\u001b[39m messages = [\u001b[43mHumanMessage\u001b[49m(content=\u001b[33m\"\u001b[39m\u001b[33mwhat if the patient is pregnant and just found out they have HIV?\u001b[39m\u001b[33m\"\u001b[39m)]\n\u001b[32m 5\u001b[39m messages = react_graph.invoke({\u001b[33m\"\u001b[39m\u001b[33mmessages\u001b[39m\u001b[33m\"\u001b[39m: messages}, config)\n\u001b[32m 6\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m m \u001b[38;5;129;01min\u001b[39;00m messages[\u001b[33m'\u001b[39m\u001b[33mmessages\u001b[39m\u001b[33m'\u001b[39m]:\n",
146
- "\u001b[31mNameError\u001b[39m: name 'HumanMessage' is not defined"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  ]
148
  }
149
  ],
150
  "source": [
151
  "# Specify a thread\n",
152
- "config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
153
  "\n",
154
- "messages = [HumanMessage(content=\"what if the patient is pregnant and just found out they have HIV?\")]\n",
155
  "messages = react_graph.invoke({\"messages\": messages}, config)\n",
156
  "for m in messages['messages']:\n",
157
  " m.pretty_print()"
 
20
  },
21
  {
22
  "cell_type": "code",
23
+ "execution_count": 2,
24
  "id": "73bd3df7",
25
  "metadata": {},
26
  "outputs": [
 
56
  " return \"RAG search results for: \" + retrieved_text\n",
57
  "\n",
58
  "tools = [rag_retrieve]\n",
59
+ "# llm_lc = ChatOpenAI(temperature = 0.0, model=\"gpt-4o\")\n",
60
+ "from langchain_ollama import ChatOllama\n",
61
+ "llm_lc = ChatOllama(model=\"llama3.2:1b\")\n",
62
  "llm_with_tools = llm_lc.bind_tools([rag_retrieve])"
63
  ]
64
  },
65
  {
66
  "cell_type": "code",
67
+ "execution_count": 3,
68
  "id": "2cb76d17",
69
  "metadata": {},
70
  "outputs": [],
 
86
  },
87
  {
88
  "cell_type": "code",
89
+ "execution_count": 4,
90
  "id": "e561b005",
91
  "metadata": {},
92
  "outputs": [
 
133
  },
134
  {
135
  "cell_type": "code",
136
+ "execution_count": 5,
137
  "id": "01fd23c5",
138
  "metadata": {},
139
  "outputs": [
140
  {
141
+ "name": "stdout",
142
+ "output_type": "stream",
143
+ "text": [
144
+ "================================\u001b[1m Human Message \u001b[0m=================================\n",
145
+ "\n",
146
+ "when should viral loads be taken?\n",
147
+ "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
148
+ "Tool Calls:\n",
149
+ " rag_retrieve (d52d2a7a-e00d-4cb7-afff-4183e6859985)\n",
150
+ " Call ID: d52d2a7a-e00d-4cb7-afff-4183e6859985\n",
151
+ " Args:\n",
152
+ " user_prompt: When should viral loads be taken?\n",
153
+ "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
154
+ "Name: rag_retrieve\n",
155
+ "\n",
156
+ "RAG search results for: Source 1: discontinued \n",
157
+ "HIV Viral Load ● For PCR positive HEIs: baseline at the time of ART initiation \n",
158
+ "● Age 0 -24 years: at month 3, then every 6 months \n",
159
+ "● Age ≥ 25 years: at month 3, then month 12, then annually thereafter if \n",
160
+ "suppressed \n",
161
+ "● For all: before any drug substitution for patients on ART for a t least 6 \n",
162
+ "months with no valid VL, at month 3 after regimen modification, and \n",
163
+ "then as per population group \n",
164
+ "● Any patient with a detectable VL during routine monitoring, follow viral \n",
165
+ "load monitoring algorithm (Figure 6.6) \n",
166
+ "HIV Viral Load \n",
167
+ "(pregnant/ \n",
168
+ "breastfeeding) ● If on ART at time of confirming pregnancy: VL done at confirmation of \n",
169
+ "pregnancy (regardless of when previously done), then every 6 months \n",
170
+ "until complete cessation of breastfeeding \n",
171
+ "● If starting ART during pregnancy or breastfeeding, VL at 3 months after \n",
172
+ "initiation, and then every 6 months until complete cessation of \n",
173
+ "breastfeeding\n",
174
+ "\n",
175
+ "Source 2: Whenever possible, use same -day point -of-care methods for viral load \n",
176
+ "testing of pregnant and breastfeeding women to expedite the return of \n",
177
+ "results and clinical decision -making. If this is not available, viral load \n",
178
+ "specimens and results for pregnant and breastfeeding women should be \n",
179
+ "given priority across the laboratory referral process (including specimen \n",
180
+ "collection, testing and return of results). \n",
181
+ "● For pregnant and breastfeeding women newly initiated on ART, obtain VL 3 \n",
182
+ "months after initiation, and then every 6 months until complete cessation of \n",
183
+ "breastfeeding \n",
184
+ "● For HIV positive women already on ART at the time of confirming pregnancy \n",
185
+ "or breastfeeding, obtain a VL irrespective of when prior VL was done, and \n",
186
+ "then every 6 months until complete cessation of breastfeeding \n",
187
+ "● For pregnant or breastfeeding women with a VL ≥ 50 copies/ml: assess for \n",
188
+ "and address potential reasons for viremia, including intensifying adherence \n",
189
+ "support, repeat the VL after 3 months of excellent adherence, including \n",
190
+ "daily witnessed ingestion, where feasible and appropriate \n",
191
+ "o If the repeat VL is 200 - 999 copies/ml consul t the Regional or National \n",
192
+ "HIV Clinical TWG \n",
193
+ "o If the repeat VL is ≥ 1,000 copies/ml, change to an effective regimen. \n",
194
+ "Refer to Table 6.10 \n",
195
+ "o If the repeat VL is < 200 copies/ml (LDL) then continue routine \n",
196
+ "monitoring\n",
197
+ "\n",
198
+ "Source 3: Kenya HIV Prevention and Treatment Guidelines, 2022 \n",
199
+ "6 - 18 \n",
200
+ "Schedule for routine viral load testing1 \n",
201
+ "• Age 0 -24 years old: at month 3, then every 6 months \n",
202
+ "• Age years old: at month 3, then month 12 and then annually \n",
203
+ "• Pregnant or breastfeeding: at confirmation of pregnancy (if already on ART) or 3 months after ART initiation (if ART initiate d during \n",
204
+ "pregnancy/breastfeeding), and then every 6 months until complete cessation of breastfeeding \n",
205
+ "• Before any drug substitution (if no VL result available from the prior 6 months) \n",
206
+ "• Three months after any regimen modification (including single -drug substitutions) \n",
207
+ "VL < 200 copies/ml\n",
208
+ " VL 200 – 999 copies/ml\n",
209
+ " VL copies/ml\n",
210
+ "Increased risk of progression to \n",
211
+ "treatment failure\n",
212
+ "Suspected treatment \n",
213
+ "failure\n",
214
+ "• Discuss patient in MDT \n",
215
+ "• Assign a case manager \n",
216
+ "• Assess for and address likely causes of non -adherence2 \n",
217
+ "• Provide enhanced adherence support/intervention as appropriate (Section \n",
218
+ "5.4 of guidelines for enhanced adherence protocol) \n",
219
+ "• Assess for other causes of viremia and manage as needed3 \n",
220
+ "• Support daily witnessed ingestion by treatment buddy or healthcare worker\n",
221
+ "• After 3 months of excellent adherence, repeat VL \n",
222
+ "VL < 200 copies/ml (LDL)\n",
223
+ "VL 200 – 999 copies/ml\n",
224
+ "VL copies/ml\n",
225
+ "• Continue ART regimen\n",
226
+ "• Routine adherence \n",
227
+ "counselling and monitoring\n",
228
+ "• Routine VL monitoring\n",
229
+ "• Reassess adherence and other \n",
230
+ "causes of viremia2,3\n",
231
+ "• Repeat VL after another 3 months of \n",
232
+ "excellent adherence\n",
233
+ "Confirms treatment failure: \n",
234
+ "• Begin treatment preparation for new regimen and \n",
235
+ "continue failing regimen until adherence \n",
236
+ "preparation completed \n",
237
+ "• Continue enhanced adherence support \n",
238
+ "• Take sample for CD4 count and assess for and \n",
239
+ "manage any OIs \n",
240
+ "• If failing a DTG or PI based regimen a DRT is \n",
241
+ "recommended in consult ation with the regional or \n",
242
+ "National HIV Clinical TWG or call Uliza Hotline \n",
243
+ " (0726 460 000) \n",
244
+ "• Schedule clinical appointment at 2 weeks after\n",
245
+ "\n",
246
+ "Source 4: Annexes \n",
247
+ " \n",
248
+ "13 - 9 Annex 8: Cont. \n",
249
+ "Section 3: Viral load \n",
250
+ "• What is viral load \n",
251
+ "- Viral load is the amount of HIV in your body \n",
252
+ "- When your viral load is high it means you have a lot of HIV in your body; this causes damage to \n",
253
+ "your body \n",
254
+ "- Viral load is measured by a blood test \n",
255
+ " \n",
256
+ "• How often is viral load measured \n",
257
+ "- Viral load is measured after being on treatment for 3 months \n",
258
+ "- After 3 months of treatment, we expect the amount of virus in your body to be undetectable; if \n",
259
+ "your VL is detectable then we have to discuss the reasons \n",
260
+ "- Having an “undetectable” VL means the test cannot measure the virus in your blood because \n",
261
+ "your ART is working, but it does not mean you are no longer infected with HIV \n",
262
+ "- Repeat viral load tests are done dependin g on how you are doing; if you are doing well on \n",
263
+ "treatment then the viral load is measured again every 6 months (for children/adolescents and \n",
264
+ "pregnant/breastfeeding) or annually \n",
265
+ "- For HEI with positive PCR, we also measure viral load at the start of treatmen t \n",
266
+ " \n",
267
+ "• What do viral load measurements mean \n",
268
+ "- After being on treatment for 3 or more months, your viral load should be undetectable \n",
269
+ "- If your viral load is undetectable, it means your treatment is working well and you should \n",
270
+ "continue taking it the same; the virus is not damaging your body any more \n",
271
+ "- If your viral load is detectable, it means your treatment is not working properly, usually because \n",
272
+ "you have been missing some of your pills; the virus is damaging your body and you and the clinic \n",
273
+ "team will need to work together to figure out how to fix the problem \n",
274
+ "Section 4: CD4 cells \n",
275
+ "• What are CD4 cells \n",
276
+ "- CD4 cells are the immune cells that protect the body from infections \n",
277
+ "- CD4 cells are measured through a blood test, called CD4 count. For adults a normal CD4 count is \n",
278
+ "above 500 \n",
279
+ " \n",
280
+ "• How are CD4 cells affected by HIV \n",
281
+ "- HIV attacks and destroys CD4 cells\n",
282
+ "\n",
283
+ "Source 5: Kenya HIV Prevention and Treatment Guidelines, 2022 \n",
284
+ " \n",
285
+ " 13 - 20 Annex 9A: Cont. \n",
286
+ "Session 3 (usually 2 weeks after Session 2, preferably with the same provider) \n",
287
+ "Review Adherence Plan \n",
288
+ "• Ask the patient if he/she thinks adherence has improved since the last visit. Enquire in a \n",
289
+ "friendly way if any doses have been missed \n",
290
+ "• Review the patient’s barriers to adherence documented during the first session and if \n",
291
+ "strategies identified have been taken up. If not, discuss why \n",
292
+ " \n",
293
+ "Identify Any New Issues \n",
294
+ "• Discuss specific reasons why the patient may have missed their pills or a clinic appointment \n",
295
+ "since the last counselling session, and determine if it is a new issue that wasn’t addressed \n",
296
+ "during the first session \n",
297
+ "• Discuss if other issues have come up because of implementing the adherence plan (e.g., perhaps \n",
298
+ "the disclosure process had unintended results) \n",
299
+ " \n",
300
+ "Referrals and Networking \n",
301
+ "• Follow -up on any referrals made during the previous session \n",
302
+ "• Determine if the patient could benefit from a home visit \n",
303
+ " \n",
304
+ "Develop Adherence Plan \n",
305
+ "• Go through each of the adherence challenges identified during the session and assist the patient \n",
306
+ "to modify their original adherence plan to address each of the issues. It is important to let the \n",
307
+ "patient come up with the solutions so that they own them \n",
308
+ "• Give another short motivati onal speech on how you believe in the patient! You know they \n",
309
+ "can do this! Together you will make sure that they suppress their viral load!! \n",
310
+ "• Agree on a follow -up date for the next session \n",
311
+ " \n",
312
+ "Repeat Viral Load \n",
313
+ "• If the adherence is good: plan for the next VL testing after 3 months and explain possible ways \n",
314
+ "forward, emphasizing the roles of the patient, the support systems and the health facility. You \n",
315
+ "can continue follow -up adherence counselling sessions during the 3 -month period if you and \n",
316
+ "the patient think th ere would be a benefit to them \n",
317
+ "“If your results come back and your VL is undetectable then you will be able to continue with same ART. \n",
318
+ "If your viral load is still greater than 1,000 copies/ml then you will need to switch to a new regimen, \n",
319
+ "probably after do ing some additional testing to see which regimen\n",
320
+ "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
321
+ "\n",
322
+ "Based on the original question \"When should viral loads be taken?\", I would answer:\n",
323
+ "\n",
324
+ "* For HEIs (HIV positive individuals) who are newly initiated on ART: Viral load should be taken at 3 months after initiation.\n",
325
+ "* If ART is started during pregnancy or breastfeeding, viral load testing should be done at 3 months after initiation, and then every 6 months until complete cessation of breastfeeding.\n",
326
+ "\n",
327
+ "These guidelines recommend taking viral loads at 3 months after ART initiation for newly initiated HEIs, as well as at 3 months if the ART starts during pregnancy or breastfeeding.\n"
328
  ]
329
  }
330
  ],
331
  "source": [
332
  "# Specify a thread\n",
333
+ "config = {\"configurable\": {\"thread_id\": \"100\"}}\n",
334
  "\n",
335
+ "messages = [HumanMessage(content=\"when should viral loads be taken?\")]\n",
336
  "messages = react_graph.invoke({\"messages\": messages}, config)\n",
337
  "for m in messages['messages']:\n",
338
  " m.pretty_print()"
chatlib/patient_sql_agent.py CHANGED
@@ -4,8 +4,8 @@ from langchain_core.prompts import ChatPromptTemplate
4
  from langchain_core.tools import tool
5
  from langchain_openai import ChatOpenAI
6
  from typing_extensions import TypedDict, Annotated
7
-
8
  from .state_types import State
 
9
  db = SQLDatabase.from_uri("sqlite:///data/patient_demonstration.sqlite")
10
  llm = ChatOpenAI(temperature = 0.0, model="gpt-4o")
11
 
@@ -14,8 +14,9 @@ system_message = """
14
  Given an input question, create a syntactically correct {dialect} query to
15
  run to help find the answer. Unless the user specifies in his question a
16
  specific number of examples they wish to obtain, always limit your query to
17
- at most {top_k} results. You can order the results by a relevant column to
18
- return the most interesting examples in the database.
 
19
 
20
  Never query for all the columns from a specific table, only ask for a the
21
  few relevant columns given the question.
@@ -24,6 +25,31 @@ Pay attention to use only the column names that you can see in the schema
24
  description. Be careful to not query for columns that do not exist. Also,
25
  pay attention to which column is in which table.
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  Only use the following tables:
28
  {table_info}
29
  """
@@ -40,7 +66,7 @@ class QueryOutput(TypedDict):
40
  query: Annotated[str, ..., "Syntactically valid SQL query."]
41
 
42
 
43
- def write_query(state: State) -> State:
44
  """Generate SQL query to fetch information."""
45
  prompt = query_prompt_template.invoke(
46
  {
@@ -48,32 +74,42 @@ def write_query(state: State) -> State:
48
  "top_k": 10,
49
  "table_info": db.get_table_info(),
50
  "input": state["question"],
 
51
  }
52
  )
 
53
  structured_llm = llm.with_structured_output(QueryOutput)
54
  result = structured_llm.invoke(prompt)
55
  return {**state, "query": result["query"]}
56
 
57
- def execute_query(state: State) -> State:
58
  """Execute SQL query."""
59
  execute_query_tool = QuerySQLDatabaseTool(db=db)
60
  return {**state, "result": execute_query_tool.invoke(state["query"])}
61
 
62
- def generate_answer(state: State) -> State:
63
- """Answer question using retrieved information as context."""
 
 
 
 
 
 
 
 
64
  prompt = (
65
  "Given the following user question, corresponding SQL query, "
66
  "and SQL result, answer the user question.\n\n"
67
  f'Question: {state["question"]}\n'
68
  f'SQL Query: {state["query"]}\n'
69
- f'SQL Result: {state["result"]}'
70
  )
71
  response = llm.invoke(prompt)
72
  return {**state, "answer": response.content}
73
 
74
  # now define a stateful tool that does the same thing
75
  @tool
76
- def sql_chain(state: State) -> State:
77
  """
78
  Annotated function that takes a question string seeking information on patient data
79
  from a SQL database, writes an SQL query to retrieve relevant data, executes the query,
 
4
  from langchain_core.tools import tool
5
  from langchain_openai import ChatOpenAI
6
  from typing_extensions import TypedDict, Annotated
 
7
  from .state_types import State
8
+
9
  db = SQLDatabase.from_uri("sqlite:///data/patient_demonstration.sqlite")
10
  llm = ChatOpenAI(temperature = 0.0, model="gpt-4o")
11
 
 
14
  Given an input question, create a syntactically correct {dialect} query to
15
  run to help find the answer. Unless the user specifies in his question a
16
  specific number of examples they wish to obtain, always limit your query to
17
+ at most {top_k} results. For questions about specific patients, filter the
18
+ PatientPKHash column using exactly the provided value: {pk_hash}. If questions
19
+ are about all patients or not about a specific patient, do not filter.
20
 
21
  Never query for all the columns from a specific table, only ask for a the
22
  few relevant columns given the question.
 
25
  description. Be careful to not query for columns that do not exist. Also,
26
  pay attention to which column is in which table.
27
 
28
+ When checking if a patient was late for an appointment, for each visit, compare the NextAppointmentDate from the previous visit to the VisitDate of the current visit.
29
+ Do not compare NextAppointmentDate to the VisitDate in the same row. Use SQL to find, for each patient, the next VisitDate after a given VisitDate, and compare it to the NextAppointmentDate from the previous visit.
30
+
31
+ Here is an example of how to do this in SQL:
32
+ SELECT
33
+ v1.PatientPKHash,
34
+ v1.VisitDate AS PreviousVisitDate,
35
+ v1.NextAppointmentDate,
36
+ v2.VisitDate AS NextVisitDate,
37
+ CASE
38
+ WHEN v2.VisitDate <= v1.NextAppointmentDate THEN 'On time'
39
+ ELSE 'Late'
40
+ END AS AttendanceStatus
41
+ FROM clinical_visits v1
42
+ JOIN clinical_visits v2
43
+ ON v1.PatientPKHash = v2.PatientPKHash
44
+ AND v2.VisitDate > v1.VisitDate
45
+ WHERE NOT EXISTS (
46
+ SELECT 1 FROM clinical_visits v3
47
+ WHERE v3.PatientPKHash = v1.PatientPKHash
48
+ AND v3.VisitDate > v1.VisitDate
49
+ AND v3.VisitDate < v2.VisitDate
50
+ )
51
+ ORDER BY v1.PatientPKHash, v1.VisitDate;
52
+
53
  Only use the following tables:
54
  {table_info}
55
  """
 
66
  query: Annotated[str, ..., "Syntactically valid SQL query."]
67
 
68
 
69
+ def write_query(state:State) -> State:
70
  """Generate SQL query to fetch information."""
71
  prompt = query_prompt_template.invoke(
72
  {
 
74
  "top_k": 10,
75
  "table_info": db.get_table_info(),
76
  "input": state["question"],
77
+ "pk_hash": state["pk_hash"]
78
  }
79
  )
80
+
81
  structured_llm = llm.with_structured_output(QueryOutput)
82
  result = structured_llm.invoke(prompt)
83
  return {**state, "query": result["query"]}
84
 
85
+ def execute_query(state:State) -> State:
86
  """Execute SQL query."""
87
  execute_query_tool = QuerySQLDatabaseTool(db=db)
88
  return {**state, "result": execute_query_tool.invoke(state["query"])}
89
 
90
+ def generate_answer(state:State) -> State:
91
+ """
92
+ Answer question using retrieved information as context.
93
+ For awareness, NextAppointmentDate is set during the VisitDate of the same entry.
94
+ To determine if the patient came on time to their next appointment, compare NextAppointmentDate
95
+ with the next recorded VisitDate. For example, if a patient has a VisitDate of
96
+ 2023-01-01 and a NextAppointmentDate of 2023-01-15, check if the next VisitDate is on or before
97
+ 2023-01-15 to determine if the patient came on time.
98
+
99
+ """
100
  prompt = (
101
  "Given the following user question, corresponding SQL query, "
102
  "and SQL result, answer the user question.\n\n"
103
  f'Question: {state["question"]}\n'
104
  f'SQL Query: {state["query"]}\n'
105
+ f'SQL Result: {state["result"]}'
106
  )
107
  response = llm.invoke(prompt)
108
  return {**state, "answer": response.content}
109
 
110
  # now define a stateful tool that does the same thing
111
  @tool
112
+ def sql_chain(state:State) -> State:
113
  """
114
  Annotated function that takes a question string seeking information on patient data
115
  from a SQL database, writes an SQL query to retrieve relevant data, executes the query,
chatlib/state_types.py CHANGED
@@ -8,4 +8,5 @@ class State(TypedDict):
8
  rag_result: str
9
  query: str
10
  result: str
11
- answer: str
 
 
8
  rag_result: str
9
  query: str
10
  result: str
11
+ answer: str
12
+ pk_hash: str
main.py CHANGED
@@ -25,15 +25,35 @@ sys_msg = SystemMessage(content="""
25
  meeting with patients. You have two tools available,
26
  one to access information from HIV clinical guidelines, the other is
27
  a SQL tool to access patient data.
 
 
 
28
  """
29
  )
30
 
31
  # Assistant Node
32
- def assistant(state: MessagesState):
33
- return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  # Graph
36
- builder = StateGraph(MessagesState)
37
 
38
  # Define nodes: these do the work
39
  builder.add_node("assistant", assistant)
@@ -51,9 +71,22 @@ builder.add_edge("tools", "assistant")
51
  react_graph = builder.compile(checkpointer=memory)
52
 
53
  # Specify a thread
54
- config = {"configurable": {"thread_id": "13"}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- messages = [HumanMessage(content="what is the proper course of treatment for someone with opportunistic infections?")]
57
- messages = react_graph.invoke({"messages": messages}, config)
58
- for m in messages['messages']:
59
  m.pretty_print()
 
25
  meeting with patients. You have two tools available,
26
  one to access information from HIV clinical guidelines, the other is
27
  a SQL tool to access patient data.
28
+
29
+ You must respond only with a JSON object specifying the tool to call and its arguments.
30
+ Do not generate any SQL queries or answers yourself.
31
  """
32
  )
33
 
34
  # Assistant Node
35
+ def assistant(state: State) -> State:
36
+ pk_hash = state.get("pk_hash", None)
37
+
38
+ if pk_hash:
39
+ pk_msg = SystemMessage(content=f"The patient identifier (pk_hash) is: {pk_hash}")
40
+ messages = [sys_msg, pk_msg] + state["messages"]
41
+ else:
42
+ messages = [sys_msg] + state["messages"]
43
+
44
+ # Get the LLM/tool response
45
+ new_message = llm_with_tools.invoke(messages)
46
+ # Extract the question from the latest HumanMessage, if present
47
+
48
+ latest_question = ""
49
+ for msg in reversed(messages):
50
+ if isinstance(msg, HumanMessage):
51
+ latest_question = msg.content
52
+ break
53
+ return {**state, "messages": state['messages'] + [new_message], "question": latest_question}
54
 
55
  # Graph
56
+ builder = StateGraph(State)
57
 
58
  # Define nodes: these do the work
59
  builder.add_node("assistant", assistant)
 
71
  react_graph = builder.compile(checkpointer=memory)
72
 
73
  # Specify a thread
74
+ memory.delete_thread("25")
75
+ config = {"configurable": {"thread_id": "25", "user_id": "1"}}
76
+
77
+ # initialize state with patient pk hash
78
+ input_state:State = {
79
+ "messages": [HumanMessage(content="how many visits were recorded in 2024?")],
80
+ "question": "",
81
+ "rag_result": "",
82
+ "query": "",
83
+ "result": "",
84
+ "answer": "",
85
+ "pk_hash": "962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73"
86
+ }
87
+
88
+ # messages = [HumanMessage(content="how many appointments has this patient had?")]
89
+ message_output = react_graph.invoke(input_state, config)
90
 
91
+ for m in message_output['messages']:
 
 
92
  m.pretty_print()
sql_agent.ipynb CHANGED
@@ -18,7 +18,8 @@
18
  " question: str\n",
19
  " query: str\n",
20
  " result: str\n",
21
- " answer: str"
 
22
  ]
23
  },
24
  {
@@ -36,12 +37,15 @@
36
  "os.environ.get(\"OPENAI_API_KEY\")\n",
37
  "\n",
38
  "db = SQLDatabase.from_uri(\"sqlite:///data/patient_demonstration.sqlite\")\n",
39
- "llm = ChatOpenAI(temperature = 0.0, model=\"gpt-4o\")"
 
 
 
40
  ]
41
  },
42
  {
43
  "cell_type": "code",
44
- "execution_count": null,
45
  "id": "f9c96976",
46
  "metadata": {},
47
  "outputs": [
@@ -56,7 +60,8 @@
56
  "run to help find the answer. Unless the user specifies in his question a\n",
57
  "specific number of examples they wish to obtain, always limit your query to\n",
58
  "at most \u001b[33;1m\u001b[1;3m{top_k}\u001b[0m results. You can order the results by a relevant column to\n",
59
- "return the most interesting examples in the database.\n",
 
60
  "\n",
61
  "Never query for all the columns from a specific table, only ask for a the\n",
62
  "few relevant columns given the question.\n",
@@ -82,7 +87,8 @@
82
  "run to help find the answer. Unless the user specifies in his question a\n",
83
  "specific number of examples they wish to obtain, always limit your query to\n",
84
  "at most {top_k} results. You can order the results by a relevant column to\n",
85
- "return the most interesting examples in the database.\n",
 
86
  "\n",
87
  "Never query for all the columns from a specific table, only ask for a the\n",
88
  "few relevant columns given the question.\n",
@@ -101,13 +107,13 @@
101
  " [(\"system\", system_message), (\"user\", user_prompt)]\n",
102
  ")\n",
103
  "\n",
104
- "# for message in query_prompt_template.messages:\n",
105
- "# message.pretty_print()"
106
  ]
107
  },
108
  {
109
  "cell_type": "code",
110
- "execution_count": 5,
111
  "id": "fee4ebcb",
112
  "metadata": {},
113
  "outputs": [],
@@ -129,6 +135,7 @@
129
  " \"top_k\": 10,\n",
130
  " \"table_info\": db.get_table_info(),\n",
131
  " \"input\": state[\"question\"],\n",
 
132
  " }\n",
133
  " )\n",
134
  " structured_llm = llm.with_structured_output(QueryOutput)\n",
@@ -138,7 +145,7 @@
138
  },
139
  {
140
  "cell_type": "code",
141
- "execution_count": 6,
142
  "id": "cfa94f19",
143
  "metadata": {},
144
  "outputs": [],
@@ -153,7 +160,7 @@
153
  },
154
  {
155
  "cell_type": "code",
156
- "execution_count": 7,
157
  "id": "7f4e8039",
158
  "metadata": {},
159
  "outputs": [],
@@ -181,7 +188,7 @@
181
  },
182
  {
183
  "cell_type": "code",
184
- "execution_count": 8,
185
  "id": "fea8652c",
186
  "metadata": {},
187
  "outputs": [],
@@ -202,7 +209,7 @@
202
  },
203
  {
204
  "cell_type": "code",
205
- "execution_count": 9,
206
  "id": "07429d93",
207
  "metadata": {},
208
  "outputs": [
@@ -210,12 +217,12 @@
210
  "data": {
211
  "text/plain": [
212
  "{'question': 'What proportion of all regimens is accounted for by the most common regimen?',\n",
213
- " 'query': 'SELECT CurrentRegimen, COUNT(*) * 1.0 / (SELECT COUNT(*) FROM clinical_visits) AS proportion\\nFROM clinical_visits\\nGROUP BY CurrentRegimen\\nORDER BY COUNT(*) DESC\\nLIMIT 1;',\n",
214
- " 'result': \"[('TDF/3TC/DTG', 0.6232196237031827)]\",\n",
215
- " 'answer': 'The most common regimen is \"TDF/3TC/DTG,\" and it accounts for approximately 62.32% of all regimens.'}"
216
  ]
217
  },
218
- "execution_count": 9,
219
  "metadata": {},
220
  "output_type": "execute_result"
221
  }
@@ -227,7 +234,7 @@
227
  },
228
  {
229
  "cell_type": "code",
230
- "execution_count": 10,
231
  "id": "c51497e2",
232
  "metadata": {},
233
  "outputs": [],
@@ -254,7 +261,7 @@
254
  },
255
  {
256
  "cell_type": "code",
257
- "execution_count": 11,
258
  "id": "495a5e45",
259
  "metadata": {},
260
  "outputs": [],
@@ -268,6 +275,8 @@
268
  " You are a helpful assistant tasked with helping clinicians\n",
269
  " access information from patient records.\n",
270
  " Only call the SQL tool when the user asks questions about patient data. \n",
 
 
271
  " For greetings, thanks, or unrelated topics, respond directly without calling any tools.\n",
272
  "\n",
273
  " \"\"\"\n",
@@ -280,7 +289,7 @@
280
  },
281
  {
282
  "cell_type": "code",
283
- "execution_count": 12,
284
  "id": "3f17bccf",
285
  "metadata": {},
286
  "outputs": [
@@ -327,21 +336,24 @@
327
  "name": "stdout",
328
  "output_type": "stream",
329
  "text": [
330
- "{'assistant': {'messages': [AIMessage(content=\"You're welcome! If you have any more questions, feel free to ask.\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 3327, 'total_tokens': 3343, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_07871e2ad8', 'id': 'chatcmpl-BkBjCRm7HUlUQZV3ntGa2Sbpz8UPJ', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='run--45e27132-7994-4010-91e7-cd18113d643c-0', usage_metadata={'input_tokens': 3327, 'output_tokens': 16, 'total_tokens': 3343, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n"
 
 
331
  ]
332
  }
333
  ],
334
  "source": [
335
  "# Specify a thread\n",
336
- "config = {\"configurable\": {\"thread_id\": \"4\"}}\n",
337
  "\n",
338
- "user_prompt = \"thanks!?\"\n",
339
  "input_state = {\n",
340
  " \"messages\": [HumanMessage(content=user_prompt)],\n",
341
  " \"question\": user_prompt,\n",
342
  " \"query\": \"\",\n",
343
  " \"result\": \"\",\n",
344
  " \"answer\": \"\",\n",
 
345
  "}\n",
346
  "# messages = react_graph.invoke(input_state, config)\n",
347
  "\n",
 
18
  " question: str\n",
19
  " query: str\n",
20
  " result: str\n",
21
+ " answer: str\n",
22
+ " pk_hash: str"
23
  ]
24
  },
25
  {
 
37
  "os.environ.get(\"OPENAI_API_KEY\")\n",
38
  "\n",
39
  "db = SQLDatabase.from_uri(\"sqlite:///data/patient_demonstration.sqlite\")\n",
40
+ "llm = ChatOpenAI(temperature = 0.0, model=\"gpt-4o\")\n",
41
+ "\n",
42
+ "# from langchain_ollama import ChatOllama\n",
43
+ "# llm = ChatOllama(model=\"llama3.2:1b\")"
44
  ]
45
  },
46
  {
47
  "cell_type": "code",
48
+ "execution_count": 3,
49
  "id": "f9c96976",
50
  "metadata": {},
51
  "outputs": [
 
60
  "run to help find the answer. Unless the user specifies in his question a\n",
61
  "specific number of examples they wish to obtain, always limit your query to\n",
62
  "at most \u001b[33;1m\u001b[1;3m{top_k}\u001b[0m results. You can order the results by a relevant column to\n",
63
+ "return the most interesting examples in the database. For questions about specific patients, filter using the \n",
64
+ "PatientPKHash column and the provided value \u001b[33;1m\u001b[1;3m{pk_hash}\u001b[0m.\n",
65
  "\n",
66
  "Never query for all the columns from a specific table, only ask for a the\n",
67
  "few relevant columns given the question.\n",
 
87
  "run to help find the answer. Unless the user specifies in his question a\n",
88
  "specific number of examples they wish to obtain, always limit your query to\n",
89
  "at most {top_k} results. You can order the results by a relevant column to\n",
90
+ "return the most interesting examples in the database. For questions about specific patients, filter using the \n",
91
+ "PatientPKHash column and the provided value {pk_hash}.\n",
92
  "\n",
93
  "Never query for all the columns from a specific table, only ask for a the\n",
94
  "few relevant columns given the question.\n",
 
107
  " [(\"system\", system_message), (\"user\", user_prompt)]\n",
108
  ")\n",
109
  "\n",
110
+ "for message in query_prompt_template.messages:\n",
111
+ " message.pretty_print()"
112
  ]
113
  },
114
  {
115
  "cell_type": "code",
116
+ "execution_count": 4,
117
  "id": "fee4ebcb",
118
  "metadata": {},
119
  "outputs": [],
 
135
  " \"top_k\": 10,\n",
136
  " \"table_info\": db.get_table_info(),\n",
137
  " \"input\": state[\"question\"],\n",
138
+ " \"pk_hash\": state[\"pk_hash\"],\n",
139
  " }\n",
140
  " )\n",
141
  " structured_llm = llm.with_structured_output(QueryOutput)\n",
 
145
  },
146
  {
147
  "cell_type": "code",
148
+ "execution_count": 5,
149
  "id": "cfa94f19",
150
  "metadata": {},
151
  "outputs": [],
 
160
  },
161
  {
162
  "cell_type": "code",
163
+ "execution_count": 6,
164
  "id": "7f4e8039",
165
  "metadata": {},
166
  "outputs": [],
 
188
  },
189
  {
190
  "cell_type": "code",
191
+ "execution_count": 7,
192
  "id": "fea8652c",
193
  "metadata": {},
194
  "outputs": [],
 
209
  },
210
  {
211
  "cell_type": "code",
212
+ "execution_count": 8,
213
  "id": "07429d93",
214
  "metadata": {},
215
  "outputs": [
 
217
  "data": {
218
  "text/plain": [
219
  "{'question': 'What proportion of all regimens is accounted for by the most common regimen?',\n",
220
+ " 'query': 'SELECT SUM(CASE WHEN CurrentRegimen = ( SELECT MIN(CurrentRegimen) FROM data_dictionary ) THEN 1 ELSE 0 END) / COUNT(DISTINCT VisitBy) FROM clinical_visits',\n",
221
+ " 'result': 'Error: (sqlite3.OperationalError) misuse of aggregate: MIN()\\n[SQL: SELECT SUM(CASE WHEN CurrentRegimen = ( SELECT MIN(CurrentRegimen) FROM data_dictionary ) THEN 1 ELSE 0 END) / COUNT(DISTINCT VisitBy) FROM clinical_visits]\\n(Background on this error at: https://sqlalche.me/e/20/e3q8)',\n",
222
+ " 'answer': \"The issue here is that you are trying to calculate the proportion of regimens by using the `MIN` function, which is not allowed in SQL. The `MIN` function returns a single value, but you need a count or sum to get an accurate result.\\n\\nTo fix this, you can use a subquery to find the minimum regimen and then divide the count of regimens by this minimum value. Here's how you can modify your query:\\n\\n```sql\\nSELECT \\n SUM(CASE WHEN CurrentRegimen = ( SELECT MIN(CurrentRegimen) FROM data_dictionary ) THEN 1 ELSE 0 END) / COUNT(DISTINCT VisitBy)\\nFROM clinical_visits;\\n```\\n\\nThis will give you the proportion of all regimens that are accounted for by the most common regimen. \\n\\nNote: If there are multiple most common regimens, this query will return one result with the highest proportion value. If you want to get all results or handle ties in a specific way (e.g., average proportion), you would need a more complex query.\"}"
223
  ]
224
  },
225
+ "execution_count": 8,
226
  "metadata": {},
227
  "output_type": "execute_result"
228
  }
 
234
  },
235
  {
236
  "cell_type": "code",
237
+ "execution_count": 7,
238
  "id": "c51497e2",
239
  "metadata": {},
240
  "outputs": [],
 
261
  },
262
  {
263
  "cell_type": "code",
264
+ "execution_count": 14,
265
  "id": "495a5e45",
266
  "metadata": {},
267
  "outputs": [],
 
275
  " You are a helpful assistant tasked with helping clinicians\n",
276
  " access information from patient records.\n",
277
  " Only call the SQL tool when the user asks questions about patient data. \n",
278
+ " If the question is about a particular patinet, filter using the PatientPKHash column\n",
279
+ " and the provided value.\n",
280
  " For greetings, thanks, or unrelated topics, respond directly without calling any tools.\n",
281
  "\n",
282
  " \"\"\"\n",
 
289
  },
290
  {
291
  "cell_type": "code",
292
+ "execution_count": 15,
293
  "id": "3f17bccf",
294
  "metadata": {},
295
  "outputs": [
 
336
  "name": "stdout",
337
  "output_type": "stream",
338
  "text": [
339
+ "{'assistant': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_cOYoWvk66qbVV8cLl3SYbRGe', 'function': {'arguments': '{\"state\":{\"messages\":[{\"content\":\"How many visits has the patient with PatientPKHash \\'962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73\\' had?\",\"type\":\"human\"}],\"question\":\"How many visits has the patient with PatientPKHash \\'962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73\\' had?\",\"query\":\"SELECT COUNT(*) FROM clinical_visits WHERE PatientPKHash = \\'962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73\\';\",\"result\":\"\",\"answer\":\"\",\"pk_hash\":\"962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73\"}}', 'name': 'sql_chain'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 219, 'prompt_tokens': 3445, 'total_tokens': 3664, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 3328}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_07871e2ad8', 'id': 'chatcmpl-Ble8i22AvKkpBDYLnESGEflGoWmZx', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--5f0b7cec-37ad-46a0-8d1d-f77a54adc4d8-0', tool_calls=[{'name': 'sql_chain', 'args': {'state': {'messages': [{'content': \"How many visits has the patient with PatientPKHash '962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73' had?\", 'type': 'human'}], 'question': \"How many visits has the patient with PatientPKHash '962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73' had?\", 'query': \"SELECT COUNT(*) FROM clinical_visits WHERE PatientPKHash = '962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73';\", 'result': '', 'answer': '', 'pk_hash': '962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73'}}, 'id': 'call_cOYoWvk66qbVV8cLl3SYbRGe', 'type': 'tool_call'}], usage_metadata={'input_tokens': 3445, 'output_tokens': 219, 'total_tokens': 3664, 'input_token_details': {'audio': 0, 'cache_read': 3328}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n",
340
+ "{'tools': {'messages': [ToolMessage(content='{\\'messages\\': [HumanMessage(content=\"How many visits has the patient with PatientPKHash \\'962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73\\' had?\", additional_kwargs={}, response_metadata={})], \\'question\\': \"How many visits has the patient with PatientPKHash \\'962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73\\' had?\", \\'query\\': \"SELECT COUNT(*) AS visit_count FROM clinical_visits WHERE PatientPKHash = \\'962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73\\';\", \\'result\\': \\'[(5,)]\\', \\'answer\\': \"The patient with PatientPKHash \\'962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73\\' has had 5 visits.\", \\'pk_hash\\': \\'962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73\\'}', name='sql_chain', id='3f79fa70-b4b4-488a-8764-78ac01c894cc', tool_call_id='call_cOYoWvk66qbVV8cLl3SYbRGe')]}}\n",
341
+ "{'assistant': {'messages': [AIMessage(content='The patient with PatientPKHash `962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73` has had 5 visits.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 50, 'prompt_tokens': 3950, 'total_tokens': 4000, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 3584}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_07871e2ad8', 'id': 'chatcmpl-Ble8nFeHj0Q5BMtPtYaT66g95yjb3', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='run--a837133b-1d53-4c6f-a8f6-d3d16ab61fda-0', usage_metadata={'input_tokens': 3950, 'output_tokens': 50, 'total_tokens': 4000, 'input_token_details': {'audio': 0, 'cache_read': 3584}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n"
342
  ]
343
  }
344
  ],
345
  "source": [
346
  "# Specify a thread\n",
347
+ "config = {\"configurable\": {\"thread_id\": \"6\"}}\n",
348
  "\n",
349
+ "user_prompt = \"how many visits has this patient had?\"\n",
350
  "input_state = {\n",
351
  " \"messages\": [HumanMessage(content=user_prompt)],\n",
352
  " \"question\": user_prompt,\n",
353
  " \"query\": \"\",\n",
354
  " \"result\": \"\",\n",
355
  " \"answer\": \"\",\n",
356
+ " \"pk_hash\": \"962885FEADB7CCF19A2CC506D39818EC448D5396C4D1AEFDC59873090C7FBF73\"\n",
357
  "}\n",
358
  "# messages = react_graph.invoke(input_state, config)\n",
359
  "\n",