2024年12月21日 星期六

CDS Hooks - 程式設計 - CDS Service端

如上篇所言,這兒是用暴力法,把MLM上的判斷邏輯,用人工方式直接轉成C#程式碼。
還記得有一個CreateEmptyCard,那個是只有MLM的宣告,卻沒有收到任何資料來源時可回應給EHR的部份。
我們這邊用了兩個MLM,要有資料來源得上Sandbox,要有確定的個案對象才好測試。所以,這邊仍暫時保留之。

===== Responses =====
還記得Call Service時,會叫用Responses.GetResponseContent(),當時,只展示了空的Card,現在不一樣了。

判斷要用什麼CDS服務是透過Id,若沒有當然是給空的。
則就是針對個別,回應正確的Card。
目前先把FHIR Resoure部份先保留,因為這塊我想要完整描述。他會用到我自己開發的FHIR SDK。這邊就先假設,已經從Resource中取回想要的值。
接著就是分別呼叫對應的CardService。

====== BMI  ======

25:就是拿傳進來的值去做邏輯判斷。
26~35:提供外部連接用。
36~44:準備建議按鈕。 篇所言,這兒是用暴力法,把MLM上的判斷邏輯,用人工方式直接轉成C#程式碼。
還記得有一個CreateEmptyCard,那個是只有MLM的宣告,卻沒有收到任何資料來源時可回應給EHR的部份。
我們這邊用了兩個MLM,要有資料來源得上Sandbox,要有確定的個案對象才好測試。所以,這邊仍暫時保留之。

===== Responses =====
還記得Call Service時,會叫用Responses.GetResponseContent(),當時,只展示了空的Card,現在不一樣了。

判斷要用什麼CDS服務是透過Id,若沒有當然是給空的。
則就是針對個別,回應正確的Card。
目前先把FHIR Resoure部份先保留,因為這塊我想要完整描述。他會用到我自己開發的FHIR SDK。這邊就先假設,已經從Resource中取回想要的值。
接著就是分別呼叫對應的CardService。

====== BMI ======

25:就是拿傳進來的值去做邏輯判斷。
26~35:提供外部連接用。
36~44:準備建議按鈕。 在此僅是產生一個按鈕,準備Feeback階段時可接收。但Action區塊沒有設定,沒有準備FHIR Resource,所以就沒有的後續作業問題。
45~57:產生Card。

至於BMIEvalue的部份,就依照MLM的邏輯判斷來處理。

===== HeartRate =====
這塊就簡單帶過。

HeartRateEvalue的部份。






2024年12月20日 星期五

CDS Hooks - 程式設計 - 實做抉擇

討論到現在,前端服務架構已逐漸成形。 現在的問題是要收來自EHR端的POST資料,更重要是FHIR Resource與FHIR Server的相關資訊。然後翠取出CDS Serevice所需的資料內容,送給他處理,並且等待結果轉化成Card類別,回覆給EHR端。

現在要面臨深思的事,這個平台到底是要處理一般化,還是要特殊化。

特殊化很單純,目前這個專案就是處理兩個CDS,BMI與Hear Rate。而且他們要的資料都很明確就是要Observation然後各自對應LOINC碼,把Value值抓出來丟給CDS。兩隻CDS收到資料後,開始進行邏輯處理(這個對資訊人員來說,就是最困難的部份)。處理好後,再把結果組合成Card後,送回給EHR。這個叫暴力實做法。

而一般化,就是說這個平台只要匯入MLM的話,就自動產生的一個CDS 服務,這是我的理想。而這個目標也是可行的。Arden Sytax已經開始支援FHIR,只是應用上還不很明確。當資料源(Data:)確定資料,那邏輯(Logic:)的部份就是Parser程式的問題。再明確一點,就是有一個MLM的編輯器,把文件寫好後,就可以產生CDS Hook要的設定檔。

可惜,這些年來我的工作一直跟FHIR無緣,只能利用下班時間研究,能量還是有限。再者也沒什麼深厚背景,可以搞新創,搞基金。總之一切隨緣。

