-
Notifications
You must be signed in to change notification settings - Fork 0
mlx-swift #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
mlx-swift #7
Changes from 7 commits
d44c30e
97c2a4c
ba0cd4f
0efbddd
63467a3
afdd8a7
ab299ec
29dbb99
deff3ba
27dd900
94ced4c
bb20641
3870eb3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| // | ||
| // This source file is part of the HealthyLLM based on the Stanford Spezi Template Application project | ||
| // | ||
| // SPDX-FileCopyrightText: 2026 Stanford University | ||
| // | ||
| // SPDX-License-Identifier: MIT | ||
| // | ||
|
|
||
| import Foundation | ||
| import HealthKit | ||
| import SpeziHealthKit | ||
|
|
||
| extension HealthDataFetcher { | ||
| func fetchElectrocardiograms( | ||
| _ healthKit: HealthKit, | ||
| limit: Int = 1 | ||
| ) async throws -> [ElectrocardiogramData] { | ||
| let samples = try await fetchElectrocardiogramSamples(healthKit, limit: limit) | ||
|
|
||
| var records: [ElectrocardiogramData] = [] | ||
| for sample in samples { | ||
| let voltages = try await fetchVoltageMeasurements(for: sample, healthKit: healthKit) | ||
| records.append( | ||
| .init( | ||
| startDate: sample.startDate, | ||
| endDate: sample.endDate, | ||
| classification: String(describing: sample.classification), | ||
| symptomsStatus: String(describing: sample.symptomsStatus), | ||
| averageHeartRate: sample.averageHeartRate?.doubleValue( | ||
| for: .count().unitDivided(by: .minute()) | ||
| ), | ||
| samplingFrequency: sample.samplingFrequency?.doubleValue(for: .hertz()), | ||
| numberOfVoltageMeasurements: voltages.count, | ||
| voltages: voltages | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| return records | ||
| } | ||
|
|
||
| private func fetchElectrocardiogramSamples( | ||
| _ healthKit: HealthKit, | ||
| limit: Int | ||
| ) async throws -> [HKElectrocardiogram] { | ||
| try await withCheckedThrowingContinuation { continuation in | ||
| let query = HKSampleQuery( | ||
| sampleType: HKObjectType.electrocardiogramType(), | ||
| predicate: nil, | ||
| limit: limit, | ||
| sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)] | ||
| ) { _, samples, error in | ||
| if let error { | ||
| continuation.resume(throwing: error) | ||
| return | ||
| } | ||
|
|
||
| continuation.resume(returning: samples as? [HKElectrocardiogram] ?? []) | ||
| } | ||
|
|
||
| healthKit.healthStore.execute(query) | ||
| } | ||
| } | ||
|
|
||
| private func fetchVoltageMeasurements( | ||
| for sample: HKElectrocardiogram, | ||
| healthKit: HealthKit | ||
| ) async throws -> [Double] { | ||
| try await withCheckedThrowingContinuation { continuation in | ||
| var voltages: [Double] = [] | ||
| var didResume = false | ||
|
|
||
| let query = HKElectrocardiogramQuery(electrocardiogram: sample) { _, measurement, done, error in | ||
| if let error, !didResume { | ||
| didResume = true | ||
| continuation.resume(throwing: error) | ||
| return | ||
| } | ||
|
|
||
| if let measurement, | ||
| let voltageQuantity = measurement.quantity(for: .appleWatchSimilarToLeadI) { | ||
| voltages.append(voltageQuantity.doubleValue(for: .volt())) | ||
| } | ||
|
|
||
| if done, !didResume { | ||
| didResume = true | ||
| continuation.resume(returning: voltages) | ||
| } | ||
| } | ||
|
|
||
| healthKit.healthStore.execute(query) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,4 +11,92 @@ import Spezi | |
|
|
||
| class HealthContextGenerator: DefaultInitializable, Module, EnvironmentAccessible { | ||
| required init() { } | ||
|
|
||
| func buildSystemPrompt(userInfo: UserInfo?, electrocardiograms: [ElectrocardiogramData]) -> HealthyLLMContextEntity { | ||
| var prompt = """ | ||
| You are a health assistant. Provide a careful, non-diagnostic interpretation of the user's health data. | ||
| If ECG data is present, focus on signal quality, rhythm regularity, heart rate, and any notable abnormalities. | ||
| Do not claim to diagnose a condition. | ||
| """ | ||
|
|
||
| if let userInfo { | ||
| prompt += "\n\nUser profile:\n" | ||
| prompt += userInfo.asJSONRepresentation(.prettyPrinted) ?? "No Data" | ||
| } | ||
|
|
||
| if !electrocardiograms.isEmpty { | ||
| prompt += "\n\nLatest ECG samples:\n" | ||
| prompt += electrocardiograms.enumerated().map { index, sample in | ||
| formatECGSample(sample, index: index + 1) | ||
| }.joined(separator: "\n\n") | ||
| } else { | ||
| prompt += "\n\nLatest ECG samples: No Data" | ||
| } | ||
|
|
||
| prompt += "\n\nRespond with a concise clinical-style summary and a safety note if the tracing looks concerning." | ||
|
|
||
| return .init(.system, content: prompt) | ||
| } | ||
|
|
||
| private func formatECGSample(_ sample: ElectrocardiogramData, index: Int) -> String { | ||
| let voltages = sample.voltages | ||
| let average = voltages.isEmpty ? nil : voltages.reduce(0, +) / Double(voltages.count) | ||
| let minimum = voltages.min() | ||
| let maximum = voltages.max() | ||
| let preview = voltages.prefix(24).map { String(format: "%.4f", $0) }.joined(separator: ", ") | ||
|
|
||
| let normalizedVoltages = zNormalize(voltages) | ||
| let normalizedPreview = normalizedVoltages.prefix(48).map { String(format: "%.6f", $0) }.joined(separator: ", ") | ||
|
|
||
| let averageHeartRateText = sample.averageHeartRate.map(String.init(describing:)) ?? "No Data" | ||
| let samplingFrequencyText = sample.samplingFrequency.map(String.init(describing:)) ?? "No Data" | ||
| let voltageMeanText = average.map(String.init(describing:)) ?? "No Data" | ||
| let voltageMinText = minimum.map(String.init(describing:)) ?? "No Data" | ||
| let voltageMaxText = maximum.map(String.init(describing:)) ?? "No Data" | ||
|
|
||
| let sleepCotStylePrompt = """ | ||
| sleep_cot_style_sample: | ||
| pre_prompt: You are given a short single-lead ECG time series segment. Analyze rhythm, signal quality, and notable concerns conservatively. | ||
| time_series_text: | ||
| - The following is the ECG time series with mean \(String(format: "%.6f", average ?? 0)) and min/max \(String(format: "%.6f", minimum ?? 0))/\(String(format: "%.6f", maximum ?? 0)). | ||
| time_series_normalized_preview: [\(normalizedPreview)] | ||
| post_prompt: First summarize waveform quality and rhythm regularity, then provide brief safety guidance and when to seek care. | ||
| """ | ||
|
Comment on lines
+48
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Treat missing voltage arrays as missing data, not zeros. When 🩺 Safer handling for empty ECG samples- let sleepCotStylePrompt = """
- sleep_cot_style_sample:
- pre_prompt: You are given a short single-lead ECG time series segment. Analyze rhythm, signal quality, and notable concerns conservatively.
- time_series_text:
- - The following is the ECG time series with mean \(String(format: "%.6f", average ?? 0)) and min/max \(String(format: "%.6f", minimum ?? 0))/\(String(format: "%.6f", maximum ?? 0)).
- time_series_normalized_preview: [\(normalizedPreview)]
- post_prompt: First summarize waveform quality and rhythm regularity, then provide brief safety guidance and when to seek care.
- """
+ let sleepCotStylePrompt: String
+ if voltages.isEmpty {
+ sleepCotStylePrompt = """
+ sleep_cot_style_sample:
+ time_series_text:
+ - No voltage samples available.
+ """
+ } else {
+ sleepCotStylePrompt = """
+ sleep_cot_style_sample:
+ pre_prompt: You are given a short single-lead ECG time series segment. Analyze rhythm, signal quality, and notable concerns conservatively.
+ time_series_text:
+ - The following is the ECG time series with mean \(String(format: "%.6f", average!)) and min/max \(String(format: "%.6f", minimum!))/\(String(format: "%.6f", maximum!)).
+ time_series_normalized_preview: [\(normalizedPreview)]
+ post_prompt: First summarize waveform quality and rhythm regularity, then provide brief safety guidance and when to seek care.
+ """
+ }🧰 Tools🪛 GitHub Check: SwiftLint / SwiftLint[failure] 59-59: 🤖 Prompt for AI Agents |
||
|
|
||
| var lines: [String] = [ | ||
| "ECG sample #\(index)", | ||
| "start_date: \(sample.startDate.formatted(date: .abbreviated, time: .shortened))", | ||
| "end_date: \(sample.endDate.formatted(date: .abbreviated, time: .shortened))", | ||
| "classification: \(sample.classification)", | ||
| "symptoms_status: \(sample.symptomsStatus)", | ||
| "average_heart_rate: \(averageHeartRateText)", | ||
| "sampling_frequency_hz: \(samplingFrequencyText)", | ||
| "voltage_count: \(sample.numberOfVoltageMeasurements)", | ||
| "voltage_mean: \(voltageMeanText)", | ||
| "voltage_min: \(voltageMinText)", | ||
| "voltage_max: \(voltageMaxText)" | ||
| ] | ||
|
|
||
| if !preview.isEmpty { | ||
| lines.append("voltage_preview: [\(preview)]") | ||
| } | ||
|
|
||
| lines.append(sleepCotStylePrompt) | ||
|
|
||
| return lines.joined(separator: "\n") | ||
| } | ||
|
|
||
| private func zNormalize(_ values: [Double]) -> [Double] { | ||
| guard !values.isEmpty else { | ||
| return [] | ||
| } | ||
|
|
||
| let mean = values.reduce(0, +) / Double(values.count) | ||
| let variance = values.reduce(0) { partial, value in | ||
| let delta = value - mean | ||
| return partial + delta * delta | ||
| } / Double(values.count) | ||
| let std = max(sqrt(variance), 1e-6) | ||
| return values.map { ($0 - mean) / std } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.