From c8ee8e6d8fa17dc2809e9e3b7d487948345cba3a Mon Sep 17 00:00:00 2001 From: Casper Bollen Date: Tue, 5 Mar 2024 14:20:44 +0100 Subject: [PATCH] fix: missing units, working on AI and solving solution rule problems --- src/Client/MUI.fs | 6 + src/Client/Views/Formulary.fs | 8 + src/Informedica.GenForm.Lib/DoseRule.fs | 2 +- src/Informedica.GenForm.Lib/DoseType.fs | 10 +- src/Informedica.GenForm.Lib/Mapping.fs | 8 +- .../PrescriptionRule.fs | 3 +- .../Scripts/DoseRule.fsx | 91 ++- src/Informedica.GenForm.Lib/SolutionRule.fs | 2 +- src/Informedica.GenForm.Lib/Types.fs | 2 +- src/Informedica.GenOrder.Lib/DrugOrder.fs | 2 +- src/Informedica.GenOrder.Lib/Scripts/Api2.fsx | 6 +- src/Informedica.GenUnits.Lib/ValueUnit.fs | 92 +++ .../Informedica.Utils.Lib.fsproj | 2 + src/Informedica.Utils.Lib/Scripts/AI.fsx | 717 ++++++++++++++++++ src/Informedica.Utils.Lib/Scripts/Gemma.fsx | 415 ++++++++++ src/Informedica.Utils.Lib/Scripts/load.fsx | 1 + 16 files changed, 1312 insertions(+), 55 deletions(-) create mode 100644 src/Informedica.Utils.Lib/Scripts/AI.fsx create mode 100644 src/Informedica.Utils.Lib/Scripts/Gemma.fsx diff --git a/src/Client/MUI.fs b/src/Client/MUI.fs index 674506b1..1d713208 100644 --- a/src/Client/MUI.fs +++ b/src/Client/MUI.fs @@ -272,6 +272,12 @@ module Icons = """ + let PsychologyIcon = JSX.jsx $""" + import PsychologyIcon from '@mui/icons-material/Psychology'; + + """ + + type Color = {| ``50`` : string; diff --git a/src/Client/Views/Formulary.fs b/src/Client/Views/Formulary.fs index ee03fa1c..7da9c98f 100644 --- a/src/Client/Views/Formulary.fs +++ b/src/Client/Views/Formulary.fs @@ -259,6 +259,13 @@ module Formulary = |> autoComplete isLoading "rts" sel (RouteChange >> dispatch) } + + + + + { match props.formulary with @@ -273,6 +280,7 @@ module Formulary = } + """ diff --git a/src/Informedica.GenForm.Lib/DoseRule.fs b/src/Informedica.GenForm.Lib/DoseRule.fs index 9534d6c3..0b3bc4aa 100644 --- a/src/Informedica.GenForm.Lib/DoseRule.fs +++ b/src/Informedica.GenForm.Lib/DoseRule.fs @@ -838,7 +838,7 @@ module DoseRule = fun (dr : DoseRule) -> match filter.DoseType, dr.DoseType with | None, _ - | _, AnyDoseType -> true + | _, NoDoseType -> true | _ -> dr.DoseType |> DoseType.toString diff --git a/src/Informedica.GenForm.Lib/DoseType.fs b/src/Informedica.GenForm.Lib/DoseType.fs index d32c07e6..7de3f7fd 100644 --- a/src/Informedica.GenForm.Lib/DoseType.fs +++ b/src/Informedica.GenForm.Lib/DoseType.fs @@ -14,7 +14,7 @@ module DoseType = | Timed _ | Discontinuous _ -> 3 | Continuous _ -> 4 - | AnyDoseType -> 100 + | NoDoseType -> 100 let eqs doseType1 doseType2 = @@ -24,7 +24,7 @@ module DoseType = | Timed _, Timed _ | Discontinuous _, Discontinuous _ | Continuous _, Continuous _ - | AnyDoseType, AnyDoseType -> true + | NoDoseType, NoDoseType -> true | _ -> false @@ -39,7 +39,7 @@ module DoseType = | "timed" -> Timed |> withText | "discontinuous" -> Discontinuous |> withText | "continuous" -> Continuous |> withText - | _ -> AnyDoseType + | _ -> NoDoseType /// Get a string representation of a dose type. @@ -58,7 +58,7 @@ module DoseType = | Timed _ | Discontinuous _ -> "onderhoud" | Continuous _ -> "continu" - | AnyDoseType -> "" + | NoDoseType -> "" - | AnyDoseType -> "" + | NoDoseType -> "" diff --git a/src/Informedica.GenForm.Lib/Mapping.fs b/src/Informedica.GenForm.Lib/Mapping.fs index 57d35cd7..92081237 100644 --- a/src/Informedica.GenForm.Lib/Mapping.fs +++ b/src/Informedica.GenForm.Lib/Mapping.fs @@ -57,12 +57,12 @@ module Mapping = let mapUnit s = if s |> String.isNullOrWhiteSpace then None else - let s = s |> String.toLower |> String.trim + let s = s |> String.trim unitMapping |> Array.tryFind (fun r -> - r.Long = s || - r.Short = s || - r.MV = s + r.Long |> String.equalsCapInsens s || + r.Short |> String.equalsCapInsens s || + r.MV |> String.equalsCapInsens s ) |> function | Some r -> $"{r.Short}[{r.Group}]" |> Units.fromString diff --git a/src/Informedica.GenForm.Lib/PrescriptionRule.fs b/src/Informedica.GenForm.Lib/PrescriptionRule.fs index 5271a273..f42e5d45 100644 --- a/src/Informedica.GenForm.Lib/PrescriptionRule.fs +++ b/src/Informedica.GenForm.Lib/PrescriptionRule.fs @@ -35,7 +35,8 @@ module PrescriptionRule = } ) |> Array.filter (fun pr -> - pr.DoseRule.DoseType <> DoseType.AnyDoseType + // filter out the dose rules that do not have a dose type + pr.DoseRule.DoseType <> DoseType.NoDoseType ) // recalculate adjusted dose limits |> Array.map (fun pr -> diff --git a/src/Informedica.GenForm.Lib/Scripts/DoseRule.fsx b/src/Informedica.GenForm.Lib/Scripts/DoseRule.fsx index 783692b9..97ae8e33 100644 --- a/src/Informedica.GenForm.Lib/Scripts/DoseRule.fsx +++ b/src/Informedica.GenForm.Lib/Scripts/DoseRule.fsx @@ -14,11 +14,14 @@ Environment.SetEnvironmentVariable("GENPRES_URL_ID", dataUrlId) #load "../Utils.fs" #load "../Mapping.fs" #load "../VenousAccess.fs" +#load "../Mapping.fs" #load "../Patient.fs" +#load "../LimitTarget.fs" #load "../DoseType.fs" #load "../Product.fs" #load "../Filter.fs" #load "../DoseRule.fs" +#load "../SolutionRule.fs" #time @@ -44,43 +47,55 @@ Product.get () let data = getData dataUrlId -data -|> Array.distinctBy (fun r -> - r.Brand, - r.Department, - r.Gender, - r.Generic, - r.Indication, - r.Route, - r.Shape, - r.Substance, - r.AdjustUnit, - r.DoseType, - r.DoseUnit, - r.Frequencies, - r.FreqUnit, - r.DurUnit, - r.IntervalUnit, - r.MaxAge, - r.MaxDur, - r.MaxInterval, - r.MaxQty, - r -) - -Web.getDataFromSheet dataUrlId "DoseRules" -|> fun data -> - let getColumn = - data - |> Array.head - |> Csv.getStringColumn - - data - |> Array.skip 1 - |> fun xs -> printfn $"{xs |> Array.length}"; xs - |> Array.distinctBy (fun row -> - row |> Array.tail - ) +open Informedica.GenUnits.Lib + +let filter = + { Filter.filter with + Patient = + { Patient.patient with + VenousAccess = [VenousAccess.CVL] + Department = Some "ICK" + Age = + Units.Time.year + |> ValueUnit.singleWithValue 12N + |> Some + Weight = + Units.Weight.kiloGram + |> ValueUnit.singleWithValue (30N) + |> Some + } + } + + +DoseRule.get () +|> Array.take 1 +|> DoseRule.filter filter +|> Array.item 0 +|> fun dr -> + SolutionRule.get () + |> SolutionRule.filter + { filter with + Generic = dr.Generic |> Some + Shape = dr.Shape |> Some + Route = dr.Route |> Some + DoseType = dr.DoseType |> DoseType.toString |> Some + } + + +"kCal" +|> Mapping.mapUnit + +Mapping.unitMapping + +let s = "IE" |> String.toLower |> String.trim +Mapping.unitMapping +|> Array.tryFind (fun r -> + r.Long |> String.equalsCapInsens s || + r.Short |> String.equalsCapInsens s || + r.MV |> String.equalsCapInsens s +) +|> function + | Some r -> $"{r.Short}[{r.Group}]" |> Units.fromString + | None -> None -|> Array.length diff --git a/src/Informedica.GenForm.Lib/SolutionRule.fs b/src/Informedica.GenForm.Lib/SolutionRule.fs index fd404826..37e2e351 100644 --- a/src/Informedica.GenForm.Lib/SolutionRule.fs +++ b/src/Informedica.GenForm.Lib/SolutionRule.fs @@ -204,7 +204,7 @@ module SolutionRule = fun (sr : SolutionRule) -> match filter.DoseType, sr.DoseType with | None, _ - | _, AnyDoseType -> true + | _, NoDoseType -> true | _ -> sr.DoseType |> DoseType.toString diff --git a/src/Informedica.GenForm.Lib/Types.fs b/src/Informedica.GenForm.Lib/Types.fs index c6e0a0e9..06cff5a6 100644 --- a/src/Informedica.GenForm.Lib/Types.fs +++ b/src/Informedica.GenForm.Lib/Types.fs @@ -62,7 +62,7 @@ module Types = | Timed of string /// A once per time | OnceTimed of string - | AnyDoseType + | NoDoseType /// A Substance type. diff --git a/src/Informedica.GenOrder.Lib/DrugOrder.fs b/src/Informedica.GenOrder.Lib/DrugOrder.fs index 77a22234..8405ceda 100644 --- a/src/Informedica.GenOrder.Lib/DrugOrder.fs +++ b/src/Informedica.GenOrder.Lib/DrugOrder.fs @@ -297,7 +297,7 @@ module DrugOrder = | Once _ -> OnceOrder | Discontinuous _ -> DiscontinuousOrder | Timed _ -> TimedOrder - | AnyDoseType -> AnyOrder + | NoDoseType -> AnyOrder Dose = dose Adjust = if au |> ValueUnit.Group.eqsGroup Units.Weight.kiloGram then diff --git a/src/Informedica.GenOrder.Lib/Scripts/Api2.fsx b/src/Informedica.GenOrder.Lib/Scripts/Api2.fsx index e98ea578..eb41a4fb 100644 --- a/src/Informedica.GenOrder.Lib/Scripts/Api2.fsx +++ b/src/Informedica.GenOrder.Lib/Scripts/Api2.fsx @@ -224,12 +224,12 @@ Patient.teenager |> ValueUnit.singleWithValue (30N) |> Some } -|> Api.scenarioResult |> Api.filter +//|> Api.scenarioResult |> Api.filter //|> fun p -> { p with VenousAccess = CVL; AgeInDays = Some 0N } |> PrescriptionRule.get |> Array.item 0 //|> Api.evaluate (OrderLogger.logger.Logger) -|> fun pr -> pr |> DrugOrder.createDrugOrder None //|> printfn "%A" -//|> fun pr -> pr |> DrugOrder.createDrugOrder (pr.SolutionRules[0] |> Some) //|> printfn "%A" +//|> fun pr -> pr |> DrugOrder.createDrugOrder None //|> printfn "%A" +|> fun pr -> pr |> DrugOrder.createDrugOrder (pr.SolutionRules[0] |> Some) //|> printfn "%A" |> DrugOrder.toOrderDto |> Order.Dto.fromDto //|> Order.toString |> List.iter (printfn "%s") |> Order.Dto.toDto diff --git a/src/Informedica.GenUnits.Lib/ValueUnit.fs b/src/Informedica.GenUnits.Lib/ValueUnit.fs index 2d07915d..e449edd3 100644 --- a/src/Informedica.GenUnits.Lib/ValueUnit.fs +++ b/src/Informedica.GenUnits.Lib/ValueUnit.fs @@ -59,6 +59,7 @@ type Unit = | Weight of WeightUnit | Height of HeightUnit | BSA of BSAUnit + | Energy of EnergyUnit and CountUnit = Times of BigRational and MassUnit = @@ -110,6 +111,10 @@ and HeightUnit = and BSAUnit = M2 of BigRational +and EnergyUnit = + | Calorie of BigRational + | KiloCalorie of BigRational + and Operator = | OpTimes | OpPer @@ -138,6 +143,7 @@ module Group = | WeightGroup | HeightGroup | BSAGroup + | EnergyGroup | CombiGroup of (Group * Operator * Group) @@ -1102,6 +1108,45 @@ module Units = Synonyms = [ "m^2" ] } + { + Unit = Energy.calorie + Group = Group.NoGroup + Abbreviation = + { + Eng = "cal" + Dut = "cal" + EngPlural = "calories" + DutchPlural = "calorieen" + } + Name = + { + Eng = "calorie" + Dut = "calorie" + EngPlural = "calories" + DutchPlural = "calorieen" + } + Synonyms = [ ] + } + + { + Unit = Energy.kiloCalorie + Group = Group.NoGroup + Abbreviation = + { + Eng = "kCal" + Dut = "kCal" + EngPlural = "kilocalories" + DutchPlural = "kilocalorieen" + } + Name = + { + Eng = "kilocalorie" + Dut = "kilocalorie" + EngPlural = "kilocalories" + DutchPlural = "kilocalorieen" + } + Synonyms = [ ] + } ] |> List.map (fun ud -> { ud with @@ -1337,6 +1382,21 @@ module Units = let m2 = 1N |> nM2 + module Energy = + + let toEnergy = Energy + + + let nCalorie n = n |> Calorie |> toEnergy + + let calorie = 1N |> nCalorie + + let nKiloCalorie n = n |> KiloCalorie |> toEnergy + + let kiloCalorie = 1N |> nKiloCalorie + + + /// /// Map a unit to a unit value and a unit /// @@ -1403,6 +1463,10 @@ module Units = | BSA g -> match g with | M2 n -> (n, BSA.m2) + | Energy e -> + match e with + | Calorie n -> (n, Energy.calorie) + | KiloCalorie n -> (n, Energy.kiloCalorie) | CombiUnit (u1, op, u2) -> failwith <| $"Cannot map combined unit %A{(u1, op, u2) |> CombiUnit}" @@ -1659,6 +1723,11 @@ module Units = | BSA g -> match g with | M2 n -> n |> f |> M2 |> BSA + | Energy e -> + match e with + | Calorie n -> n |> f |> Calorie + | KiloCalorie n -> n |> f |> KiloCalorie + |> Energy | CombiUnit (u1, op, u2) -> (app u1, op, app u2) |> CombiUnit app u @@ -1737,6 +1806,10 @@ module Units = | BSA g -> match g with | M2 n -> n |> Some + | Energy e -> + match e with + | Calorie n -> n |> Some + | KiloCalorie n -> n |> Some | CombiUnit _ -> None app u @@ -1840,6 +1913,13 @@ module Units = | M2 _, M2 _ -> true | BSA _, _ | _, BSA _ -> false + | Energy e1, Energy e2 -> + match e1, e2 with + | Calorie _, Calorie _ + | KiloCalorie _, KiloCalorie _ -> true + | _ -> false + | Energy _, _ + | _, Energy _ -> false | CombiUnit (ul1, op1, ur1), CombiUnit (ul2, op2, ur2) -> op1 = op2 && eqsUnit ul1 ul2 && eqsUnit ur1 ur2 @@ -1895,6 +1975,7 @@ module ValueUnit = | Weight _ -> Group.WeightGroup | Height _ -> Group.HeightGroup | BSA _ -> Group.BSAGroup + | Energy _ -> Group.EnergyGroup | CombiUnit (ul, op, ur) -> (get ul, op, get ur) |> Group.CombiGroup get u @@ -1924,6 +2005,7 @@ module ValueUnit = | Group.InterNatUnitGroup | Group.WeightGroup | Group.HeightGroup + | Group.EnergyGroup | Group.BSAGroup -> g = g2 | Group.CombiGroup (gl, _, gr) -> cont gl || cont gr @@ -2011,6 +2093,7 @@ module ValueUnit = | Group.WeightGroup -> "Weight" | Group.HeightGroup -> "Height" | Group.BSAGroup -> "BSA" + | Group.EnergyGroup -> "Energy" | Group.CombiGroup (gl, op, gr) -> let gls = str gl s let grs = str gr s @@ -2080,6 +2163,11 @@ module ValueUnit = 1N |> HeightCentiMeter |> Height ] | Group.BSAGroup -> [ 1N |> M2 |> BSA ] + | Group.EnergyGroup -> + [ + 1N |> Calorie |> Energy + 1N |> KiloCalorie |> Energy + ] | Group.CombiGroup _ -> [] @@ -2226,6 +2314,10 @@ module ValueUnit = | BSA g -> match g with | M2 n -> n * one + | Energy e -> + match e with + | Calorie n -> n * one + | KiloCalorie n -> n * kilo | CombiUnit (u1, op, u2) -> let m1 = get u1 m let m2 = get u2 m diff --git a/src/Informedica.Utils.Lib/Informedica.Utils.Lib.fsproj b/src/Informedica.Utils.Lib/Informedica.Utils.Lib.fsproj index 90e81d8c..efd7cde3 100644 --- a/src/Informedica.Utils.Lib/Informedica.Utils.Lib.fsproj +++ b/src/Informedica.Utils.Lib/Informedica.Utils.Lib.fsproj @@ -40,6 +40,8 @@ + + \ No newline at end of file diff --git a/src/Informedica.Utils.Lib/Scripts/AI.fsx b/src/Informedica.Utils.Lib/Scripts/AI.fsx new file mode 100644 index 00000000..d266275c --- /dev/null +++ b/src/Informedica.Utils.Lib/Scripts/AI.fsx @@ -0,0 +1,717 @@ + + +#r "nuget: Newtonsoft.Json" + + +module Texts = + + let testTexts = [ + """ +alprazolam +6 jaar tot 18 jaar Startdosering: 0,125 mg/dag, éénmalig. Onderhoudsdosering: Op geleide van klinisch beeld verhogen met stappen van 0,125-0,25 mg/dosis tot max 0,05 mg/kg/dag in 3 doses. Max: 3 mg/dag. Advies inname/toediening: De dagdosis indien mogelijk verdelen over 3 doses.Bij plotselinge extreme slapeloosheid: alleen voor de nacht innemen; dosering op geleide van effect ophogen tot max 0,05 mg/kg, maar niet hoger dan 3 mg/dag.De effectiviteit bij de behandeling van acute angst is discutabel. +""" + """ +acetylsalicylzuur +1 maand tot 18 jaar Startdosering:Acetylsalicylzuur: 30 - 50 mg/kg/dag in 3 - 4 doses. Max: 3.000 mg/dag. +""" + """ +paracetamol +Oraal: Bij milde tot matige pijn en/of koorts: volgens het Kinderformularium van het NKFK bij een leeftijd van 1 maand–18 jaar: 10–15 mg/kg lichaamsgewicht per keer, zo nodig 4×/dag, max. 60 mg/kg/dag en max. 4 g/dag. +""" + """ +amitriptyline +6 jaar tot 18 jaar Startdosering: voor de nacht: 10 mg/dag in 1 dosisOnderhoudsdosering: langzaam ophogen met 10 mg/dag per 4-6 weken naar 10 - 30 mg/dag in 1 dosis. Max: 30 mg/dag. Behandeling met amitriptyline mag niet plotseling worden gestaakt vanwege het optreden van ontwenningsverschijnselen; de dosering moet geleidelijk worden verminderd.Uit de studie van Powers (2017) blijkt dat de werkzaamheid van amitriptyline bij migraine profylaxe niet effectiever is t.o.v. placebo. Desondanks menen experts dat in individuele gevallen behandeling met amitriptyline overwogen kan worden. +""" + ] + + + + let systemDoseIndicationExpert = """ +You are an expert on medication prescribing, preparation and administration. +You have to answer questions about texts that describing a drug dose. +You are asked to extract structured information from that text. + +Are the indications for a specific drug. An indication for the drug is the reason to +prescribe the drug with a dose advice. + +You answer all questions with ONLY the shortest possible answer to the question. + """ + + + let systemDosePatientExpert = """ +You are an expert on medication prescribing, preparation and administration. +You have to answer questions about texts that describing a drug dose. +You are asked to extract structured information from that text. + +The information you are looking for pertains to the patient category the dose text applies to: +- the gender of the patient +- the minimum age of the patient +- the maximum age of the patient +- the minimum weight of the patient +- the maximum weight of the patient +- the minimum body surface area of the patient +- the maximum body surface area of the patient +- the minimum gestational age of the patient +- the maximum gestational age of the patient +- the minimum post menstrual age of the patient +- the maximum post menstrual age of the patient + +If the required answer is not available you return an empty string + +You answer all questions with ONLY the shortest possible answer to the question. + """ + + + let systemDoseQuantityExpert = """ +You are an expert on medication prescribing, preparation and administration. +You have to answer questions about texts that describing a drug dose. +You are asked to extract structured information from that text. + +The information will one of the following: +- a quantity with a number and a unit, example: 40 mg/day +- or a single unit, example: day +- or a list of numbers, example: 1;2;3 + +An adjust unit (AdjustUnit) can only be 'kg' body weight or 'mˆ2' body surface area. +A substance unit is a unit that belongs to either a mass unit group, molar unit group +or is an IU/IE (international unit of measurement). + +You answer all questions with ONLY the shortest possible answer to the question. + """ + + let doseRuleStructure = """ + { + "doseRule": { + "generic": "acetaminophen/ibuprofen", // "The generic name in all small caps, for multiple substances drugs concatenated with '/'" + "shape": "tablet", // "The shape name of the generic drug from the Z-index" + "brand": "BrandX", // "The specific Brand a dose rule applies to" + "GPKs": ["GPK123", "GPK456"], // "Specific medication identifiers (Generic Product Codes)" + "route": "oral", // "The route name using the mapping in the Routes sheet" + "indication": "pain relief", // "The indication of the dose rule from the Kinderformularium or Farmacotherapeutisch Kompas" + "dep": "pediatrics", // "The department the dose rule applies to" + "gender": "any", // "The gender the dose rule applies to" + "ageRange": { + "minAge": "0 days", // "The minimum age in days the dose rule applies to" + "maxAge": "6570 days" // "The maximum age in days the dose rule applies to" + }, + "weightRange": { + "minWeight": "1000 gram", // "The minimum weight in grams the dose rule applies to" + "maxWeight": "50000 gram" // "The maximum weight in grams the dose rule applies to" + }, + "BSARange": { + "minBSA": "0.5 mˆ2", // "The minimum BSA in m2 the dose rule applies to" + "maxBSA": "1.5 mˆ2" // "The maximum BSA in m2 the dose rule applies to" + }, + "gestationalAgeRange": { + "minGestAge": "0 days", // "The minimum gestational age in days the dose rule applies to" + "maxGestAge": "280 days" // "The maximum gestational age in days the dose rule applies to" + }, + "postmenstrualAgeRange": { + "minPMAge": "0 days", // "The minimum postmenstrual age in days the dose rule applies to" + "maxPMAge": "294 days" // "The maximum postmenstrual age in days the dose rule applies to" + }, + "doseType": "continuous", // "The dose type ('once', 'onceTimed', 'discontinuous', 'timed', 'continuous')" + "doseText": "Continuous intravenous infusion", // "Description of the DoseType" + "substance": "acetaminophen", // "The substance used, part of the generic name" + "timeRange": { + "minTime": 30, // "The minimum time for infusion of a dose" + "maxTime": 120, // "The maximum time for infusion of a dose" + "timeUnit": "minutes" // "The time unit to measure the infusion" + }, + "intervalRange": { + "minInt": 4, // "The minimum interval between two doses" + "maxInt": 8, // "The maximum interval between two doses" + "intUnit": "hours" // "The interval unit" + }, + "durationRange": { + "minDur": 1, // "The minimum duration of the dose rule" + "maxDur": 7, // "The maximum duration of the dose rule" + "durUnit": "days" // "The duration time unit" + }, + "quantityRange": { + "minQty": "10 mg", // "The minimum dose quantity per dose" + "maxQty": "15 mg", // "The maximum dose quantity per dose" + "normQtyAdj": "10 mg/kg", // "The normal adjusted quantity per dose" + "minQtyAdj": "9 mg/kg", // "The minimum adjusted quantity per dose" + "maxQtyAdj": "11 mg/kg", // "The maximum adjusted quantity per dose" + "doseUnit": "mg", // "The dose unit of the substance" + "adjustUnit": "kg", // "The unit to adjust the dose with" + }, + "frequencyRange": { + "minPerTime": "40 mg/day", // "The minimum dose quantity per freq time unit" + "maxPerTime": "60 mg/day", // "The maximum dose quantity per freq time unit" + "normPerTimeAdj": "40/kg/day", // "The normal adjusted dose quantity per frequency unit" + "minPerTimeAdj": "36/kg/day", // "The minimum adjusted dose quantity per freq time unit" + "maxPerTimeAdj": "44/kg/day" // "The maximum dose adjusted quantity per freq time unit" + "doseUnit": "mg", // "The dose unit of the substance" + "adjustUnit": "kg", // "The unit to adjust the dose with" + "freqs": "4;6;8", // "The possible frequencies, i.e. doses per freq unit, number list separated with ';'" + "freqUnit": "day", // "The time unit of the frequencies" + }, + "rateRange": { + "minRate": "2 mg/hour", // "The minimum dose rate" + "maxRate": "4 mg/hour", // "The maximum dose rate" + "minRateAdj": "2 mg/kg/hour", // "The minimum adjusted dose rate" + "maxRateAdj": "4 mg/kg/hour" // "The maximum adjusted dose rate" + "adjustUnit": "kg", // "The unit to adjust the dose with" + "rateUnit": "hour", // "The time unit for a dose rate" + } + } + } + """ + + + let doseQuantity = """ + { + "doseQuantity": { + "timeRange": { + "minTime": "", // "A Quantity. The minimum time for infusion of a dose" + "maxTime": "", // "A Quantity. The maximum time for infusion of a dose" + "timeUnit": "" // "A Unit. The time unit to measure the infusion" + }, + "intervalRange": { + "minInt": "", // "A Quantity. The minimum interval between two doses" + "maxInt": "", // "A Quantity. The maximum interval between two doses" + "intUnit": "" // "A Unit. The interval unit" + }, + "durationRange": { + "minDur": "", // "A Quantity. The minimum duration of the dose rule" + "maxDur": "", // "A Quantity. The maximum duration of the dose rule" + "durUnit": "" // "A Unit. The duration time unit" + }, + "quantityRange": { + "minQty": "", // "A Quantity. The minimum dose quantity per dose" + "maxQty": "", // "A Quantity. The maximum dose quantity per dose" + "normQtyAdj": "", // "A Quantity. The normal adjusted quantity per dose" + "minQtyAdj": "", // "A Quantity. The minimum adjusted quantity per dose" + "maxQtyAdj": "", // "A Quantity. The maximum adjusted quantity per dose" + "doseUnit": "", // "A Unit. The dose unit of the substance" + "adjustUnit": "" // "A Unit. The unit to adjust the dose with" + }, + "frequencyRange": { + "minPerTime": "", // "A Quantity. The minimum dose quantity per freq time unit" + "maxPerTime": "", // "A Quantity. The maximum dose quantity per freq time unit" + "normPerTimeAdj": "", // "A Quantity. The normal adjusted dose quantity per frequency unit" + "minPerTimeAdj": "", // "A Quantity. The minimum adjusted dose quantity per freq time unit" + "maxPerTimeAdj": "", // "A Quantity. The maximum dose adjusted quantity per freq time unit" + "doseUnit": "", // "A Unit. The dose unit of the substance" + "adjustUnit": "", // "A Unit. The unit to adjust the dose with" + "freqs": "", // "A list of numbers. The possible frequencies, i.e. doses per freq unit, number list separated with ';'" + "freqUnit": "" // "A Unit. The time unit of the frequencies" + }, + "rateRange": { + "minRate": "", // "A Quantity. The minimum dose rate" + "maxRate": "", // "A Quantity. The maximum dose rate" + "minRateAdj": "", // "A Quantity. The minimum adjusted dose rate" + "maxRateAdj": "", // "A Quantity. The maximum adjusted dose rate" + "doseUnit": "", // "A Unit. The dose unit of the substance" + "adjustUnit": "", // "A Unit. The unit to adjust the dose with" + "rateUnit": "" // "A Unit. The time unit for a dose rate" + } + } + } + """ + + + let context = $""" + A dose quantity can be characterized by the following structure: + + {doseQuantity} + + Each field is either + - a quantity with a number and a unit, example: 40 mg/day + - or a single unit: example: day + - or a list of numbers: example: 1;2;3 + """ + + let freqRange = """ + "frequencyRange": { + "minPerTime": , + "maxPerTime": , + "normPerTimeAdj": , + "minPerTimeAdj": , + "maxPerTimeAdj": , + "doseUnit": , + "adjustUnit": , + "freqs": , + "freqUnit": + } + """ + + +/// Utility methods to use ollama +/// https://github.com/ollama/ollama/blob/main/docs/api.md#generate-a-chat-completion +module Ollama = + + open System + open System.Net.Http + open System.Text + open Newtonsoft.Json + + + type Configuration() = + member val num_keep: Nullable = Nullable() with get, set + member val seed: Nullable = Nullable() with get, set + member val num_predict: Nullable = Nullable() with get, set + member val top_k: Nullable = Nullable() with get, set + member val top_p: Nullable = Nullable() with get, set + member val tfs_z: Nullable = Nullable() with get, set + member val typical_p: Nullable = Nullable() with get, set + member val repeat_last_n: Nullable = Nullable() with get, set + member val temperature: Nullable = Nullable() with get, set + member val repeat_penalty: Nullable = Nullable() with get, set + member val presence_penalty: Nullable = Nullable() with get, set + member val frequency_penalty: Nullable = Nullable() with get, set + member val mirostat: Nullable = Nullable() with get, set + member val mirostat_tau: Nullable = Nullable() with get, set + member val mirostat_eta: Nullable = Nullable() with get, set + member val penalize_newline: Nullable = Nullable() with get, set + member val stop: string[] = [||] with get, set + member val numa: Nullable = Nullable() with get, set + member val num_ctx: Nullable = Nullable() with get, set + member val num_batch: Nullable = Nullable() with get, set + member val num_gqa: Nullable = Nullable() with get, set + member val num_gpu: Nullable = Nullable() with get, set + member val main_gpu: Nullable = Nullable() with get, set + member val low_vram: Nullable = Nullable() with get, set + member val f16_kv: Nullable = Nullable() with get, set + member val vocab_only: Nullable = Nullable() with get, set + member val use_mmap: Nullable = Nullable() with get, set + member val use_mlock: Nullable = Nullable() with get, set + member val rope_frequency_base: Nullable = Nullable() with get, set + member val rope_frequency_scale: Nullable = Nullable() with get, set + member val num_thread: Nullable = Nullable() with get, set + + + let options = + let opts = Configuration() + opts.seed <- 101 + opts.temperature <- 0. + opts.repeat_last_n <- 64 + opts.num_ctx <- 2048 + opts.mirostat <- 0 + + opts + + + module Roles = + + let user = "user" + let system = "system" + let assistent = "assistent" + + + type Message = + { + role : string + content : string + } + + + module Message = + + let create role content = + { + role = role + content = content + } + + let user = create Roles.user + + let system = create Roles.system + + + + type Response = + | Success of ModelResponse + | Error of string + and ModelResponse = { + model: string + created_at: string + response: string + message: Message + ``done``: bool + context: int list + total_duration: int64 + load_duration: int64 + prompt_eval_duration: int64 + eval_count: int + eval_duration: int64 + } + + + type ModelDetails = { + format: string + family: string + families : string [] + parameter_size: string + quantization_level: string + } + + type Model = { + name: string + modified_at: string + size: int64 + digest: string + details: ModelDetails + } + + type Models = { + models: Model list + } + + + type Embedding = { + embedding : float [] + } + + + type Details = { + format: string + family: string + families: string list + parameter_size: string + quantization_level: string + } + + type ModelConfig = { + modelfile: string + parameters: string + template: string + details: Details + } + + module EndPoints = + + [] + let generate = "http://localhost:11434/api/generate" + + [] + let pull = "http://localhost:11434/api/pull" + + [] + let chat = "http://localhost:11434/api/chat" + + [] + let tags = "http://localhost:11434/api/tags" + + [] + let embeddings = "http://localhost:11434/api/embeddings" + + [] + let show = "http://localhost:11434/api/show" + + + + // Create an HTTP client + let client = new HttpClient() + + let pullModel name = + + let pars = + {| + name = name + |} + |> JsonConvert.SerializeObject + + let content = new StringContent(pars, Encoding.UTF8, "application/json") + + // Asynchronous API call + async { + let! response = client.PostAsync(EndPoints.pull, content) |> Async.AwaitTask + let! responseBody = response.Content.ReadAsStringAsync() |> Async.AwaitTask + return responseBody + } + + + let generate model prompt = + + let pars = + {| + model = model + prompt = prompt + options = options + (* + {| + seed = 101 + temperature = 0. + |} + *) + stream = false + |} + |> JsonConvert.SerializeObject + + let content = new StringContent(pars, Encoding.UTF8, "application/json") + + // Asynchronous API call + async { + let! response = client.PostAsync(EndPoints.generate, content) |> Async.AwaitTask + let! responseBody = response.Content.ReadAsStringAsync() |> Async.AwaitTask + + let modelResponse = + try + responseBody + |> JsonConvert.DeserializeObject + |> Success + with + | e -> e.ToString() |> Error + return modelResponse + } + + + let chat model messages (message : Message) = + + let messages = + {| + model = model + messages = + [ message ] + |> List.append messages + options = options + stream = false + |} + |> JsonConvert.SerializeObject + + let content = new StringContent(messages, Encoding.UTF8, "application/json") + + // Asynchronous API call + async { + let! response = client.PostAsync(EndPoints.chat, content) |> Async.AwaitTask + let! responseBody = response.Content.ReadAsStringAsync() |> Async.AwaitTask + + let modelResponse = + try + responseBody + |> JsonConvert.DeserializeObject + |> Success + with + | e -> + e.ToString() |> Error + + return modelResponse + } + + + let listModels () = + + // Asynchronous API call + async { + let! response = client.GetAsync(EndPoints.tags) |> Async.AwaitTask + let! responseBody = response.Content.ReadAsStringAsync() |> Async.AwaitTask + let models = + try + responseBody + |> JsonConvert.DeserializeObject + with + | e -> e.ToString() |> failwith + + return models + } + + let showModel model = + let prompt = + {| + name = model + |} + |> JsonConvert.SerializeObject + + let content = new StringContent(prompt, Encoding.UTF8, "application/json") + + // Asynchronous API call + async { + let! response = client.PostAsync(EndPoints.show, content) |> Async.AwaitTask + let! responseBody = response.Content.ReadAsStringAsync() |> Async.AwaitTask + + let modelConfig = + try + responseBody + |> JsonConvert.DeserializeObject + with + | e -> e.ToString() |> failwith + return modelConfig + } + + + let embeddings model prompt = + let prompt = + {| + model = model + prompt = prompt + |} + |> JsonConvert.SerializeObject + + let content = new StringContent(prompt, Encoding.UTF8, "application/json") + + // Asynchronous API call + async { + let! response = client.PostAsync(EndPoints.embeddings, content) |> Async.AwaitTask + let! responseBody = response.Content.ReadAsStringAsync() |> Async.AwaitTask + let models = + try + responseBody + |> JsonConvert.DeserializeObject + with + | e -> e.ToString() |> failwith + + return models + } + + + + let run llm messages message = + message + |> chat llm messages + |> Async.RunSynchronously + |> function + | Success response -> + [message; response.message] + |> List.append messages + | Error s -> + printfn $"oops: {s}" + messages + + + module Models = + + let llama2 = "llama2" + + let ``llama2:13b-chat`` = "llama2:13b-chat" + + let gemma = "gemma" + + let ``gemma:7b-instruct`` = "gemma:7b-instruct" + + let mistral = "mistral" + + let ``mistral:7b-instruct`` = "mistral:7b-instruct" + + + + let runLlama2 = run Models.llama2 + + + let runLlama2_13b_chat = run Models.``llama2:13b-chat`` + + + let runGemma = run Models.gemma + + + let runGemma_7b_instruct = run Models.``gemma:7b-instruct`` + + + let runMistral = run Models.mistral + + + let runMistral_7b_instruct = run Models.``mistral:7b-instruct`` + + + + module Operators = + + + let mutable runModel = runLlama2 + + + let (>>?) msgs msg = + printfn $"### PROMPT:{msg}" + + msg + |> Message.user + |> runModel msgs + |> fun msgs -> + let answer = + (msgs |> List.last).content.Trim() + + printfn $"### ANSWER:\n{answer}\n" + msgs + + + +open Ollama.Operators + + + +let testModel name modelToRun = + + + printfn $"\n\n# Running: {name}\n\n" + + + runModel <- modelToRun + + + for text in Texts.testTexts do + + Texts.systemDoseQuantityExpert + |> Ollama.Message.system + |> runModel [] + >>? $""" +The text between the ''' describes dose quantities for a +substance: + +'''{text}''' + +For which substance? +Give the answer as Substance : ? +""" + >>? """ +What is the unit used for the substance, the substance unit? +Give the answer as SubstanceUnit : ? +""" + >>? """ +What is the unit to adjust the dose for? +Give the answer as AdjustUnit : ? +""" + >>? """ +What is the time unit for the dose frequency? +Give the answer as TimeUnit : ? +""" + >>? """ +What is the maximum dose per time in SubstanceUnit/TimeUnit? +Give the answer as MaximumDosePerTime: ? +""" + >>? """ +What is the dose, adjusted for weight in SubstanceUnit/AdjustUnit/TimeUnit? +Give the answer as AdjustedDosePerTime: ? +""" + >>? """ +What is the number of doses per TimeUnit? +Give the answer as Frequency: ? +""" + >>? """ +Summarize the previous answers as: + +- Substance: ? +- SubstanceUnit: ? +- AdjustUnit: ? +- TimeUnit: ? +- MaximumDosePerTime: ? +- AdjustedDosePerTime: ? +- Frequency: ? + +""" + |> ignore + + +let testAll () = + + [ + Ollama.Models.gemma, Ollama.runGemma + Ollama.Models.``gemma:7b-instruct``, Ollama.runGemma_7b_instruct + Ollama.Models.llama2, Ollama.runLlama2 + Ollama.Models.``llama2:13b-chat``, Ollama.runLlama2_13b_chat + Ollama.Models.mistral, Ollama.runMistral + Ollama.Models.``mistral:7b-instruct``, Ollama.runMistral_7b_instruct + ] + |> List.iter (fun (n, m) -> testModel n m) + + + +Ollama.options.penalize_newline <- true +Ollama.options.top_k <- 10 +Ollama.options.top_p <- 0.95 + + +// The winner is: +Ollama.runMistral_7b_instruct +|> testModel Ollama.Models.``mistral:7b-instruct`` diff --git a/src/Informedica.Utils.Lib/Scripts/Gemma.fsx b/src/Informedica.Utils.Lib/Scripts/Gemma.fsx new file mode 100644 index 00000000..99c940b7 --- /dev/null +++ b/src/Informedica.Utils.Lib/Scripts/Gemma.fsx @@ -0,0 +1,415 @@ + + +#r "nuget: Newtonsoft.Json" + + +module Texts = + + + let systemDoseQuantityExpert = """ +You are an expert on medication prescribing, preparation and administration. +You have to answer questions about texts that describing a drug dose. +You are asked to extract structured information from that text. + +The information will one of the following: +- a quantity with a number and a unit, example: 40 mg/day +- or a single unit, example: day +- or a list of numbers, example: 1;2;3 + +An adjust unit (AdjustUnit) can only be 'kg' body weight or 'mˆ2' body surface area. +A substance unit is a unit that belongs to either a mass unit group, molar unit group +or is an international unit of measurement. + +You answer all questions with ONLY the shortest possible answer to the question. + """ + + + let testTexts = [ + """ +alprazolam +6 jaar tot 18 jaar Startdosering: 0,125 mg/dag, éénmalig. Onderhoudsdosering: Op geleide van klinisch beeld verhogen met stappen van 0,125-0,25 mg/dosis tot max 0,05 mg/kg/dag in 3 doses. Max: 3 mg/dag. Advies inname/toediening: De dagdosis indien mogelijk verdelen over 3 doses.Bij plotselinge extreme slapeloosheid: alleen voor de nacht innemen; dosering op geleide van effect ophogen tot max 0,05 mg/kg, maar niet hoger dan 3 mg/dag.De effectiviteit bij de behandeling van acute angst is discutabel. +""" + """ +acetylsalicylzuur +1 maand tot 18 jaar Startdosering:Acetylsalicylzuur: 30 - 50 mg/kg/dag in 3 - 4 doses. Max: 3.000 mg/dag. +""" + """ +paracetamol +Oraal: Bij milde tot matige pijn en/of koorts: volgens het Kinderformularium van het NKFK bij een leeftijd van 1 maand–18 jaar: 10–15 mg/kg lichaamsgewicht per keer, zo nodig 4×/dag, max. 60 mg/kg/dag en max. 4 g/dag. +""" + """ +amitriptyline +6 jaar tot 18 jaar Startdosering: voor de nacht: 10 mg/dag in 1 dosisOnderhoudsdosering: langzaam ophogen met 10 mg/dag per 4-6 weken naar 10 - 30 mg/dag in 1 dosis. Max: 30 mg/dag. Behandeling met amitriptyline mag niet plotseling worden gestaakt vanwege het optreden van ontwenningsverschijnselen; de dosering moet geleidelijk worden verminderd.Uit de studie van Powers (2017) blijkt dat de werkzaamheid van amitriptyline bij migraine profylaxe niet effectiever is t.o.v. placebo. Desondanks menen experts dat in individuele gevallen behandeling met amitriptyline overwogen kan worden. +""" + ] + + + + +/// Utility methods to use ollama +/// https://github.com/ollama/ollama/blob/main/docs/api.md#generate-a-chat-completion +module Ollama = + + open System + open System.Net.Http + open System.Text + open Newtonsoft.Json + + + module Roles = + + let user = "user" + let system = "system" + let assistent = "assistent" + + + type Message = + { + role : string + content : string + } + + + module Message = + + let create role content = + { + role = role + content = content + } + + let user = create Roles.user + + let system = create Roles.system + + + + type Response = + | Success of ModelResponse + | Error of string + and ModelResponse = { + model: string + created_at: string + response: string + message: Message + ``done``: bool + context: int list + total_duration: int64 + load_duration: int64 + prompt_eval_duration: int64 + eval_count: int + eval_duration: int64 + } + + + type ModelDetails = { + format: string + family: string + families : string [] + parameter_size: string + quantization_level: string + } + + type Model = { + name: string + modified_at: string + size: int64 + digest: string + details: ModelDetails + } + + type Models = { + models: Model list + } + + + type Embedding = { + embedding : float [] + } + + + type Details = { + format: string + family: string + families: string list + parameter_size: string + quantization_level: string + } + + type ModelConfig = { + modelfile: string + parameters: string + template: string + details: Details + } + + module EndPoints = + + [] + let generate = "http://localhost:11434/api/generate" + + [] + let pull = "http://localhost:11434/api/pull" + + [] + let chat = "http://localhost:11434/api/chat" + + [] + let tags = "http://localhost:11434/api/tags" + + [] + let embeddings = "http://localhost:11434/api/embeddings" + + [] + let show = "http://localhost:11434/api/show" + + + + // Create an HTTP client + let client = new HttpClient() + + + let pullModel name = + + let pars = + {| + name = name + |} + |> JsonConvert.SerializeObject + + let content = new StringContent(pars, Encoding.UTF8, "application/json") + + // Asynchronous API call + async { + let! response = client.PostAsync(EndPoints.pull, content) |> Async.AwaitTask + let! responseBody = response.Content.ReadAsStringAsync() |> Async.AwaitTask + return responseBody + } + + + let generate model prompt = + + let pars = + {| + model = model + prompt = prompt + options = + {| + seed = 101 + temperature = 0. + |} + stream = false + |} + |> JsonConvert.SerializeObject + + let content = new StringContent(pars, Encoding.UTF8, "application/json") + + // Asynchronous API call + async { + let! response = client.PostAsync(EndPoints.generate, content) |> Async.AwaitTask + let! responseBody = response.Content.ReadAsStringAsync() |> Async.AwaitTask + + let modelResponse = + try + responseBody + |> JsonConvert.DeserializeObject + |> Success + with + | e -> e.ToString() |> Error + return modelResponse + } + + + let chat model messages (message : Message) = + + let messages = + {| + model = model + messages = + [ message ] + |> List.append messages + options = + {| + seed = 101 + temperature = 0. + |} + stream = false + |} + |> JsonConvert.SerializeObject + + let content = new StringContent(messages, Encoding.UTF8, "application/json") + + // Asynchronous API call + async { + let! response = client.PostAsync(EndPoints.chat, content) |> Async.AwaitTask + let! responseBody = response.Content.ReadAsStringAsync() |> Async.AwaitTask + + let modelResponse = + try + responseBody + |> JsonConvert.DeserializeObject + |> Success + with + | e -> + e.ToString() |> Error + + return modelResponse + } + + + let listModels () = + + // Asynchronous API call + async { + let! response = client.GetAsync(EndPoints.tags) |> Async.AwaitTask + let! responseBody = response.Content.ReadAsStringAsync() |> Async.AwaitTask + let models = + try + responseBody + |> JsonConvert.DeserializeObject + with + | e -> e.ToString() |> failwith + + return models + } + + let showModel model = + let prompt = + {| + name = model + |} + |> JsonConvert.SerializeObject + + let content = new StringContent(prompt, Encoding.UTF8, "application/json") + + // Asynchronous API call + async { + let! response = client.PostAsync(EndPoints.show, content) |> Async.AwaitTask + let! responseBody = response.Content.ReadAsStringAsync() |> Async.AwaitTask + + let modelConfig = + try + responseBody + |> JsonConvert.DeserializeObject + with + | e -> e.ToString() |> failwith + return modelConfig + } + + + let embeddings model prompt = + let prompt = + {| + model = model + prompt = prompt + |} + |> JsonConvert.SerializeObject + + let content = new StringContent(prompt, Encoding.UTF8, "application/json") + + // Asynchronous API call + async { + let! response = client.PostAsync(EndPoints.embeddings, content) |> Async.AwaitTask + let! responseBody = response.Content.ReadAsStringAsync() |> Async.AwaitTask + let models = + try + responseBody + |> JsonConvert.DeserializeObject + with + | e -> e.ToString() |> failwith + + return models + } + + + + let run llm messages message = + message + |> chat llm messages + |> Async.RunSynchronously + |> function + | Success response -> + [message; response.message] + |> List.append messages + | Error s -> + printfn $"oops: {s}" + messages + + + let runGemma_7b_instruct = run "gemma:7b-instruct" + + + + module Operators = + + + let mutable runModel = runGemma_7b_instruct + + + let (>>?) msgs msg = + printfn $"## ME:\n{msg}" + + msg + |> Message.user + |> runModel msgs + |> fun msgs -> + printfn $"## GEMMA:\n{(msgs |> List.last).content}\n" + msgs + + + +open Ollama.Operators + + + +for text in Texts.testTexts do + + Texts.systemDoseQuantityExpert + |> Ollama.Message.system + |> runModel [] + >>? $""" +The text between the ''' describes dose quantities for a +substance: + +'''{text}''' + +For which substance? +Give the answer as Substance : ? +""" + >>? """ +What is the unit used for the substance, the substance unit? +Give the answer as SubstanceUnit : ? +""" + >>? """ +What is the unit to adjust the dose for? +Give the answer as AdjustUnit : ? +""" + >>? """ +What is the time unit for the dose frequency? +Give the answer as TimeUnit : ? +""" + >>? """ +What is the maximum dose per time in SubstanceUnit/TimeUnit? +Give the answer as MaximumDosePerTime: ? +""" + >>? """ +What is the dose, adjusted for weight in SubstanceUnit/AdjustUnit/TimeUnit? +Give the answer as AdjustedDosePerTime: ? +""" + >>? """ +What is the number of doses per TimeUnit? +Give the answer as Frequency: ? +""" + >>? """ +Summarize the previous answers: +Substance: ? +SubstanceUnit: ? +AdjustUnit: ? +TimeUnit: ? +MaximumDosePerTime: ? +AdjustedDosePerTime: ? +Frequency: ? +""" + |> ignore + + diff --git a/src/Informedica.Utils.Lib/Scripts/load.fsx b/src/Informedica.Utils.Lib/Scripts/load.fsx index 5439ad93..79828b34 100644 --- a/src/Informedica.Utils.Lib/Scripts/load.fsx +++ b/src/Informedica.Utils.Lib/Scripts/load.fsx @@ -1,5 +1,6 @@ #r "nuget: Unquote" #r "nuget: MathNet.Numerics.FSharp" +#r "nuget: Newtonsoft.Json" #load "../Continuation.fs" #load "../Constants.fs"