==========

雖然說是暴力實做法,但是,跟FHIR Server互動這塊,還是會提供程式碼參考。雖然,目前挑的這兩個案例,只會用到Observation,但實務上,不會這麼簡單。(哈~要找到漂亮複雜的CDS,那就超出我能力範圍)。只是Access Token的部份,我還是會保留。畢竟我也不是專搞慈善事業的。



2024年12月19日 星期四

CDS Hooks - 程式開發 - Call Service

當EHR叫用cds-services得知目前CDS Server支援了哪些CDS項目。接著就依據Id名稱,正式叫用服務。但,這時候已經把兩邊搞的天翻地覆了。

EHR要依據CDS Hooks端的要求,把所有資料準備好,接著用POST方式叫用服務。

CDS Hooks端收到資料後,就得開始找出真正提供CDS服務者。這個過程有點複雜,因為使用的Hooks不同,FHIR Resource會出現在不同的地方。另外,會有Reference Datatype的問題,不過這個可以在一開始Prefetch時,把查詢的URL先搞定。

這篇只說明呼叫時的運作,還沒有到CDS Service。

程式

===== Call Service ====
根據CDS Hooks的規範書可知,選定服務後在/cds-services/{service.id} 。

30: 根據傳進來的service.id來取得他的Hook內容。
32: 需要一個負責回應Card內容的類別Responses,呼叫啟始設定函數InitHook()。
33: 再呼叫GetResponseContent()。注意,這個函數要處理很多事情,包含去呼叫對應的CDS,然後產生適當的Card出來。
40~44: 這是要符合CDS Hooks規範要求。

===== Hooks =====
這個類別上次就看過,在這邊是已知Service.Id之下,來找出對應的Hook內容。

===== Responses.InitHook() =====
這裡的重點不是函數本身,而他ParseRequestModel();

在EHR送過來的資料中,有三個非常重要的東西其型態卻是object。分別為context,
prefectch與fhirAuthorization。避免複雜fhirAuthorization部份不實做。
利用ParseRequestModel()來拆解內容,並給後續步驟使用。
還有一個因素是FHIR Resource都有可能在Context與Prefectch中。前者是草稿中,後者是已結案的。

===== Responses.GetResponseContent() =====
目前這個函數的內容是暫時的,因為真正提供CDS服務的類別還沒完成,所以Card的內容都不會有。

這邊使用到一個CardService,他是用來與CDS溝通,並產生Card類別提供給EHR端。目前先用他的一個產生空白Card的函數。
這邊有個CardModel,這是依據CDS Hooks規範要求所設計。

===== CardModel =====
他衍生了很多類別,這邊就不多說。會有專篇來討論Card這個複雜的傢伙。

===== CardService =====
這個類別以後會很複雜,目前就單純先給個CreateEmptyCard的還是來做驗證之用。

另外,我也沒有使用Sandbox進行測試,所以Body資料是空的,只能一步一步寫,一步一步解說。

測試

===== BMICalculation =====

===== HeartRateMonitoring =====


CDS Hooks - 程式開發 - Discovery

整個遊戲的一開端,就是EHR端要先知道CDS Hooks端提供了哪些CDS服務。這個階段稱之為Discovery。不過,有些平台例如EPIC,他就會省略這一段。

程式

===== Discovery =====

根據CDS Hooks的規範書可知,CDS Hooks端要提供一個endpoint 叫cds-services。

