Skip to content

Add LLM function calling for dynamic HealthKit data - #58

Open
ahmedrezik wants to merge 35 commits into
StanfordBDHG:mainfrom
ahmedrezik:feature/llm-function-calling
Open

ahmedrezik wants to merge 35 commits into
StanfordBDHG:mainfrom
ahmedrezik:feature/llm-function-calling

Conversation

@ahmedrezik

@ahmedrezik ahmedrezik commented Feb 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Replace the static 14-day data dump with on-demand LLM tool use (function calling) for OpenAI sessions
  • Add two LLM functions: get_health_metric (fetch any metric, 1-90 days) and compare_periods (compare two time ranges); the available metrics are exposed through the metric parameter's enum schema
  • Add HealthMetric enum mapping metric names to HealthKit identifiers/units
  • Refactor HealthDataFetcher to use SpeziHealthKit statisticsQuery() and query() APIs instead of raw HKStatisticsCollectionQueryDescriptor/HKSampleQueryDescriptor, leveraging the shared HealthKit module via @Dependency
  • Re-key fetchAndProcessHealthData by date so sparse statisticsQuery results no longer index out-of-bounds
  • Clip sleep sessions to each 3 PM–3 PM window in bucketSleepSessions so a session that straddles the boundary isn't double-counted
  • Make HealthDataFetcher final + @unchecked Sendable (matching SpeziHealthKit.HealthKit's pattern), dropping nonisolated(unsafe) from the LLM function structs
  • Fog/Local/Mock sessions remain unchanged (still use 14-day system prompt)
  • Add .callingTools UI indicator while the LLM fetches health data
  • Update onboarding text to reflect on-demand data fetching

Test plan

  • Build succeeds on iPhone 17 simulator (zero errors/warnings)
  • Unit tests pass: PromptGeneratorTests (rewritten on a deterministic fixture), ComparePeriodsFunctionTests (covers resolveRange / percentChange), HealthDataFetcherTests (covers bucketSleepSessions including the 3 PM-boundary clipping case)
  • Manual: OpenAI mode — ask "How were my steps last week?" — should see tool call then response
  • Manual: Fog/Local mode — should work exactly as before (14-day dump)
  • Manual: "Compare my sleep this week vs last week" — ComparePeriodsFunction called
  • Manual: Verify .callingTools indicator appears while tools execute
  • Manual: out-of-range days (e.g. 0 or 100) surfaces an invalidDateRange tool error to the model rather than silently clamping

🤖 Generated with Claude Code

@ahmedrezik
ahmedrezik force-pushed the feature/llm-function-calling branch 8 times, most recently from 6876f4a to 9e35724 Compare February 26, 2026 04:36
…tcher

Introduce HealthMetric enum mapping metric names to HKQuantityTypeIdentifier,
HKUnit, and HKStatisticsOptions. Add fetchQuantityData(for:unit:options:from:to:)
and fetchSleepData(from:to:) methods for on-demand date-range queries.
Create three LLMFunction implementations:
- GetHealthMetricFunction: fetch daily values for any metric over 1-90 days
- GetAvailableMetricsFunction: list all available health metrics
- ComparePeriodsFunction: compare a metric between two time periods
Add static buildToolUsePrompt() method that instructs the LLM to use
function calling tools to fetch health data on demand, rather than
having all data pre-loaded in the system prompt.
Add prepareLLMWithTools() for OpenAI sessions with function calling.
Update resetChat() to use static tool-use prompt for OpenAI sessions
and re-fetch data only for legacy (Fog/Local/Mock) sessions.
Configure LLMOpenAISchema with GetHealthMetricFunction,
GetAvailableMetricsFunction, and ComparePeriodsFunction for on-demand
HealthKit queries. Add overlay indicator when LLM is calling tools.
Non-OpenAI paths (Fog/Local/Mock) remain unchanged.
Verify buildToolUsePrompt() contains expected instructions, tool names,
and today's date.
Move @parameter attributes to same line as var declarations.
Add swiftlint:disable:next for long LLM function description strings.
- Move nested enums before static properties (type_contents_order)
- Extract chatContentView method after computed properties
- Change @parameter Int types to String (SpeziLLM BinaryInteger init
  is not public), parse in execute()
- Mark healthDataFetcher as nonisolated(unsafe) for Sendable conformance
- Remove superfluous swiftlint:disable commands
Replace '14 days will be uploaded' with 'fetched on demand when you
ask questions' to match the new tool-use architecture.
…n metadata

- HealthMetricTests: verify all 6 cases, raw value round-trip, HK
  identifier/unit/options mappings, cumulative vs discrete options,
  sleep nil properties, display names and descriptions
- GetAvailableMetricsFunctionTests: verify execute() returns all
  metrics with descriptions, and function name/description metadata
- LLMFunctionMetadataTests: verify function names, MetricType enum
  parity with HealthMetric, unique function names, and tool-use prompt
  references all function names

All 18 tests pass on iPhone 17 simulator.
Replace the standalone HKHealthStore with a @dependency on the shared
SpeziHealthKit HealthKit module. All quantity and sleep queries now use
healthKit.statisticsQuery() and healthKit.query() instead of raw
HKStatisticsCollectionQueryDescriptor and HKSampleQueryDescriptor.
Also adds a sampleType computed property to HealthMetric.
## ♻️ Current situation & Problem

The app requests HealthKit read permission for `.heartRate` but queries
  `.restingHeartRate` in `HealthDataFetcher`. These are distinct
`HKQuantityTypeIdentifier` values, so HealthKit silently returns no data
for
  resting heart rate, and all days report 0 bpm.

  Resolves StanfordBDHG#56 


  ## ⚙️ Release Notes

- Adds `.restingHeartRate` to the HealthKit `RequestReadAccess` quantity
array
   so the fetcher can retrieve resting heart rate data


  ## 📚 Documentation

  No documentation changes needed. This is a one-line permission fix in
  `HealthGPTAppDelegate.swift`.


  ## ✅ Testing

- Complete onboarding and verify the HealthKit permission dialog now
includes
  "Resting Heart Rate"
- Ask the LLM about resting heart rate and confirm it returns actual
values
  instead of 0 or "Data doesn't exist"


  ### Code of Conduct & Contributing Guidelines
By creating and submitting this pull request, you agree to follow our
[Code of


Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordBDHG/.github/blob/ma
  in/CONTRIBUTING.md):
  - [x] I agree to follow the [Code of

Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordBDHG/.github/blob/ma
  in/CONTRIBUTING.md).

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@PSchmiedmayer
PSchmiedmayer requested a review from Copilot March 25, 2026 08:52
@PSchmiedmayer

Copy link
Copy Markdown
Member

@coderabbitai Review

@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Mar 25, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds LLM tool-calling for on-demand HealthKit queries: two new functions (get_health_metric, compare_periods), a HealthMetric enum, refactors HealthDataFetcher for date-range queries and sleep bucketing, updates prompt logic for tool-use, and integrates tools into the UI and LLM preparation.

Changes

Cohort / File(s) Summary
New LLM Functions
HealthGPT/HealthGPT/Functions/GetHealthMetricFunction.swift, HealthGPT/HealthGPT/Functions/ComparePeriodsFunction.swift
Adds get_health_metric and compare_periods LLM functions that call the fetcher to return formatted daily values (1–90 days) and period comparisons (averages, delta, percent change), with input validation and error propagation.
Health Metric Definition
HealthGPT/HealthGPT/HealthMetric.swift
Introduces HealthMetric enum mapping metrics to HealthKit sample types, aggregation options, unit labels, and display names (includes sleep special-casing).
Health Data Fetcher
HealthGPT/HealthGPT/HealthDataFetcher.swift
Replaces internal HKHealthStore with injected healthKit; adds fetchQuantityData(for:aggregatedBy:from:to:) and fetchSleepData(from:to:); refactors last-two-weeks helpers to reuse generic fetchers; removes older predicate-based helpers.
Errors
HealthGPT/HealthGPT/HealthDataFetcherError.swift
Adds unsupportedMetric and invalidDateRange error cases for function/reporting errors.
LLM / Prompting
HealthGPT/HealthGPT/HealthDataInterpreter.swift, HealthGPT/HealthGPT/PromptGenerator.swift
Adds useToolPrompt flag and parameter to prepareLLM; builds system prompt conditionally via buildPrompt(usesTools:) (prompt no longer fetches health data when tool-based), default prompt healthData == [].
UI & Integration
HealthGPT/HealthGPT/HealthGPTView.swift
Injects HealthDataFetcher, registers GetHealthMetricFunction and ComparePeriodsFunction in the LLM schema, calls prepareLLM(..., useToolPrompt: true), extracts chat UI into helper, and suppresses triggers while .callingTools.
Localization
HealthGPT/Supporting Files/Localizable.xcstrings
Updates HEALTHKIT_PERMISSIONS_DESCRIPTION to reflect on-demand fetching; removes duplicate LLM Settings entry.
Tests
HealthGPTTests/PromptGeneratorTests.swift
Updates test to call PromptGenerator.buildPrompt(usesTools: false) and relax assertions to key substrings.
Other small changes
HealthGPT/HealthGPT/HealthDataInterpreter.swift imports
Adjusted imports: added SpeziLLMFog, removed SpeziLLMOpenAI.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant LLM as LLM
    participant GetFunc as GetHealthMetricFunction
    participant CompareFunc as ComparePeriodsFunction
    participant Fetcher as HealthDataFetcher
    participant HealthKit as HealthKit

    User->>LLM: Ask health question
    LLM->>GetFunc: call get_health_metric(metric, days)
    GetFunc->>Fetcher: fetchQuantityData / fetchSleepData (date range)
    Fetcher->>HealthKit: Query samples (date range)
    HealthKit-->>Fetcher: Samples
    Fetcher-->>GetFunc: [(date, value)] or [(date, hours)]
    GetFunc-->>LLM: Formatted daily results

    LLM->>CompareFunc: call compare_periods(metric, p1Start, p1End, p2Start, p2End)
    CompareFunc->>Fetcher: fetchAverage (period1)
    Fetcher->>HealthKit: Query period1 samples
    HealthKit-->>Fetcher: Samples
    Fetcher-->>CompareFunc: period1 average
    CompareFunc->>Fetcher: fetchAverage (period2)
    Fetcher->>HealthKit: Query period2 samples
    HealthKit-->>Fetcher: Samples
    Fetcher-->>CompareFunc: period2 average
    CompareFunc-->>LLM: Comparison string (avg, delta, percent)
    LLM-->>User: Response using fetched data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • Issue #57: Implements LLM function-calling and on-demand HealthKit queries (adds get_health_metric/compare_periods, refactors fetcher, and tool-use prompts), matching the requested dynamic HealthKit tool feature.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: adding LLM function calling for dynamic HealthKit data fetching, which is the primary objective of replacing the static 14-day data dump with on-demand tool use.
Description check ✅ Passed The pull request description clearly relates to the changeset, outlining the shift from static 14-day data dumps to on-demand LLM function calling, the addition of new LLM functions and HealthMetric enum, refactoring of HealthDataFetcher, and UI updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds OpenAI function-calling support to fetch HealthKit metrics on demand (replacing the static 14‑day dump), including new tool prompts, metric mappings, and UI feedback while tools execute.

Changes:

  • Introduces tool-use system prompt and three LLM functions (get_health_metric, get_available_metrics, compare_periods).
  • Adds HealthMetric enum mapping metrics to HealthKit identifiers/units and refactors HealthDataFetcher onto SpeziHealthKit queries.
  • Updates UI to show a .callingTools indicator and adjusts onboarding/localization text for on-demand fetching.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
HealthGPTTests/PromptGeneratorTests.swift Adds coverage for the new tool-use system prompt.
HealthGPTTests/LLMFunctionMetadataTests.swift Verifies tool metadata and prompt references for function calling.
HealthGPTTests/HealthMetricTests.swift Adds tests for HealthMetric mappings and invariants.
HealthGPTTests/GetAvailableMetricsFunctionTests.swift Tests execution/metadata for get_available_metrics.
HealthGPT/Supporting Files/Localizable.xcstrings Adds/updates strings for tool-calling indicator and OpenAI onboarding copy.
HealthGPT/HealthGPT/PromptGenerator.swift Adds buildToolUsePrompt() for function-calling sessions.
HealthGPT/HealthGPT/HealthMetric.swift Introduces metric-to-HealthKit mapping (identifiers, units, options).
HealthGPT/HealthGPT/HealthGPTView.swift Wires OpenAI tools into schema + shows .callingTools overlay indicator.
HealthGPT/HealthGPT/HealthDataInterpreter.swift Adds tool-based session preparation and adjusts reset behavior for OpenAI sessions.
HealthGPT/HealthGPT/HealthDataFetcher.swift Switches to SpeziHealthKit APIs and adds flexible date-range fetch helpers.
HealthGPT/HealthGPT/Functions/GetHealthMetricFunction.swift Adds tool to fetch metric series (1–90 days).
HealthGPT/HealthGPT/Functions/GetAvailableMetricsFunction.swift Adds tool to list available metrics.
HealthGPT/HealthGPT/Functions/ComparePeriodsFunction.swift Adds tool to compare two time periods for a metric.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +66 to +72
let percentChange = avg2 != 0 ? (diff / avg2) * 100 : 0

return """
\(healthMetric.displayName) comparison:
Period 1 (\(period1Label)): avg \(String(format: "%.1f", avg1))
Period 2 (\(period2Label)): avg \(String(format: "%.1f", avg2))
Difference: \(String(format: "%+.1f", diff)) (\(String(format: "%+.1f", percentChange))%)

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When avg2 == 0 and avg1 != 0, returning 0% change is mathematically incorrect and will mislead users. Consider representing percent change as “N/A”/nil for a zero baseline (or use an alternate representation), while still reporting the absolute difference.

Suggested change
let percentChange = avg2 != 0 ? (diff / avg2) * 100 : 0
return """
\(healthMetric.displayName) comparison:
Period 1 (\(period1Label)): avg \(String(format: "%.1f", avg1))
Period 2 (\(period2Label)): avg \(String(format: "%.1f", avg2))
Difference: \(String(format: "%+.1f", diff)) (\(String(format: "%+.1f", percentChange))%)
let percentChangeString: String
if avg2 != 0 {
let percentChange = (diff / avg2) * 100
percentChangeString = String(format: "%+.1f%%", percentChange)
} else {
percentChangeString = "N/A"
}
return """
\(healthMetric.displayName) comparison:
Period 1 (\(period1Label)): avg \(String(format: "%.1f", avg1))
Period 2 (\(period2Label)): avg \(String(format: "%.1f", avg2))
Difference: \(String(format: "%+.1f", diff)) (\(percentChangeString))

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +34
@Parameter(description: "Number of past days to fetch (1-90)") var days: String

nonisolated(unsafe) let healthDataFetcher: HealthDataFetcher

func execute() async throws -> String? {
let clampedDays = max(1, min(Int(days) ?? 7, 90))

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declaring numeric tool parameters as String will publish a string-typed schema to OpenAI function calling, which tends to decrease tool-call reliability and forces manual parsing. Prefer an integer parameter type (e.g., Int) for days (and similarly for the period parameters in ComparePeriodsFunction) and clamp/validate as needed.

Suggested change
@Parameter(description: "Number of past days to fetch (1-90)") var days: String
nonisolated(unsafe) let healthDataFetcher: HealthDataFetcher
func execute() async throws -> String? {
let clampedDays = max(1, min(Int(days) ?? 7, 90))
@Parameter(description: "Number of past days to fetch (1-90)") var days: Int
nonisolated(unsafe) let healthDataFetcher: HealthDataFetcher
func execute() async throws -> String? {
let clampedDays = max(1, min(days, 90))

Copilot uses AI. Check for mistakes.

@Parameter(description: "Number of past days to fetch (1-90)") var days: String

nonisolated(unsafe) let healthDataFetcher: HealthDataFetcher

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nonisolated(unsafe) opts out of Swift concurrency safety and can allow cross-actor access to HealthDataFetcher from arbitrary executors, which is risky for an @Observable module that uses dependencies. A safer approach is to run the fetch on a known actor (e.g., mark execute() as @MainActor if HealthDataFetcher is main-actor bound, or wrap calls in await MainActor.run { ... }, or make the fetcher an actor/Sendable-safe wrapper).

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +53
let toolUsePrompt = PromptGenerator.buildToolUsePrompt()
let today = DateFormatter.localizedString(from: Date(), dateStyle: .full, timeStyle: .none)

#expect(toolUsePrompt.contains("HealthGPT"))
#expect(toolUsePrompt.contains("Today is \(today)"))

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test can be flaky around midnight because buildToolUsePrompt() and the test compute Date() separately (potentially yielding different “Today is …” values). Consider injecting a fixed date into buildToolUsePrompt (or checking for the "Today is " prefix rather than an exact localized date string) to make the test deterministic.

Copilot uses AI. Check for mistakes.
}
},
"Fetching your health data..." : {

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new string key has an empty definition, which makes localization behavior unclear and inconsistent with other entries that define localizations (and it’s also inconsistent with the project’s existing key style like WELCOME_TITLE). Consider adding at least an en localization entry (and optionally switching to a stable key like FETCHING_HEALTH_DATA while keeping the displayed value in the localization payload).

Suggested change
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fetching your health data..."
}
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +35
@Parameter(description: "Start of period 1 in days ago (e.g. 7 means 7 days ago)") var period1Start: String

@Parameter(description: "End of period 1 in days ago (e.g. 0 means today)") var period1End: String

@Parameter(description: "Start of period 2 in days ago") var period2Start: String

@Parameter(description: "End of period 2 in days ago") var period2End: String

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The period parsing/bounds behavior (string-to-int parsing, swapping start/end via max/min, and the “0 means today” semantics) is core tool behavior but isn’t covered by tests in this PR. Adding unit tests for common/edge inputs (e.g., non-numeric strings, reversed ranges, and ensuring “today” is excluded if that’s the intended contract) would help prevent regressions.

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
HealthGPT/Supporting Files/Localizable.xcstrings (1)

80-85: ⚠️ Potential issue | 🟠 Major

Make this privacy disclosure backend-specific.

Line 85 says HealthKit data is fetched on demand and sent to OpenAI, but this PR keeps Local/Fog/Mock on the legacy 14-day flow and Local/Fog do not send data to OpenAI at all. That makes the onboarding copy false in supported configurations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@HealthGPT/Supporting` Files/Localizable.xcstrings around lines 80 - 85, The
HealthKit permission copy in the HEALTHKIT_PERMISSIONS_DESCRIPTION string is
inaccurate for Local/Fog/Mock backends; create backend-specific localized
strings (e.g., HEALTHKIT_PERMISSIONS_DESCRIPTION_OPENAI and
HEALTHKIT_PERMISSIONS_DESCRIPTION_LOCAL) or make the text conditional to avoid
claiming data is sent to OpenAI, update the onboarding code that displays
HEALTHKIT_PERMISSIONS_DESCRIPTION to instead select the appropriate key based on
the current backend (Local/Fog/Mock vs OpenAI), and ensure Local/Fog/Mock
variants explicitly state data is kept local or not sent to OpenAI.
🧹 Nitpick comments (4)
HealthGPT/HealthGPT/HealthDataFetcher.swift (1)

227-234: Fetch sleep samples once per range, not once per day.

The healthKit.query call on Lines 230-234 sits inside the day loop, so a 90-day request becomes 90 sleep queries and compare_periods can double that. That will make sleep tool calls much slower than the quantity path.

Please verify with a long-range sleep prompt that query count and latency stay acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@HealthGPT/HealthGPT/HealthDataFetcher.swift` around lines 227 - 234, The
sleep HK query is being executed inside the per-day loop (using asleepPredicate
and healthKit.query) causing N queries for N days; instead, run a single
healthKit.query for the full requested time span (use the overall
HealthKitQueryTimeRange covering startOfSleep for the first day through
endOfSleep for the last day), store the returned results in the results
variable, then inside the day loop filter those cached results into per-day
buckets by sample timestamps; ensure the same cached results are reused if
compare_periods runs to avoid duplicate queries and maintain existing
asleepPredicate and any downstream filtering logic that uses the sample
timestamps.
HealthGPT/HealthGPT/HealthDataInterpreter.swift (1)

72-84: Type-based prompt selection may be fragile for future extensibility.

The check llm is LLMOpenAISession assumes all OpenAI sessions use tools. If someone later creates an OpenAI session via prepareLLM(with:) (without tools), resetChat() would incorrectly use the tool-use prompt.

Consider storing a flag or the prompt type explicitly during session creation, rather than inferring from the session type.

♻️ Suggested approach
 `@Observable`
 class HealthDataInterpreter: DefaultInitializable, Module, EnvironmentAccessible {
     `@ObservationIgnored` `@Dependency`(LLMRunner.self) private var llmRunner
     `@ObservationIgnored` `@Dependency`(HealthDataFetcher.self) private var healthDataFetcher
     
     var llm: (any LLMSession)?
     `@ObservationIgnored` private var systemPrompt = ""
+    `@ObservationIgnored` private var usesToolBasedPrompt = false

Then set usesToolBasedPrompt = true in prepareLLMWithTools and use that flag in resetChat().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@HealthGPT/HealthGPT/HealthDataInterpreter.swift` around lines 72 - 84, The
type-check in resetChat() is brittle; add a stored Bool flag (e.g.,
usesToolBasedPrompt) to the HealthDataInterpreter and set it to true inside
prepareLLMWithTools (and false in other prepareLLM(...) paths), then update
resetChat() to choose between PromptGenerator.buildToolUsePrompt() and await
generateSystemPrompt() based on usesToolBasedPrompt instead of llm is
LLMOpenAISession; ensure systemPrompt, llm?.context.reset(), and
llm?.context.append(systemMessage:) behavior remains unchanged.
HealthGPT/HealthGPT/Functions/GetHealthMetricFunction.swift (2)

63-64: Quantity metric output lacks unit labels.

Sleep data output includes "hours" (line 46), but quantity metrics don't include their units. This inconsistency may confuse the LLM when interpreting values.

♻️ Suggested fix to include unit labels
-            let lines = data.map { "\(dateFormatter.string(from: $0.date)): \(String(format: "%.1f", $0.value))" }
+            let unitLabel = healthMetric.unitLabel ?? ""
+            let lines = data.map { "\(dateFormatter.string(from: $0.date)): \(String(format: "%.1f", $0.value)) \(unitLabel)" }

You would need to add a unitLabel property to HealthMetric that returns user-friendly strings like "steps", "calories", "minutes", "lbs", "bpm".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@HealthGPT/HealthGPT/Functions/GetHealthMetricFunction.swift` around lines 63
- 64, The output for quantity metrics omits unit labels, causing inconsistency
with Sleep which prints "hours"; add a unitLabel property to the HealthMetric
type (e.g., returning "steps", "calories", "minutes", "lbs", "bpm") and update
GetHealthMetricFunction's formatting (the block using dateFormatter,
healthMetric.displayName, clampedDays, and the data.map closure) to append the
metric's unitLabel to each value (e.g., format values as "12.3 bpm" or "100
steps") so all metrics consistently include units in the returned string.

14-21: MetricType duplicates HealthMetric cases—code duplication persists but is verified via tests.

MetricType mirrors HealthMetric exactly. If a new metric is added to HealthMetric, this enum must be updated manually or the new metric won't be available via the LLM function.

The duplication is already monitored by tests in LLMFunctionMetadataTests.swift that verify MetricType.allCases matches HealthMetric.allCases, so drift will be caught at test time. Consider refactoring to reduce the duplication itself through protocol conformance or a shared definition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@HealthGPT/HealthGPT/Functions/GetHealthMetricFunction.swift` around lines 14
- 21, MetricType duplicates HealthMetric; remove the duplicated enum by having
HealthMetric itself conform to LLMFunctionParameterEnum (and
CaseIterable/RawRepresentable if required) and update GetHealthMetricFunction to
use HealthMetric instead of MetricType so new metrics are automatically
available; ensure LLMFunctionMetadataTests still compares HealthMetric.allCases
to the expected set and adjust any references to MetricType to HealthMetric.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@HealthGPT/HealthGPT/Functions/ComparePeriodsFunction.swift`:
- Around line 65-66: The percent-change computation in ComparePeriodsFunction
(variables diff, avg1, avg2, percentChange) treats avg2 == 0 as 0% which is
misleading; change percentChange to an optional (e.g., Double?) and set it to
nil when avg2 == 0 (or otherwise return a sentinel like .none), and update the
downstream formatting/return logic that consumes percentChange to render "N/A"
(or "—") when percentChange is nil instead of printing 0.0%; keep the numeric
calculation unchanged for avg2 != 0 ((diff / avg2) * 100) and ensure any callers
of percentChange handle the optional safely.

In `@HealthGPT/HealthGPT/HealthDataFetcher.swift`:
- Around line 217-223: The loop that builds sleep buckets uses an inclusive
upper bound (while currentDay <= endDay) which creates an extra sleep window
compared to other metrics; change the loop to use an exclusive upper bound
(while currentDay < endDay) or otherwise adjust the endDay used for sleep (e.g.,
subtract one day) so sleep windows match the exclusive endDate semantics used
elsewhere; update the loop controlling symbol (currentDay, endDay) and keep the
existing calendar.date(bySettingHour:..., of: previousDay/ currentDay) logic
intact so get_health_metric/compare_periods produce consistent ranges.
- Around line 14-18: HealthDataFetcher currently hard-depends on HealthKit via
`@Dependency`(HealthKit.self) but HealthKit is registered only conditionally
(HKHealthStore.isHealthDataAvailable()), causing failures when HealthKit is
absent; change HealthDataFetcher to not require a guaranteed HealthKit
dependency by making the dependency optional or injected at init (e.g., replace
`@Dependency`(HealthKit.self) with an optional/nullable dependency or add
init(healthKit: HealthKit?) and store it), and update callers (the OpenAI tool
users GetHealthMetricFunction and ComparePeriodsFunction and the unconditional
HealthDataFetcher() instantiation) to either pass nil when HealthKit is
unavailable or guard early for healthKit == nil before using it; ensure any
HealthKit usage inside HealthDataFetcher checks for nil and fails gracefully.

In `@HealthGPT/HealthGPT/HealthGPTView.swift`:
- Around line 142-149: The onChange handler that increments messageTaskId should
also exclude the LLM's .callingTools state so tool executions don't get
interrupted: update the condition where you check llm.state (in the closure
watching llm.context and the logic that currently checks llm.state !=
.generating) to also ensure llm.state != .callingTools before bumping
messageTaskId, so the .task(id: self.messageTaskId) that calls
healthDataInterpreter.queryLLM() won't be cancelled/restarted while tools are
being invoked and their results appended to llm.context.

In `@HealthGPTTests/HealthMetricTests.swift`:
- Around line 9-12: Reorder the import statements in HealthMetricTests.swift to
satisfy SwiftLint's alphabetical import rule: place regular imports
alphabetically (Foundation then HealthKit), and move the testable import
"@testable import HealthGPT" after the regular imports; ensure the final order
is import Foundation, import HealthKit, `@testable` import HealthGPT, import
Testing so that `@testable` stays after the regular imports.

---

Outside diff comments:
In `@HealthGPT/Supporting` Files/Localizable.xcstrings:
- Around line 80-85: The HealthKit permission copy in the
HEALTHKIT_PERMISSIONS_DESCRIPTION string is inaccurate for Local/Fog/Mock
backends; create backend-specific localized strings (e.g.,
HEALTHKIT_PERMISSIONS_DESCRIPTION_OPENAI and
HEALTHKIT_PERMISSIONS_DESCRIPTION_LOCAL) or make the text conditional to avoid
claiming data is sent to OpenAI, update the onboarding code that displays
HEALTHKIT_PERMISSIONS_DESCRIPTION to instead select the appropriate key based on
the current backend (Local/Fog/Mock vs OpenAI), and ensure Local/Fog/Mock
variants explicitly state data is kept local or not sent to OpenAI.

---

Nitpick comments:
In `@HealthGPT/HealthGPT/Functions/GetHealthMetricFunction.swift`:
- Around line 63-64: The output for quantity metrics omits unit labels, causing
inconsistency with Sleep which prints "hours"; add a unitLabel property to the
HealthMetric type (e.g., returning "steps", "calories", "minutes", "lbs", "bpm")
and update GetHealthMetricFunction's formatting (the block using dateFormatter,
healthMetric.displayName, clampedDays, and the data.map closure) to append the
metric's unitLabel to each value (e.g., format values as "12.3 bpm" or "100
steps") so all metrics consistently include units in the returned string.
- Around line 14-21: MetricType duplicates HealthMetric; remove the duplicated
enum by having HealthMetric itself conform to LLMFunctionParameterEnum (and
CaseIterable/RawRepresentable if required) and update GetHealthMetricFunction to
use HealthMetric instead of MetricType so new metrics are automatically
available; ensure LLMFunctionMetadataTests still compares HealthMetric.allCases
to the expected set and adjust any references to MetricType to HealthMetric.

In `@HealthGPT/HealthGPT/HealthDataFetcher.swift`:
- Around line 227-234: The sleep HK query is being executed inside the per-day
loop (using asleepPredicate and healthKit.query) causing N queries for N days;
instead, run a single healthKit.query for the full requested time span (use the
overall HealthKitQueryTimeRange covering startOfSleep for the first day through
endOfSleep for the last day), store the returned results in the results
variable, then inside the day loop filter those cached results into per-day
buckets by sample timestamps; ensure the same cached results are reused if
compare_periods runs to avoid duplicate queries and maintain existing
asleepPredicate and any downstream filtering logic that uses the sample
timestamps.

In `@HealthGPT/HealthGPT/HealthDataInterpreter.swift`:
- Around line 72-84: The type-check in resetChat() is brittle; add a stored Bool
flag (e.g., usesToolBasedPrompt) to the HealthDataInterpreter and set it to true
inside prepareLLMWithTools (and false in other prepareLLM(...) paths), then
update resetChat() to choose between PromptGenerator.buildToolUsePrompt() and
await generateSystemPrompt() based on usesToolBasedPrompt instead of llm is
LLMOpenAISession; ensure systemPrompt, llm?.context.reset(), and
llm?.context.append(systemMessage:) behavior remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cc1687b5-a596-489a-8628-506e7a9534e9

📥 Commits

Reviewing files that changed from the base of the PR and between 6e8b3db and 9034ba3.

📒 Files selected for processing (13)
  • HealthGPT/HealthGPT/Functions/ComparePeriodsFunction.swift
  • HealthGPT/HealthGPT/Functions/GetAvailableMetricsFunction.swift
  • HealthGPT/HealthGPT/Functions/GetHealthMetricFunction.swift
  • HealthGPT/HealthGPT/HealthDataFetcher.swift
  • HealthGPT/HealthGPT/HealthDataInterpreter.swift
  • HealthGPT/HealthGPT/HealthGPTView.swift
  • HealthGPT/HealthGPT/HealthMetric.swift
  • HealthGPT/HealthGPT/PromptGenerator.swift
  • HealthGPT/Supporting Files/Localizable.xcstrings
  • HealthGPTTests/GetAvailableMetricsFunctionTests.swift
  • HealthGPTTests/HealthMetricTests.swift
  • HealthGPTTests/LLMFunctionMetadataTests.swift
  • HealthGPTTests/PromptGeneratorTests.swift

Comment on lines 14 to 18
@Observable
class HealthDataFetcher: DefaultInitializable, Module, EnvironmentAccessible {
@ObservationIgnored private let healthStore = HKHealthStore()
@ObservationIgnored @Dependency(HealthKit.self) private var healthKit

required init() { }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Expectation:
# - HealthKit registration is conditional.
# - HealthDataFetcher and OpenAI tool wiring are unconditional.
rg -n -C2 'HKHealthStore\.isHealthDataAvailable|HealthDataFetcher\(\)|@Dependency\(HealthKit\.self\)|GetHealthMetricFunction|ComparePeriodsFunction' \
  "HealthGPT/HealthGPTAppDelegate.swift" \
  "HealthGPT/HealthGPT/HealthDataFetcher.swift" \
  "HealthGPT/HealthGPT/HealthGPTView.swift"

Repository: StanfordBDHG/HealthGPT

Length of output: 1967


Don't require HealthKit here unless it is always registered.

HealthDataFetcher declares a hard dependency on HealthKit via @Dependency(HealthKit.self) at line 16, but in HealthGPTAppDelegate.swift the HealthKit module is only registered conditionally when HKHealthStore.isHealthDataAvailable() returns true (lines 24–26). However, HealthDataFetcher() is instantiated unconditionally at line 34, and the OpenAI tool functions that use it (GetHealthMetricFunction and ComparePeriodsFunction at HealthGPTView.swift lines 61, 63) are also unconditional. This creates a missing-dependency failure path on devices without HealthKit support.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@HealthGPT/HealthGPT/HealthDataFetcher.swift` around lines 14 - 18,
HealthDataFetcher currently hard-depends on HealthKit via
`@Dependency`(HealthKit.self) but HealthKit is registered only conditionally
(HKHealthStore.isHealthDataAvailable()), causing failures when HealthKit is
absent; change HealthDataFetcher to not require a guaranteed HealthKit
dependency by making the dependency optional or injected at init (e.g., replace
`@Dependency`(HealthKit.self) with an optional/nullable dependency or add
init(healthKit: HealthKit?) and store it), and update callers (the OpenAI tool
users GetHealthMetricFunction and ComparePeriodsFunction and the unconditional
HealthDataFetcher() instantiation) to either pass nil when HealthKit is
unavailable or guard early for healthKit == nil before using it; ensure any
HealthKit usage inside HealthDataFetcher checks for nil and fails gracefully.

Comment thread HealthGPT/HealthGPT/HealthGPTView.swift Resolved
Comment thread HealthGPTTests/HealthMetricTests.swift Resolved
@codecov

codecov Bot commented Mar 25, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.70%. Comparing base (6e8b3db) to head (66bfe38).

Files with missing lines Patch % Lines
...T/HealthGPT/Functions/ComparePeriodsFunction.swift 0.00% 11 Missing ⚠️
HealthGPT/HealthGPT/HealthDataFetcher.swift 10.00% 9 Missing ⚠️
.../HealthGPT/Functions/GetHealthMetricFunction.swift 0.00% 5 Missing ⚠️
HealthGPT/HealthGPT/HealthGPTView.swift 83.34% 2 Missing ⚠️
HealthGPT/HealthGPT/HealthMetric.swift 83.34% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #58      +/-   ##
==========================================
- Coverage   89.00%   80.70%   -8.30%     
==========================================
  Files          30       37       +7     
  Lines         200      259      +59     
==========================================
+ Hits          178      209      +31     
- Misses         22       50      +28     
Files with missing lines Coverage Δ
...lthGPT/Functions/GetAvailableMetricsFunction.swift 100.00% <100.00%> (ø)
HealthGPT/HealthGPT/HealthDataInterpreter.swift 100.00% <100.00%> (ø)
HealthGPT/HealthGPT/PromptGenerator.swift 100.00% <100.00%> (ø)
...lthGPTTests/GetAvailableMetricsFunctionTests.swift 100.00% <100.00%> (ø)
HealthGPTTests/HealthMetricTests.swift 100.00% <100.00%> (ø)
HealthGPTTests/LLMFunctionMetadataTests.swift 100.00% <100.00%> (ø)
HealthGPTTests/PromptGeneratorTests.swift 100.00% <100.00%> (ø)
HealthGPT/HealthGPT/HealthMetric.swift 83.34% <83.34%> (ø)
HealthGPT/HealthGPT/HealthGPTView.swift 92.31% <83.34%> (-4.75%) ⬇️
.../HealthGPT/Functions/GetHealthMetricFunction.swift 0.00% <0.00%> (ø)
... and 2 more

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6e8b3db...66bfe38. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- Use SleepSession/splitIntoSleepSessions() for proper overlap handling
- Replace duplicate MetricType enum with shared HealthMetric (LLMFunctionParameterEnum)
- Use native Int @parameter types instead of String parsing
- Throw errors instead of returning error strings from LLM functions
- Add unitLabel property to HealthMetric for consistent output formatting
- Unify prompt generation with shared basePrompt to eliminate duplication
- Replace type-check (llm is LLMOpenAISession) with stored usesToolBasedPrompt flag
- Replace central overlay with navigation toolbar subtitle for tool-calling state
- Remove GetAvailableMetricsFunction (metrics auto-discovered from enum schema)
- Extract defaultLookbackDays constant to avoid hardcoded values
- Refactor HealthDataFetcher with flexible date-range queries and shared helpers
- Fix test import ordering and midnight flakiness in prompt tests
ahmedrezik added a commit to ahmedrezik/HealthGPT that referenced this pull request Apr 10, 2026
- Use SleepSession/splitIntoSleepSessions() for proper overlap handling
- Replace duplicate MetricType enum with shared HealthMetric (LLMFunctionParameterEnum)
- Use native Int @parameter types instead of String parsing
- Throw errors instead of returning error strings from LLM functions
- Add unitLabel property to HealthMetric for consistent output formatting
- Unify prompt generation with shared basePrompt to eliminate duplication
- Replace type-check (llm is LLMOpenAISession) with stored usesToolBasedPrompt flag
- Replace central overlay with navigation toolbar subtitle for tool-calling state
- Remove GetAvailableMetricsFunction (metrics auto-discovered from enum schema)
- Extract defaultLookbackDays constant to avoid hardcoded values
- Refactor HealthDataFetcher with flexible date-range queries and shared helpers
- Fix test import ordering and midnight flakiness in prompt tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests that just assert string literals or check every property 1:1
provide no regression value. Keep only buildMainPrompt which tests
real formatting logic.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
HealthGPT/HealthGPT/Functions/GetHealthMetricFunction.swift (1)

27-30: ⚠️ Potential issue | 🟠 Major

Reject out-of-range days instead of silently clamping it.

If the model asks for 120 days, this still returns 90 days of data and the tool call looks successful. That makes bad tool arguments indistinguishable from valid ones. Fail fast here so the model can retry with a legal 1...90 value.

Proposed fix
     func execute() async throws -> String? {
-        let clampedDays = max(1, min(days, 90))
+        guard (1...90).contains(days) else {
+            throw HealthDataFetcherError.invalidDateRange
+        }
+
         let endDate = Calendar.current.startOfDay(for: .now)
-        guard let startDate = Calendar.current.date(byAdding: .day, value: -clampedDays, to: endDate) else {
+        guard let startDate = Calendar.current.date(byAdding: .day, value: -days, to: endDate) else {
             throw HealthDataFetcherError.invalidDateRange
         }
@@
-            return "\(metric.displayName) for the last \(clampedDays) days:\n" + lines.joined(separator: "\n")
+            return "\(metric.displayName) for the last \(days) days:\n" + lines.joined(separator: "\n")
@@
-            return "\(metric.displayName) for the last \(clampedDays) days:\n" + lines.joined(separator: "\n")
+            return "\(metric.displayName) for the last \(days) days:\n" + lines.joined(separator: "\n")
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@HealthGPT/HealthGPT/Functions/GetHealthMetricFunction.swift` around lines 27
- 30, In GetHealthMetricFunction.execute(), don't silently clamp the input days;
validate that the property days is within 1...90 and if not throw a clear
validation error so the caller/model can retry with a legal value. Replace the
clampedDays logic with an explicit guard that checks (1...90).contains(days) and
throw a descriptive error (e.g., ValidationError.invalidParameter or a new
DaysOutOfRangeError) before computing endDate/startDate; keep the existing
startDate calculation (Calendar.current.date(byAdding: .day, value: -days, to:
endDate)) unchanged when days is valid.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@HealthGPT/HealthGPT/Functions/ComparePeriodsFunction.swift`:
- Around line 35-55: Reject any period offsets that are positive (future) and
validate start<=end; compute day-aligned ranges by using
Calendar.startOfDay(for:) on .now then subtracting whole days for starts and
using startOfDay(for: start).addingTimeInterval(24*60*60) for exclusive upper
bounds passed to fetchAverage (or subtract 1 second to make inclusive if
fetchAverage expects inclusive end); replace the current
period1StartDate/period1EndDate/period2StartDate/period2EndDate calculations
with these day-aligned computations (use Calendar.startOfDay and date(byAdding:
.day,...)), and update the period1Label/period2Label to show the inclusive
human-readable end day (formatting the startOfDay and endDayMinusOneSecond as
needed) so labels match the data fetched; ensure errors throw
HealthDataFetcherError.invalidDateRange when offsets are invalid.

In `@HealthGPT/HealthGPT/HealthDataFetcher.swift`:
- Around line 16-21: Reorder the type members so the initializer comes after the
stored properties: move the required init() below the stored dependency and
static property declarations (the `@ObservationIgnored`
`@Dependency`(HealthKit.self) private var healthKit and private static let
defaultLookbackDays) to satisfy SwiftLint's type_contents_order rule; ensure no
other members are split by the initializer and run lint to confirm the violation
is resolved.
- Around line 135-153: The two-week convenience fetches use Date.now and can
include partial current-day data; update fetchSleepData and
fetchLastTwoWeeksData to anchor to full-day boundaries by computing endDate as
Calendar.current.startOfDay(for: Date.now) and then deriving startDate by
subtracting Self.defaultLookbackDays from that startOfDay; update references to
fetchQuantityData/fetchSleepData to use these adjusted startDate and endDate so
the helpers match PromptGenerator.basePrompt/GetHealthMetricFunction semantics
and avoid leaking partial current-day values.

In `@HealthGPTTests/LLMFunctionMetadataTests.swift`:
- Around line 38-42: The test toolUsePromptReferencesAllFunctionNames asserts
that buildToolUsePrompt() contains specific function names
(GetHealthMetricFunction.name, ComparePeriodsFunction.name), but
buildToolUsePrompt() intentionally returns generic tool-use instructions while
actual function names are registered in the OpenAI schema (HealthGPTView.swift);
update the test by removing or disabling the two name-specific assertions in
toolUsePromptReferencesAllFunctionNames, and instead assert only on the generic
guidance expected from PromptGenerator.buildToolUsePrompt() (or add a new test
that validates the schema registration in HealthGPTView.swift if you want to
cover function names separately).

---

Duplicate comments:
In `@HealthGPT/HealthGPT/Functions/GetHealthMetricFunction.swift`:
- Around line 27-30: In GetHealthMetricFunction.execute(), don't silently clamp
the input days; validate that the property days is within 1...90 and if not
throw a clear validation error so the caller/model can retry with a legal value.
Replace the clampedDays logic with an explicit guard that checks
(1...90).contains(days) and throw a descriptive error (e.g.,
ValidationError.invalidParameter or a new DaysOutOfRangeError) before computing
endDate/startDate; keep the existing startDate calculation
(Calendar.current.date(byAdding: .day, value: -days, to: endDate)) unchanged
when days is valid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d4eabc77-c90a-44f9-aad2-49b1aca6b494

📥 Commits

Reviewing files that changed from the base of the PR and between 66bfe38 and 50935ea.

📒 Files selected for processing (12)
  • HealthGPT/HealthGPT/Functions/ComparePeriodsFunction.swift
  • HealthGPT/HealthGPT/Functions/GetHealthMetricFunction.swift
  • HealthGPT/HealthGPT/HealthDataFetcher.swift
  • HealthGPT/HealthGPT/HealthDataFetcherError.swift
  • HealthGPT/HealthGPT/HealthDataInterpreter.swift
  • HealthGPT/HealthGPT/HealthGPTView.swift
  • HealthGPT/HealthGPT/HealthMetric.swift
  • HealthGPT/HealthGPT/PromptGenerator.swift
  • HealthGPT/Supporting Files/Localizable.xcstrings
  • HealthGPTTests/HealthMetricTests.swift
  • HealthGPTTests/LLMFunctionMetadataTests.swift
  • HealthGPTTests/PromptGeneratorTests.swift
✅ Files skipped from review due to trivial changes (2)
  • HealthGPT/HealthGPT/HealthDataFetcherError.swift
  • HealthGPTTests/HealthMetricTests.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • HealthGPTTests/PromptGeneratorTests.swift
  • HealthGPT/Supporting Files/Localizable.xcstrings
  • HealthGPT/HealthGPT/HealthMetric.swift

…g indicator, reuse SpeziHealthKit metric definitions

- Merge buildToolUsePrompt/buildMainPrompt into single buildPrompt(usesTools:)
  and collapse duplicated branching in HealthDataInterpreter
- Replace custom toolbar subtitle with SpeziChat's built-in
  messagePendingAnimation typing indicator (matching LLMonFHIR pattern)
- Derive HealthMetric displayName/unitLabel from SpeziHealthKit's
  SampleType.displayTitle and displayUnit instead of hardcoding
- Remove unused CALLING_TOOLS_SUBTITLE localization and description property

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@HealthGPT/HealthGPT/HealthGPTView.swift`:
- Around line 60-63: The OpenAI schema registration only includes
GetHealthMetricFunction and ComparePeriodsFunction but omits the
get_available_metrics tool; update the LLMOpenAISchema initialization to also
register the get_available_metrics function by adding the corresponding function
registration (e.g., GetAvailableMetricsFunction or the actual
get_available_metrics function wrapper) alongside
GetHealthMetricFunction(healthDataFetcher: healthDataFetcher) and
ComparePeriodsFunction(healthDataFetcher: healthDataFetcher) so the model can
call it.

In `@HealthGPT/HealthGPT/PromptGenerator.swift`:
- Around line 16-21: Move the type property before the initializer to satisfy
SwiftLint's type_contents_order rule: place the private static var basePrompt
declaration above init(with healthData: [HealthData] = []) so that static/type
properties (basePrompt) appear before instance initializers (init(with:)).
Ensure any related comments or attributes for basePrompt remain with that
declaration and update ordering only—no other logic changes.
- Around line 16-18: The non-tool prompt path assumes 14 entries and can index
out of range; update PromptGenerator (init(with healthData:)) and the non-tool
flow that calls buildFourteenDaysHealthDataPrompt() to first check
healthData.count >= 14 and either (a) pad/augment the array with neutral/default
HealthData objects to reach 14 entries or (b) switch to a shorter-data prompt
builder (e.g., buildShortHealthDataPrompt()) when fewer entries exist; ensure
buildFourteenDaysHealthDataPrompt() itself defensively validates indices and
returns a safe fallback so the app never crashes on partial/empty healthData.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6bcf6edc-e3d1-4585-b60a-c0d948c7a8dd

📥 Commits

Reviewing files that changed from the base of the PR and between 04550af and 44abfeb.

📒 Files selected for processing (6)
  • HealthGPT/HealthGPT/HealthDataInterpreter.swift
  • HealthGPT/HealthGPT/HealthGPTView.swift
  • HealthGPT/HealthGPT/HealthMetric.swift
  • HealthGPT/HealthGPT/PromptGenerator.swift
  • HealthGPT/Supporting Files/Localizable.xcstrings
  • HealthGPTTests/PromptGeneratorTests.swift
✅ Files skipped from review due to trivial changes (1)
  • HealthGPT/HealthGPT/HealthMetric.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • HealthGPTTests/PromptGeneratorTests.swift
  • HealthGPT/Supporting Files/Localizable.xcstrings
  • HealthGPT/HealthGPT/HealthDataInterpreter.swift

Comment thread HealthGPT/HealthGPT/HealthGPTView.swift Resolved
… ordering

- Use Date.now instead of startOfDay in GetHealthMetricFunction and Date() in
  HealthDataFetcher+Process to match the reviewed convention
- Update PromptGenerator to say current-day data may be partial instead of
  claiming it is absent
- Reorder HealthDataFetcher members so properties precede init (type_contents_order)
- Validate ComparePeriodsFunction rejects negative (future) period offsets
@ahmedrezik

Copy link
Copy Markdown
Contributor Author

@PSchmiedmayer Thank you for taking the time to review Paul, I recognize the mismatch of the repo from the ecosystem and I am spending time aligning it with the Spezi eco and other projects.
I will @ you when I finish the updates

- Move static basePrompt property above instance property and init to
  satisfy SwiftLint type_contents_order rule
- Replace hardcoded 0...13 index loop with for-in over healthData array
  to prevent crash when fewer than 14 entries are available
- Make HealthDataFetcher final + @unchecked Sendable, matching the
  pattern used by SpeziHealthKit's own HealthKit module, and drop
  nonisolated(unsafe) from the LLM function structs.
- Replace silent day-range clamp in GetHealthMetricFunction with an
  invalidDateRange throw so the model sees the error.
- Replace HealthMetric.unitLabel "hours" fallback with an explicit
  switch and convert quantityValue's preconditionFailure(.sleep) into
  an unsupportedMetric throw.
- Hoist the 3 PM sleep-window boundary into sleepWindowHour.
- Drop unnecessary throws on GetAvailableMetricsFunction.execute.
- Add ComparePeriodsFunctionTests and HealthDataFetcherTests covering
  resolveRange / percentChange / bucketSleepSessions; refactor
  PromptGeneratorTests onto a deterministic fixture.
ahmedrezik added a commit to ahmedrezik/HealthGPT that referenced this pull request Apr 20, 2026
- Make HealthDataFetcher final + @unchecked Sendable, matching the
  pattern used by SpeziHealthKit's own HealthKit module, and drop
  nonisolated(unsafe) from the LLM function structs.
- Replace silent day-range clamp in GetHealthMetricFunction with an
  invalidDateRange throw so the model sees the error.
- Replace HealthMetric.unitLabel "hours" fallback with an explicit
  switch and convert quantityValue's preconditionFailure(.sleep) into
  an unsupportedMetric throw.
- Hoist the 3 PM sleep-window boundary into sleepWindowHour.
- Drop unnecessary throws on GetAvailableMetricsFunction.execute.
- Add ComparePeriodsFunctionTests and HealthDataFetcherTests covering
  resolveRange / percentChange / bucketSleepSessions; refactor
  PromptGeneratorTests onto a deterministic fixture.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Re-key fetchAndProcessHealthData by date (startOfDay dictionary
  lookup) so a sparse statisticsQuery result no longer indexes
  out-of-bounds. Drops the now-unused fetchLastTwoWeeks* helpers.
- Clip sleep sessions to each window in bucketSleepSessions so a
  session that straddles 3 PM is split proportionally instead of
  fully credited to both adjacent days. Add a regression test.
- Drop GetAvailableMetricsFunction; the metric enum is already
  exposed through the parameter schema, so a separate listing tool
  is redundant.
ahmedrezik added a commit to ahmedrezik/HealthGPT that referenced this pull request Apr 20, 2026
- Re-key fetchAndProcessHealthData by date (startOfDay dictionary
  lookup) so a sparse statisticsQuery result no longer indexes
  out-of-bounds. Drops the now-unused fetchLastTwoWeeks* helpers.
- Clip sleep sessions to each window in bucketSleepSessions so a
  session that straddles 3 PM is split proportionally instead of
  fully credited to both adjacent days. Add a regression test.
- Drop GetAvailableMetricsFunction; the metric enum is already
  exposed through the parameter schema, so a separate listing tool
  is redundant.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the hardcoded `1...14` loop bound in fetchAndProcessHealthData
with a named constant on HealthDataFetcher, addressing the review note
about the magic number.
- Rename range tuples to `period1Range`/`period2Range` so the
  destructured `.start`/`.end` reads as a date range, not the raw
  parameter.
- Rename the per-period averages to `period1Mean`/`period2Mean` to
  match what the helper actually returns, and rename `fetchAverage`
  to `averageValue` so the call site says what the value is, not how
  it was obtained.
- Extract the duplicated "MMM d - MMM d" formatter into
  `Self.format(range:)`.
@ahmedrezik
ahmedrezik force-pushed the feature/llm-function-calling branch from ba68639 to 5420c33 Compare April 20, 2026 01:13
ahmedrezik and others added 6 commits April 20, 2026 03:15
- Return validation errors from tool execute() as strings so the model
  can self-correct, rather than throwing (which aborts generation)
- Conform HealthDataFetcherError to LocalizedError for readable alerts
- Replace DateFormatter with Date.FormatStyle (.iso8601, .dateTime)
- Distinguish "no data" from "avg 0.0" in period comparison via Double?
- Rename usesToolBasedPrompt/useToolPrompt to usesTools everywhere
- Collapse HealthMetric.displayName; throw on calendar-arithmetic failure
  in fetchSleepData instead of silently returning []

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move static helpers above instance methods so type_method precedes other_method per SwiftLint's type_contents_order rule.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- resolveRange params: start/end -> startDaysAgo/endDaysAgo (units in names)
- switch bindings: current/baseline -> period1Value/period2Value (avoid implying period 1 is always newer)
- period1Mean/period2Mean -> period1Average/period2Average (match averageValue())
- format(range:) -> formatDateRange(_:) (more specific, drops redundant label)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the 3 PM to 3 PM windowing and proportional clipping with a
wake-up-day attribution rule (session belongs to the calendar day of
its end date), matching the Health app. Drops the asleep predicate so
Spezi's 60-min gap rule sees awake samples when grouping;
totalTimeSpentAsleep still only counts asleep phases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ahmedrezik and others added 3 commits April 22, 2026 21:41
Pull validation and formatting out of the LLM function execute() paths
into testable statics: resolveDays, resolvePeriodOffsets, resolveRanges,
buildHealthData with a DailyMetricValues struct. Add unit tests that
exercise each helper directly so the branches don't require HealthKit.

Replace the (offsets?, error?) tuple return on resolvePeriodOffsets with
an OffsetsResolution enum so the type system proves resolved xor missing
and the redundant fallback throw is gone.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ahmedrezik

Copy link
Copy Markdown
Contributor Author

@PSchmiedmayer Happy to get your feedback and comments.

This PR adds LLM function-calling support so HealthGPT can fetch Apple Health data on demand instead of relying only on a preloaded 14-day prompt.

It introduces typed health metrics plus two OpenAI tools: get_health_metric
for daily metric values and compare_periods for comparing averages across
date ranges. It also refactors HealthKit fetching around SpeziHealthKit query
APIs, including reusable daily aggregation and sleep-session bucketing.

The chat flow now uses tool-aware prompts for OpenAI sessions, keeps the existing preloaded-data path for non-tool LLM backends, and shows the pending state while tools are running. The PR also adds focused unit coverage for prompt behavior, metric helpers, date-range validation, period comparison, and sleep bucketing.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants