diff --git a/src/client.ts b/src/client.ts index 53a4c4b..1fd3cc8 100644 --- a/src/client.ts +++ b/src/client.ts @@ -4,6 +4,7 @@ import { AlertsResource } from './resources/alerts.js' import { ComplianceResource } from './resources/compliance.js' import { ContextResource } from './resources/context.js' import { EventsResource } from './resources/events.js' +import { HealthResource } from './resources/health.js' import { LatticeResource } from './resources/lattice.js' import { MemoryResource } from './resources/memory.js' @@ -54,6 +55,7 @@ export class ThinkFleetMemory { readonly events: EventsResource readonly alerts: AlertsResource readonly compliance: ComplianceResource + readonly health: HealthResource constructor(options: ThinkFleetMemoryOptions) { if (!options.apiKey) { @@ -80,5 +82,6 @@ export class ThinkFleetMemory { this.events = new EventsResource(http) this.alerts = new AlertsResource(http) this.compliance = new ComplianceResource(http) + this.health = new HealthResource(http) } } diff --git a/src/index.ts b/src/index.ts index 74f9d8a..b94c450 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,6 +70,24 @@ export type { ConsentStatus, } from './resources/consent.js' export { LatticeResource } from './resources/lattice.js' +export { HealthResource } from './resources/health.js' + +// Types — health +export type { + Biomarker, + Sex, + ActivityLevel, + ConditionStatus, + DemographicsInput, + ConditionInput, + HealthAgeComponent, + BiologicalAge, + PredictedHealthCondition, + BiomarkerReading, + HealthProfile, + CohortConditionRisk, + CohortHealthRisk, +} from './types/health.js' // Types — common export type { BaseModel } from './types/common.js' diff --git a/src/resources/health.ts b/src/resources/health.ts new file mode 100644 index 0000000..403ac62 --- /dev/null +++ b/src/resources/health.ts @@ -0,0 +1,147 @@ +import type { HttpClient } from '../core/http-client.js' +import type { RequestOptions } from '../core/types.js' +import { MemoryItemType, MemoryScope, type MemoryItem } from '../types/memory.js' +import type { + Biomarker, + CohortHealthRisk, + ConditionInput, + DemographicsInput, + HealthProfile, + Subject, +} from '../types/health.js' + +/** + * Health — biological ("health") age + condition prediction for the + * memory engine's health vertical. + * + * Health data is just memory data: you record biomarkers, demographics, + * and ICD-10 diagnoses as memory items, and the engine derives a + * biological age (PhenoAge core + composite adjustments) and condition + * predictions (biomarkers trending toward / above clinical thresholds), + * plus cohort base rates ("of patients like this one, X% have Y"). + * + * You decide where the data originates — EHR feed, document extraction, + * wearable, manual entry. The SDK only gives you the typed way in and out. + * + * Requires the `@thinkfleet/pack-healthcare` pack enabled on the project; + * the read methods return FAILED_PRECONDITION otherwise. + * + * Recorded items are stored as non-activity `fact` memories, so they feed + * the health engine without being mined as behavioral patterns. + * + * @example + * ```ts + * const subject = { kind: 'patient', externalId: 'p-123' } + * await tf.health.recordDemographics(subject, { ageYears: 54, sex: 'female', weightKg: 82, heightCm: 170, activity: 'low' }) + * await tf.health.recordBiomarker(subject, 'hba1c', 6.2, { unit: '%' }) + * await tf.health.recordCondition(subject, { icd10: 'I10', status: 'active' }) + * + * const profile = await tf.health.getProfile(subject) + * console.log(profile.biologicalAge?.biologicalAgeYears, profile.predictedConditions) + * + * const cohort = await tf.health.getCohortRisk(subject) + * for (const r of cohort.risks) console.log(`${r.condition}: ${(r.cohortPrevalence * 100).toFixed(0)}% of similar patients`) + * ``` + */ +export class HealthResource { + constructor(private readonly http: HttpClient) {} + + // ── Input — record health signals (stored as memory items) ── + + /** + * Record a biomarker reading. Send whatever unit the lab reported via + * `opts.unit`; the engine normalizes it. + */ + async recordBiomarker( + subject: Subject, + biomarker: Biomarker | (string & {}), + value: number, + opts?: { unit?: string; observedAt?: string }, + options?: RequestOptions, + ): Promise { + const health: Record = { biomarker, value } + if (opts?.unit) health.unit = opts.unit + if (opts?.observedAt) health.observedAt = opts.observedAt + return this.http.post( + '/admin/memory', + { + content: `${biomarker} = ${value}${opts?.unit ? ` ${opts.unit}` : ''}`, + type: MemoryItemType.FACT, + scope: MemoryScope.PROJECT, + category: 'health', + source: 'sdk:health', + metadata: { subject, health }, + }, + options, + ) + } + + /** Record/refresh a subject's demographics. Latest values win. */ + async recordDemographics( + subject: Subject, + demographics: DemographicsInput, + options?: RequestOptions, + ): Promise { + return this.http.post( + '/admin/memory', + { + content: 'Demographics update', + type: MemoryItemType.FACT, + scope: MemoryScope.PROJECT, + category: 'health', + source: 'sdk:health', + metadata: { subject, demographic: demographics }, + }, + options, + ) + } + + /** Record an ICD-10 diagnosis. */ + async recordCondition( + subject: Subject, + condition: ConditionInput, + options?: RequestOptions, + ): Promise { + return this.http.post( + '/admin/memory', + { + content: `Diagnosis ${condition.icd10}`, + type: MemoryItemType.FACT, + scope: MemoryScope.PROJECT, + category: 'health', + source: 'sdk:health', + metadata: { subject, condition }, + }, + options, + ) + } + + // ── Read — derived profile + cohort outcomes ── + + /** + * Biological-age estimate + condition predictions + latest biomarkers + * for a subject, derived from their recorded health data. + */ + async getProfile(subject: Subject, options?: RequestOptions): Promise { + return this.http.post('/lattice/health/profile', { subject }, options) + } + + /** + * Cohort outcomes — condition prevalence among the patients most similar + * to this subject by baseline features. An epidemiological base rate to + * complement the individual projections from `getProfile`. + * + * @param opts.k cohort size (nearest patients); default 25. + */ + async getCohortRisk( + subject: Subject, + opts?: { k?: number }, + options?: RequestOptions, + ): Promise { + return this.http.post( + '/lattice/health/cohort-risk', + { subject, ...(opts?.k != null ? { k: opts.k } : {}) }, + options, + ) + } +} diff --git a/src/types/health.ts b/src/types/health.ts new file mode 100644 index 0000000..65d342f --- /dev/null +++ b/src/types/health.ts @@ -0,0 +1,130 @@ +// Health prediction types — biological ("health") age + condition +// prediction + cohort outcomes, served by the memory engine's health +// vertical (gated behind the @thinkfleet/pack-healthcare pack). +// +// Health data IS memory data: you record biomarkers / demographics / +// diagnoses as memory items (see HealthResource.record*), and the engine +// derives the profile from them. You decide where the data comes from — +// EHR, document extraction, manual entry — the SDK just gives you the +// typed way in. + +import type { Subject } from './lattice.js' + +export type { Subject } + +/** Canonical biomarker keys the engine understands. Free-form string on + * the wire (so new markers don't break the contract); these are the + * recognized values. */ +export type Biomarker = + // PhenoAge panel + | 'albumin' + | 'creatinine' + | 'glucose_fasting' + | 'crp' + | 'lymphocyte_pct' + | 'mcv' + | 'rdw' + | 'alkaline_phosphatase' + | 'wbc' + // Cardiometabolic + | 'hba1c' + | 'ldl' + | 'hdl' + | 'total_cholesterol' + | 'triglycerides' + | 'systolic_bp' + | 'diastolic_bp' + +export type Sex = 'male' | 'female' | 'unknown' +export type ActivityLevel = 'sedentary' | 'low' | 'moderate' | 'high' +export type ConditionStatus = 'active' | 'resolved' | 'historical' + +export interface DemographicsInput { + ageYears?: number + sex?: Sex + weightKg?: number + heightCm?: number + activity?: ActivityLevel +} + +export interface ConditionInput { + /** ICD-10 code, e.g. "E11.9". */ + icd10: string + status?: ConditionStatus + /** ISO timestamp. */ + onsetAt?: string +} + +// ── Read shapes ── + +export interface HealthAgeComponent { + label: string + yearsDelta: number +} + +export interface BiologicalAge { + biologicalAgeYears: number + chronologicalAgeYears: number + deltaYears: number + /** "phenoage_hybrid" | "composite" */ + method: string + confidence: number + components: HealthAgeComponent[] + /** 10-year mortality score (0..1) from PhenoAge, when available. */ + mortalityScore?: number | null +} + +export interface PredictedHealthCondition { + /** Canonical key, e.g. "type2_diabetes". */ + condition: string + label: string + /** "above_threshold_now" | "threshold_projection" */ + basis: string + biomarker: string + currentValue: number + threshold: number + /** ISO timestamp; set only for threshold_projection. */ + projectedOnsetAt?: string | null + confidence: number + rationale: string + sourceMemoryIds: string[] +} + +export interface BiomarkerReading { + biomarker: string + value: number + unit: string + observedAt: string +} + +export interface HealthProfile { + subject: Subject + biologicalAge?: BiologicalAge | null + predictedConditions: PredictedHealthCondition[] + /** ICD-10 codes already diagnosed (active) on record. */ + diagnosedConditions: string[] + latestBiomarkers: BiomarkerReading[] + /** Always populated — these are screening indicators, not a diagnosis. */ + disclaimer: string + generatedAt: string +} + +export interface CohortConditionRisk { + condition: string + /** Fraction of the cohort carrying this condition (0..1). */ + cohortPrevalence: number + cohortSize: number + countWith: number + meanSimilarity: number + confidence: number + rationale: string +} + +export interface CohortHealthRisk { + subject: Subject + cohortSize: number + populationSize: number + risks: CohortConditionRisk[] + disclaimer: string + generatedAt: string +}