(這不是C# DotNet教學,細節就不多提)

這個Endpoint內容很直覺。就設計一個Hooks類別提供所有Hooks清單。

===== Hooks =====


Hooks類別,主要目的是去讀取HookSettings.json檔案。這個檔案就是描述著會有哪些CDS Hooks服務。

讀進來後,就可以提供GetHooksList函數了。


其中ServiceModel就是依照CDS Hooks規範之要求所設計的。

===== ServicesModel, HookItem  =====



這兩個類別應該不需要多做解釋。那個UsageRequirements抱歉,我也還沒搞清楚實際用意為何。

===== HookSettings.json =====

 回到這個設定檔。


透露一下,這個設定檔是產生自另一個介面,其內容都是來自MLM(當然,您不需要如此)。唯一要注意的是Prefetch這段。這是要求EHR端,要使用這個CDS服務時,需要準備什麼樣的FHIR Resource。這塊自動化的程度,仍有待思考。

==========

測試



2024年12月15日 星期日

CDS Hooks - 程式開發 - 簡易說明

開發CDS Hooks,其實不難,難的是你要提供什麼服務。

在此架構中,EHR是需求端,CDS Hooks是服務(回應)端。之間只有資料層面的交換,並沒有UI(網頁)內容呈現。(若有,那請走Smart on FHIR架構) 

本次系統開發採用C# DotNet 9,用Web API專案範本。(若Smart on FHIR就需MVC或Blazor) 

依據CDS Hooks規範,需要提供下列服務(Endpoint)

  1. Discovery:讓EHR端知道CDS端提供了哪些CDS項目。 CDS端要告知每一項服務會走什麼Hook(這個會決定Context內容),其id編碼是什麼(如Patient Id, Encounter Id)。然後可以順便告知,要使用這項服務的話,需先準備哪些FHIR Resource。上述內容CDS端會回傳services這個json檔。
  2. CDS Service:有了上述服務清單,EHR端的使用者就要決定採用哪一項服務。EHR決定要呼叫那個服務時,就得先準備好幾項東西,例如Fhir Server在哪(若還要再撈其他資料,例如遇到Reference型態),fhirAuthorization相關資訊(若需要的話),還有Conttext所要求的Id內容,另外,當初在Discovery時,CDS端告知需要事先準備的FHIR Resource。CDS端收到資料後,經過處理,然後回傳card這個json檔。EHR端要負責解析呈現這個card json檔內容。
  3. Feeback::上一階段回傳的card內容,其實包含了很多按鈕宣告。而CDS端則需要接收EHR使用者到底按了那個按鈕的訊息,然後進行下一個步驟。例如刪除Resource或者新增Resource。
雖然,表面說來很容易,但為了系統彈性等因素,相關配套物件設計就不能少。例如:
  1.  Prefetch的宣告:重點就是如何宣告一個Resource的查詢URL,而能夠效率最佳化。(不用一來一往,重複存取)
  2. Hooks的選用:目前這塊也不是很穩定。向CDS Hooks Sandbox他也只支援patient-view與order-select。這塊會影響到Perfetch能產生多複雜的查詢語法。
  3. Authorization設計:CDS Hooks與Smart on FHIR架構差距此為重要一點。CDS Hooks是請EHR端準備好Access Token相關資料,請一併送過來。而Smart on FHIR則是需要一段一段交握中取回。
  4. Card的設計:這個是最困難的部份,因為要配合CDS端的設計,畢竟Card能夠處理的是有限的。CDS設計了太複雜的回應結果,是很難用Card來完整呈現。再來,Card是有EHR負責Render,所以,呈現的結果可能不會是CDS端所想要的。
  5. Feeback設計:EHR端的回饋,其實是由CDS端決定的。也就是把按鈕設計在Card中。問題就是,能設計到多複雜,要細分到什麼程度?後續的處理模式刪除或新增,這個還得問問FHIR Server是否有開放支援。
總之,CDS Hooks並非像有些人口中所言,可以輕輕鬆鬆完成的事情。

CDS HOOKS - 選擇測試平台(CDS Hooks Sandbox)

原則上應該要有自己發展的EHR,然後搭配自己的FHIR Server來進行展示。可惜,自己工作不在FHIR上,無法專心開發相關系統。
但就CDS Hooks的開發者來說,就是開發一個單純提供資料處理服務,資料來源(FHIR Server)與展示平台(EHR)本來就是要來自外界。

目前CDS Hooks Sanbox 最好的測試平台是:https://sandbox.cds-hooks.org/
(用Chrome開啟會有問題)
一些設定功能可參考這篇
 
1.的部份,就是接著我們要開發的部份。
2.的部份,是我們選擇從那個FHIR Server調取哪一個病人的資料。
當然這個介面就是用來暫時替代EHR。

這系列強調的是實做,至於CDS Hooks的規範部份,請參考這系列文章




2024年12月14日 星期六

CDS HOOKS - 確認測試資料源

這個步驟在實際上線時,可有可無。若個案沒有所需資料,當然此項CDS HOOKS就不成立。但為了說明實做,總不能最後結果都是個案無資料。

目前各家可公開測試的FHIR Server都有些差異性,而且,查詢特定資料的方法也可以多樣性。所以,這篇的目的就是想先確認,哪一個FHIR Server可以提供穩定資料來源方便測試。

使用工具就是之前開發的FHIR Query Builder工具。有興趣可以參考這篇

預計檢視的FHIR Server有:(為降低實做複雜度,僅採用Open)

要實做CDS的項目有下列,而根據MLM的描述,要實做這些項目,所需要的資料分別註記於後。
  • BMI Calculation
    • bmi: numeric (input)
    • weight: numeric (option)
    • height: numeric (option)
    • bmi_category: text (output)
  • Blood Glucose Monitoring
    • blood_glucose: numeric (input)
    • recommendation: text (output)
  • Heart Rate Monitoring
    • heart_rate: numeric (input)
    • recommendation: text (output)
接著要問有沒有FHIR Profile可以參考呢?
=====補充說明=====
Blood Glucose的部份有一點麻煩。他是來自NHSN Reporting: Adverse Drug Events - Glycemic Control,其中code的部份,他要求綁定VSAC 2.16.840.1.113762.1.4.1190.38。要查他的碼,得要先註冊UMLS。然後查得這是一系列編碼。

以我能力是無法做出正確判斷。我的能力只能告知這些東西要怎麼取得,要怎麼看,最後的臨床知識,我就不能逞強。也就是說,這個CDS HOOKS我就暫時不處理了。
==========

接著就是要從FHIR Server查看看,是否有個案可以提供測試與展示。(不想篇幅過長,就展示結果)
=====BMI https://server.fire.ly/ ====
根據Profile,要找Observation
  1. code/Coding/system = http://loinc.org
  2. code/Coding/code = 39156-5
這個FHIR Server可以,高達122筆個案符合此條件。


=====Heart Rate  https://server.fire.ly/  =====
根據Profile,要找Observation
  1. code/Coding/system = http://loinc.org
  2. code/Coding/code = 8867-4
這個也不錯,有283個個案符合。





2024年12月12日 星期四

CDS HOOKS - 思維再論

其實講定說要使用CDS HOOKS時,資料來源就已經固定是FHIR了。

但就操作流程來看,使用者(醫師)一定正在使用某個系統,而這個系統已經有個案的基本資料。就看你要怎麼稱呼這個「系統」,你就當他是EHR吧。接著的問題是,這個EHR的資料從哪裡來?

  1. 從這個EHR所依附的資料庫?
  2. 從FHIR來?
  3. 從CDA R2來。
也許這是系統設計問題吧。接著要問,使用者只是看看資料而已?還是要做什麼業務行為?會不會有維護異動資料的行為?那新增或更新後的資料要送到何處去?

回到原點,要搞CDS HOOKS得先要有類似EHR的系統,讓使用者有個案資料可以看,然後在進行業務過程中,去選擇適當的CDS。
===========
前面談到的是,臨床資料的問題。再切回另一個面向,那使用者到底需要什麼樣的臨床決策支援呢?也就是說,若您是一個CDS的開發者,您當然想要開發一個醫師會想用的東西。
注意,CDS HOOKS不會去開發一個非常完整的系統(Smart on FHIR才會),一般而言,他會單點突破的東西。就是提供一些必要的臨床資料後,在根據某些運算法則或臨床常規等,給出一個建議,甚至不會去做實質的治療行為。
好囉,根據你想要做的臨床決策性東西,他背後的運算法則或臨床常規要怎麼取得呢?當然最簡單的方法就是問醫師,查臨床路徑,去研究一堆的期刊論文。但對資訊人員來說,是有點困難。
是的,最理想的方式就是有可參考的MLM文件。他是用技術人員看得懂得語法所描述的。但MLM並不容易撰寫。雖然市場上已經有編輯器可以使用,但實用性,與降低門檻上,還是不甚理想。
==========

我想表達的是,動不動就說要搞CDS HOOKS,那就別鬧了。

2024年12月11日 星期三

CDS HOOKS - 資料來源: 從Arden Syntax (MLM) 看FHIR與CDA R2

 Arden Syntax簡單的說就是把醫師的臨床決策思維,用結構化、電子化方式表達出來。既然可行,此決策所需的臨床資料當然是數位化。

若是直接從資料庫的話,那也就沒什麼好討論的。此篇是想用一個MLM來看看,要怎麼從FHIR或CDA R2中取得資料。或許我想表達的是臨床資料是時間性的。

===== MLM =====

title: "Heart Rate Rhythm Monitoring"
mlmname: "HeartRateRhythmMonitoring"
version: "1.0"
institution: "Example Hospital"
author: "Dr. Jane Smith"
specialist: "Cardiology"
date: "2024-12-10"
validation: testing
purpose: "To monitor the heart rate rhythm of a patient and provide recommendations"
explanation: "This MLM checks the patient's heart rate and rhythm, and provides recommendations based on the results."
knowledge:
type: data-driven
data:
heart_rate: numeric
heart_rhythm: text
recommendation: text
logic:
evoke:
if exists(heart_rate) and exists(heart_rhythm)
data:
heart_rate := getField("heart_rate")
heart_rhythm := getField("heart_rhythm")
action:
if heart_rate < 60 then
recommendation := "Bradycardia detected. Consider further evaluation and possible intervention."
else if heart_rate >= 60 and heart_rate <= 100 then
recommendation := "Heart rate is within the normal range. Continue regular monitoring."
else if heart_rate > 100 then
recommendation := "Tachycardia detected. Consider further evaluation and possible intervention."
if heart_rhythm == "irregular" then
recommendation := recommendation + " Irregular heart rhythm detected. Consider further evaluation and possible intervention."
message:
"The patient's heart rate is {heart_rate} and the rhythm is {heart_rhythm}. Recommendation: {recommendation}."
===========
這個MLM是用來監控心跳,然後給出建議或警告。所需要的資料為heart rate與heart rhythm。
接著來看看FHIR有沒有Profile來規範。

===== FHIR =====
所幸,US Core IG有Heart Rate Profile。

從此結構得知,要取得Heart Rate要找Observation這個Resource,且要找到(過濾)Loinc Code為8867-4者,然後再去取ValueQuantity的Value內容。
可惜,找不到heart_rhythm的規範。從MLM中只知道他是一段文字。
==========

接著來看若資料從CDA R2來取得時。
從C-CDA Online (https://www.hl7.org/ccdasearch/) 查詢可得,有Vital Signs Section,其中有包含Heart Rate Rhythm 子區段。

===== CDA R2 =====
<section>
<templateId root="2.16.840.1.113883.10.20.22.2.4.1"/>
<templateId root="2.16.840.1.113883.10.20.22.2.4.1" extension="2015-08-01"/>
<code code="8716-3" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC"
displayName="Vital signs"/>
<title>Vital Signs (Last Filed)</title>
<text>
<table>
<thead>
<tr>
<th>Date</th>
<th>Pulse</th>
<th>Heart Rhythm</th>
<!-- other vitals signs could exist here-->
</tr>
</thead>
<tbody>
<tr>
<td>05/20/2014 7:36pm</td>
<td ID="Pulse_1">80 /min</td>
<td ID="Pulsequalifier_1">Heart irregularly irregular</td>
<!-- other vitals sign values could exist here-->
</tr>
</tbody>
</table>
</text>
<entry typeCode="DRIV">
<!-- When a set of vital signs are recorded together, include them in single clustered organizer-->
<organizer classCode="CLUSTER" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.22.4.26"/>
<templateId root="2.16.840.1.113883.10.20.22.4.26" extension="2015-08-01"/>
<id extension="21688133041015158234"
root="2.16.840.1.113883.3.42.126.100001.19"/>
<code code="46680005" displayName="Vital Signs" codeSystem="2.16.840.1.113883.6.96"
codeSystemName="SNOMED CT">
<translation code="74728-7"
displayName="Vital signs, weight, height, head circumference, oximetry, BMI, and BSA panel "
codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC"/>
</code>
<statusCode code="completed"/>
<effectiveTime value="20140520193605-0600"/>
<!-- Each vital sign should be its own component. -->
<component>
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.22.4.27"/>
<templateId root="2.16.840.1.113883.10.20.22.4.27" extension="2014-06-09"/>
<id extension="216881330410151584"
root="2.16.840.1.113883.3.42.126.100001.19"/>
<code code="8867-4" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC"
displayName="Heart rate"/>
<text>
<reference value="#Pulse_1"/>
</text>
<statusCode code="completed"/>
<effectiveTime value="20140520193605-0600"/>
<value xsi:type="PQ" value="80" unit="/min"/>
</observation>
</component>
<component>
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.22.4.2"/>
<templateId root="2.16.840.1.113883.10.20.22.4.2" extension="2015-08-01"/>
<id extension="216881330410151"
root="2.16.840.1.113883.3.42.126.100001.19"/>
<code xsi:type="CE" codeSystem="2.16.840.1.113883.6.1"
codeSystemName="LOINC"
code="8884-9" displayName="Heart rate rhythm"/>
<text>
<reference value="#Pulsequalifier_1"/>
</text>
<statusCode code="completed" />
<effectiveTime value="20140520193605-0600" />
<value xsi:type="CE" codeSystem="2.16.840.1.113883.6.96"
codeSystemName="SNOMED CT" code="248651005"
displayName="Heart irregularly irregular (finding)"/>
</observation>
</component>
</organizer>
</entry>
</section>
這是一個Vital Sign的Template,他可以包含兩個Component,分別是Heart rate (LOINC: 8867-4)與Heart rate rhythm (LOINC: 8884-9)。然後再分別從value中取出內容值。其中Heart rate rhythm比較麻煩,他是一個編碼文字採用SNOMED CT。
==========

===== 結語 =====
先捨棄Heart rate rhythm這個要求。畢竟MLM中也是獨立判斷項目。
從使用者應用的角度來說,CDS HOOKS的應用情境應該是醫師先定位好病患,然後,看這個醫師「註冊」了什麼樣的CDS HOOKS來協助。
這時候情境就要確認:
(1)醫師當時是要處理什麼事情。
(2)臨床資料是否已經取得。
(2)CDS是自動啟動還是要醫師點選。
(3)需要參考CDS建議後,才寫入病歷資料。

畢竟我不是臨床人員,所以我也不知道答案。但就我淺薄的概念來看,CDA R2更適合這個情境。
我的感覺,這時候醫師不會無聊的去亂按鈕,嚐鮮式的去按按鈕,看看有什麼東西。
有可能就是正要寫Progress Note。然後有收集了一些Vital Signs (也許是從FHIR來),然後一股腦地都丟給MLM(當然不是本篇所示,可以更複雜),然後得回一堆的建議,之後複製到病歷中,產生一份Progress Note。從Progress Note的建議內容來說,是可以有Vital Signs區段。這時候,所有的建議是匯集在同一份臨床文件上。

若是走FHIR當然也是可行,只是每一項Vital Signs的內容與建議,要找定適合的Profile(定義正確的Code),分別用Observation,POST回Server。
=============
不管用誰,都得搞定實做。CDS HOOK要怎麼基於MLM來實做呢?這是接著要克服的東西。