diff --git a/src/lib/ai/coach/__tests__/snapshot.test.ts b/src/lib/ai/coach/__tests__/snapshot.test.ts index e2355d6e8..4ca883b76 100644 --- a/src/lib/ai/coach/__tests__/snapshot.test.ts +++ b/src/lib/ai/coach/__tests__/snapshot.test.ts @@ -322,6 +322,7 @@ describe("buildCoachSnapshot", () => { ], "weekly": [], }, + "unit": "kg/m²", }, "dayStrain": { "days": 2, @@ -410,6 +411,7 @@ describe("buildCoachSnapshot", () => { }, ], }, + "unit": "°C", }, "steps": { "timeline": { @@ -428,14 +430,23 @@ describe("buildCoachSnapshot", () => { }, ], }, + "unit": "steps", }, "weight": { "aggregate": { + "allTimeAvg": undefined, + "allTimeMax": undefined, + "allTimeMin": undefined, + "avg30": undefined, + "avg7": undefined, + "avg90": undefined, "bmi": 24.7, "coverage": { "count": 2, }, "latest": 80.8, + "slope30": undefined, + "unit": "kg", }, "timeline": { "recent": [ @@ -453,6 +464,7 @@ describe("buildCoachSnapshot", () => { }, ], }, + "unit": "kg", }, }, "snapshotJson": "{ @@ -490,12 +502,14 @@ describe("buildCoachSnapshot", () => { } }, "weight": { + "unit": "kg", "aggregate": { "latest": 80.8, "bmi": 24.7, "coverage": { "count": 2 - } + }, + "unit": "kg" }, "timeline": { "recent": [ @@ -563,6 +577,7 @@ describe("buildCoachSnapshot", () => { } }, "bodyMassIndex": { + "unit": "kg/m²", "timeline": { "recent": [ { @@ -580,6 +595,7 @@ describe("buildCoachSnapshot", () => { } }, "steps": { + "unit": "steps", "timeline": { "recent": [ { @@ -598,6 +614,7 @@ describe("buildCoachSnapshot", () => { } }, "skinTemperature": { + "unit": "°C", "timeline": { "recent": [ { @@ -657,6 +674,55 @@ describe("buildCoachSnapshot", () => { expect(parsed.scope.window).toBe("last30days"); }); + it("states weight, temperature and the grounding bands in an imperial reader's units", async () => { + prismaMock.user.findUnique.mockResolvedValue({ + coachPrefsJson: null, + timezone: "UTC", + unitPreference: "imperial", + glucoseUnit: "mmol/L", + hasDiabetes: false, + }); + featuresMock.mockResolvedValue({ + weight: { + latest: 80, + avg7: 80.5, + bmi: 24.7, + coverage: { count: 2 }, + }, + }); + prismaMock.measurement.findMany.mockResolvedValue([ + daysAgo(4, 80, "WEIGHT"), + daysAgo(2, 81, "WEIGHT"), + daysAgo(3, 36.6, "BODY_TEMPERATURE"), + daysAgo(1, 37, "BODY_TEMPERATURE"), + ]); + + const out = await buildCoachSnapshot("imperial-user", { + window: "last30days", + sources: ["weight", "body_temp"], + }); + const parsed = JSON.parse(out.snapshotJson); + expect(parsed.weight.unit).toBe("lb"); + expect(parsed.weight.aggregate).toMatchObject({ + unit: "lb", + latest: 176.4, + avg7: 177.5, + bmi: 24.7, + }); + expect( + parsed.weight.timeline.recent.map((r: { value: number }) => r.value), + ).toEqual([176.4, 178.6]); + expect(parsed.bodyTemperature.unit).toBe("°F"); + expect( + parsed.bodyTemperature.timeline.recent.map( + (r: { value: number }) => r.value, + ), + ).toEqual([97.9, 98.6]); + // The band beside it is in °F too, placed on the canonical °C mean. + expect(out.referenceGrounding).toContain("96.3–99.3 °F"); + expect(out.referenceGrounding).not.toContain("°C"); + }); + it("includes day-level BP rows with weekday labels for the recent window", async () => { featuresMock.mockResolvedValue({ bloodPressure: { diff --git a/src/lib/ai/coach/__tests__/system-prompt.test.ts b/src/lib/ai/coach/__tests__/system-prompt.test.ts index 41f47706c..d40440bc0 100644 --- a/src/lib/ai/coach/__tests__/system-prompt.test.ts +++ b/src/lib/ai/coach/__tests__/system-prompt.test.ts @@ -120,6 +120,15 @@ describe("getCoachSystemPrompt — EN", () => { expect(prompt).not.toMatch(/70–180 mg\/dL/); }); + it("tells the model every block is already in the reader's unit", () => { + expect(prompt).toMatch( + /A block's "unit" \(and the workouts block's "distanceUnit"\)/, + ); + expect(prompt).toMatch(/never convert them/); + // No fixed list that offers kg (or both glucose units) for one quantity. + expect(prompt).not.toMatch(/"unit" is one of/); + }); + it("documents the WHOOP-native dayStrain block vs the computed proxy", () => { // v1.17.0 — native 0–21 strain preferred over the computed 0–100 proxy. expect(prompt).toMatch(/dayStrain/); @@ -204,12 +213,16 @@ describe("getCoachSystemPrompt — DE", () => { expect(prompt).toMatch(/außerhalb dessen, womit ich helfen kann/); }); - it("lists glucose + duration units in the evidence-block enumeration", () => { - // v1.17.0 — the DE unit list must sanction mg/dL, mmol/L and min so a - // German coach can cite glucose + duration values with a valid token. - expect(prompt).toMatch(/mg\/dL/); - expect(prompt).toMatch(/mmol\/L/); - expect(prompt).toMatch(/min[\s\S]*\(Schlafschuld/); + it("takes the evidence-block unit from the snapshot, not from a fixed list", () => { + // The list used to be fixed (mmHg, kg, …, mg/dL, mmol/L): a reader on + // pounds had no sanctioned token, and both glucose units were offered + // for one quantity. The unit now comes from the block that holds the + // value; minutes stay named for the sleep-debt figures. + expect(prompt).toMatch( + /"unit" ist die Einheit, die der Snapshot für diesen Wert angibt/, + ); + expect(prompt).toMatch(/min für Schlafschuld/); + expect(prompt).not.toMatch(/"unit" ist eines aus/); }); it("carries the v1.21.2 narrative-presence clauses (A4/A5/A6/A7)", () => { @@ -310,3 +323,14 @@ describe("conditional trajectory ground rule (v1.11.0 Epic B, Pillar 3)", () => expect(de).toMatch(/datiertes\s+Ereignis/i); }); }); + +describe("getCoachSystemPrompt — native locales name the reader's unit", () => { + it.each(["fr", "es", "it", "pl"] as const)( + "%s carries the block-unit rule and no fixed unit list", + (locale) => { + const prompt = getCoachSystemPrompt(locale); + expect(prompt).toContain('"distanceUnit"'); + expect(prompt).not.toMatch(/mmHg, kg, bpm/); + }, + ); +}); diff --git a/src/lib/ai/coach/snapshot-blocks/__tests__/workouts-block-units.test.ts b/src/lib/ai/coach/snapshot-blocks/__tests__/workouts-block-units.test.ts new file mode 100644 index 000000000..9ed3415a4 --- /dev/null +++ b/src/lib/ai/coach/snapshot-blocks/__tests__/workouts-block-units.test.ts @@ -0,0 +1,54 @@ +/** + * Workout distances reach the Coach in the reader's distance unit, named once + * on the block, instead of raw metres under a metre-named key. + */ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@/lib/logging/context", () => ({ annotate: vi.fn() })); + +import { buildWorkoutsBlock } from "../workouts-block"; +import type { UnitPreference } from "@/lib/measurements/display-transform"; + +function build(unitPreference: UnitPreference) { + const snapshot: Record = {}; + buildWorkoutsBlock({ + workoutRows: [ + { + sportType: "running", + startedAt: new Date("2026-09-20T07:00:00Z"), + durationSec: 1800, + totalEnergyKcal: 300, + totalDistanceM: 5000, + avgHeartRate: 150, + maxHeartRate: 170, + source: "APPLE_HEALTH", + }, + ], + sourcePriorityJson: null, + userTz: "UTC", + snapshot, + metrics: new Set(), + counts: {}, + registerBlock: () => {}, + unitPreference, + }); + return snapshot.workouts as { + distanceUnit: string; + recent: Array>; + }; +} + +describe("buildWorkoutsBlock — distance unit", () => { + it("states distance in miles for an imperial reader", () => { + const block = build("imperial"); + expect(block.distanceUnit).toBe("mi"); + expect(block.recent[0]).toMatchObject({ distance: 3.11 }); + expect(block.recent[0]).not.toHaveProperty("distanceM"); + }); + + it("states distance in kilometres for a metric reader", () => { + const block = build("metric"); + expect(block.distanceUnit).toBe("km"); + expect(block.recent[0]).toMatchObject({ distance: 5 }); + }); +}); diff --git a/src/lib/ai/coach/snapshot-blocks/core-metrics-block.ts b/src/lib/ai/coach/snapshot-blocks/core-metrics-block.ts index 2a3e23493..eda25227a 100644 --- a/src/lib/ai/coach/snapshot-blocks/core-metrics-block.ts +++ b/src/lib/ai/coach/snapshot-blocks/core-metrics-block.ts @@ -1,10 +1,16 @@ import type { AggregatedFeatures } from "@/lib/insights/features"; +import { weightFeatureInReaderUnits } from "@/lib/insights/features-units"; +import { + getReadingTransform, + type UnitPreferences, +} from "@/lib/measurements/display-transform"; import type { ReferenceMetric } from "@/lib/reference-ranges"; import { bpBandFromRows, bucketWeekly, buildDailyBpRows, buildDailyValueRows, + timelineInUnit, type CoarseTimelineTail, } from "../snapshot-series"; import type { @@ -38,6 +44,8 @@ interface CoreMetricsBlockContext { counts: NonNullable; registerBlock: (key: string, source: CoachScopeSource) => void; groundingValues: Map; + /** The reader's units; the weight block is stated in them. */ + units: UnitPreferences; } export function buildCoreMetricsBlocks( @@ -93,16 +101,24 @@ export function buildCoreMetricsBlocks( if (ctx.sources.has("weight") && ctx.features.weight) { const rows = byType("WEIGHT"); + const aggregate = weightFeatureInReaderUnits( + ctx.features.weight, + ctx.units, + ); ctx.snapshot.weight = { - aggregate: ctx.features.weight, - timeline: { - recent: buildDailyValueRows(rows, ctx.recentCutoff, ctx.userTz), - weekly: bucketWeekly( - rows.filter((row) => row.measuredAt < ctx.recentCutoff), - ctx.userTz, - ), - ...(ctx.coarseTails.weight ? { coarse: ctx.coarseTails.weight } : {}), - }, + unit: aggregate.unit, + aggregate, + timeline: timelineInUnit( + { + recent: buildDailyValueRows(rows, ctx.recentCutoff, ctx.userTz), + weekly: bucketWeekly( + rows.filter((row) => row.measuredAt < ctx.recentCutoff), + ctx.userTz, + ), + ...(ctx.coarseTails.weight ? { coarse: ctx.coarseTails.weight } : {}), + }, + getReadingTransform("WEIGHT", ctx.units), + ), }; ctx.metrics.add("weight"); ctx.windows.add("last7days"); diff --git a/src/lib/ai/coach/snapshot-blocks/value-series-blocks.ts b/src/lib/ai/coach/snapshot-blocks/value-series-blocks.ts index df6c25b88..a31da2d04 100644 --- a/src/lib/ai/coach/snapshot-blocks/value-series-blocks.ts +++ b/src/lib/ai/coach/snapshot-blocks/value-series-blocks.ts @@ -1,7 +1,15 @@ import { annotate } from "@/lib/logging/context"; +import { + getReadingTransform, + type UnitPreferences, +} from "@/lib/measurements/display-transform"; import type { ReferenceMetric } from "@/lib/reference-ranges"; import { sourceCluster } from "../clusters"; -import { bucketWeekly, buildDailyValueRows } from "../snapshot-series"; +import { + bucketWeekly, + buildDailyValueRows, + timelineInUnit, +} from "../snapshot-series"; import type { CoachProvenance, CoachProvenanceMetric, @@ -241,8 +249,13 @@ interface ValueSeriesBlocksContext { counts: NonNullable; registerBlock: (key: string, source: CoachScopeSource) => void; groundingValues: Map; + /** The reader's units; every block is stated and labelled in them. */ + units: UnitPreferences; } +/** Unit tokens that are not a unit a reader could be quoted. */ +const UNLABELLED_UNITS: ReadonlySet = new Set(["", "unknown", "score"]); + export function buildValueSeriesBlocks( ctx: Readonly, ): void { @@ -270,14 +283,26 @@ export function buildValueSeriesBlocks( } const recentRows = buildDailyValueRows(rows, ctx.recentCutoff, ctx.userTz); + // Aggregated on the canonical value (the grounding below reads that), + // stated in the reader's unit. The two HRV estimators share one unit. + const transform = getReadingTransform( + typeof block.type === "string" ? block.type : block.type[0], + ctx.units, + ); ctx.snapshot[block.snapshotKey] = { - timeline: { - recent: recentRows, - weekly: bucketWeekly( - rows.filter((row) => row.measuredAt < ctx.recentCutoff), - ctx.userTz, - ), - }, + ...(UNLABELLED_UNITS.has(transform.displayUnit) + ? {} + : { unit: transform.displayUnit }), + timeline: timelineInUnit( + { + recent: recentRows, + weekly: bucketWeekly( + rows.filter((row) => row.measuredAt < ctx.recentCutoff), + ctx.userTz, + ), + }, + transform, + ), }; ctx.metrics.add(block.metric); ctx.counts[block.metric] = rows.length; diff --git a/src/lib/ai/coach/snapshot-blocks/workouts-block.ts b/src/lib/ai/coach/snapshot-blocks/workouts-block.ts index 2a396428d..c44a8be52 100644 --- a/src/lib/ai/coach/snapshot-blocks/workouts-block.ts +++ b/src/lib/ai/coach/snapshot-blocks/workouts-block.ts @@ -20,6 +20,11 @@ * the UI. Run the SAME picker here, keyed off the user's own * `sourcePriorityJson`, so both surfaces agree. */ +import { + applyDisplayTransform, + getQuantityTransform, + type UnitPreference, +} from "@/lib/measurements/display-transform"; import { pickCanonicalWorkoutRows } from "@/lib/measurements/pick-canonical-workout-rows"; import type { MeasurementSource } from "@/generated/prisma/client"; import { annotate } from "@/lib/logging/context"; @@ -57,6 +62,8 @@ interface WorkoutsBlockContext { metrics: Set; counts: NonNullable; registerBlock: (key: string, source: CoachScopeSource) => void; + /** The reader's metric/imperial choice; distances are stated in it. */ + unitPreference: UnitPreference; } export function buildWorkoutsBlock(ctx: Readonly): void { @@ -68,7 +75,9 @@ export function buildWorkoutsBlock(ctx: Readonly): void { metrics, counts, registerBlock, + unitPreference, } = ctx; + const distanceTransform = getQuantityTransform("distance", unitPreference); // The workout sessions are read in parallel by the builder, raw // (every source, every duplicate). Collapse cross-source duplicates // with the same picker the UI uses before narrating anything. @@ -92,7 +101,10 @@ export function buildWorkoutsBlock(ctx: Readonly): void { sport: w.sportType, durationMin: Math.round(w.durationSec / 60), energyKcal: w.totalEnergyKcal ?? null, - distanceM: w.totalDistanceM ?? null, + distance: + w.totalDistanceM == null + ? null + : applyDisplayTransform(w.totalDistanceM, distanceTransform), avgHr: w.avgHeartRate ?? null, maxHr: w.maxHeartRate ?? null, })); @@ -121,6 +133,7 @@ export function buildWorkoutsBlock(ctx: Readonly): void { })) .sort((a, b) => b.count - a.count); snapshot.workouts = { + distanceUnit: distanceTransform.displayUnit, recent: recentList, perSport: sportRollup, totalInWindow: workoutRows.length, diff --git a/src/lib/ai/coach/snapshot-series.ts b/src/lib/ai/coach/snapshot-series.ts index 4df20de6e..f06fc3328 100644 --- a/src/lib/ai/coach/snapshot-series.ts +++ b/src/lib/ai/coach/snapshot-series.ts @@ -8,6 +8,10 @@ * is one-way (snapshot → series), never back. */ import { userDayKey } from "@/lib/tz/format"; +import { + applyDisplayTransform, + type DisplayTransform, +} from "@/lib/measurements/display-transform"; import { buildTieredSeries } from "@/lib/rollups/tiered-context"; import type { MeasurementType } from "@/generated/prisma/client"; import { buildBaselineBand } from "@/lib/insights/derived/baseline"; @@ -257,6 +261,62 @@ export async function buildCoarseTimelineTail( }; } +/** + * A built timeline in the reader's unit. The series are aggregated on the + * canonical value and converted once at the end, so a mean in pounds is the + * converted mean of the kilograms, not a mean of rounded pound readings. + * Identity for every transform that does not rescale. + */ +export function timelineInUnit( + timeline: { + recent: DailyValueRow[]; + weekly: WeeklyBucket[]; + coarse?: CoarseTimelineTail; + }, + transform: DisplayTransform, +): { + recent: DailyValueRow[]; + weekly: WeeklyBucket[]; + coarse?: CoarseTimelineTail; +} { + const v = (value: number) => applyDisplayTransform(value, transform); + return { + recent: timeline.recent.map((row) => ({ ...row, value: v(row.value) })), + weekly: timeline.weekly.map((bucket) => ({ + ...bucket, + mean: v(bucket.mean), + })), + ...(timeline.coarse + ? { + coarse: { + monthly: timeline.coarse.monthly.map( + ([start, mean, min, max]) => + [start, v(mean), v(min), v(max)] as [ + string, + number, + number, + number, + ], + ), + yearly: timeline.coarse.yearly.map( + ([start, mean, min, max]) => + [start, v(mean), v(min), v(max)] as [ + string, + number, + number, + number, + ], + ), + anomalies: timeline.coarse.anomalies.map((a) => ({ + ...a, + value: v(a.value), + })), + }, + } + : {}), + }; +} + /** * Pair systolic + diastolic into one row per day. Days with only one * side measured are dropped — the Coach is asked about "BP" as a pair, diff --git a/src/lib/ai/coach/snapshot.ts b/src/lib/ai/coach/snapshot.ts index bcc502e9f..4f7ed8b79 100644 --- a/src/lib/ai/coach/snapshot.ts +++ b/src/lib/ai/coach/snapshot.ts @@ -882,6 +882,7 @@ async function buildCoachSnapshotImpl( counts, registerBlock, groundingValues, + units, }); // Medication compliance lives outside the structured features — the block // is assembled in `snapshot-blocks/compliance-block.ts` from the ledger @@ -911,6 +912,7 @@ async function buildCoachSnapshotImpl( counts, registerBlock, groundingValues, + units, }); // ── v1.7.0 sleep block (with optional per-stage enrichment) ─────── // Assembled in `snapshot-blocks/sleep-block.ts` from the dedicated @@ -976,6 +978,7 @@ async function buildCoachSnapshotImpl( metrics, counts, registerBlock, + unitPreference: units.system, }); } diff --git a/src/lib/ai/coach/system-prompt.ts b/src/lib/ai/coach/system-prompt.ts index d1e9427fe..349a100a3 100644 --- a/src/lib/ai/coach/system-prompt.ts +++ b/src/lib/ai/coach/system-prompt.ts @@ -337,13 +337,16 @@ ISO-week means. - The SNAPSHOT is ADDITIVE. Beyond blood pressure, weight, pulse, mood, and medication, it MAY carry body-composition, activity, workouts, sleep (with per-stage minutes), glucose (split by - context: fasting / postprandial / random / bedtime, carrying its - own display "unit" of mg/dL or mmol/L), mobility & + context: fasting / postprandial / random / bedtime), mobility & gait, and environmental-exposure blocks — but only when the user enabled the cluster AND has data. A block that is absent means the user has no such data or did not share it; never invent or assume a cluster the SNAPSHOT does not contain. The "workouts" block carries the recent sessions plus a per-sport rollup, not every session. +- A block's "unit" (and the workouts block's "distanceUnit") is the + unit the user reads their numbers in, and every figure in that block + is already in it. Quote the figures in that unit; never convert them + and never name a different unit for the same quantity. - The SNAPSHOT MAY carry a "derived" block: compact wellness summaries the app already computed (readiness, recovery, sleep score, stress, @@ -460,8 +463,8 @@ Rules: or a day-pin ("Tue 6 May"). - "value" is a pre-formatted display string ("138/85", "84.2", "4.1"). Format mood as N/5 when numeric. -- "unit" is one of mmHg, kg, bpm, /5, %, mg/dL, mmol/L, min (sleep - debt / social jetlag). +- "unit" is the unit the snapshot gives for that value (e.g. mmHg, + bpm, /5, %, min for sleep debt / social jetlag). - "window" is one of last7days, last30days, last90days, allTime; omit it for day-level pins. - Omit the entire block (no ---KEYVALUES--- line, no ---END--- line) @@ -917,6 +920,10 @@ ISO-Wochenmittel zusammen. das der SNAPSHOT nicht enthält. Der "workouts"-Block führt die jüngsten Einheiten plus eine Zusammenfassung je Sportart, nicht jede Einheit. +- Die "unit" eines Blocks (und "distanceUnit" im "workouts"-Block) + ist die Einheit, in der der Nutzer seine Werte liest; jede Zahl im + Block steht schon in ihr. Nenne die Zahlen in dieser Einheit, rechne + sie nie um und nenne nie eine andere Einheit für dieselbe Größe. - Der "memory"-Block des SNAPSHOT kann eine "facts"-Liste tragen — dauerhafte Dinge, die du über diesen Nutzer gelernt hast (stabile Vorlieben, vom Nutzer selbst genannte gesundheitliche Umstände, @@ -987,8 +994,8 @@ Regeln: oder ein Tages-Pin ("Di 6. Mai"). - "value" ist ein vorformatierter Anzeigestring ("138/85", "84,2", "4,1"). Stimmung als N/5 formatieren, wenn numerisch. -- "unit" ist eines aus mmHg, kg, bpm, /5, %, mg/dL, mmol/L, min - (Schlafschuld / sozialer Jetlag). +- "unit" ist die Einheit, die der Snapshot für diesen Wert angibt + (z. B. mmHg, bpm, /5, %, min für Schlafschuld / sozialen Jetlag). - "window" ist eines aus last7days, last30days, last90days, allTime; bei Tages-Pins weglassen. - Lass den ganzen Block weg (KEINE ---KEYVALUES---Zeile, KEINE diff --git a/src/lib/ai/prompts/native-prompts.ts b/src/lib/ai/prompts/native-prompts.ts index 8f8e76d77..aa0a79503 100644 --- a/src/lib/ai/prompts/native-prompts.ts +++ b/src/lib/ai/prompts/native-prompts.ts @@ -83,21 +83,25 @@ const DAY_LEVEL_BODY: Record = { - Pour une question portant sur un jour ou un jour-de-semaine précis ("pourquoi la tension était-elle plus élevée lundi dernier ?"), répondez depuis "timeline.recent" — citez la lecture réelle de ce jour avec la date et le jour-de-semaine. - Quand aucune ligne n'existe pour le jour mentionné, dites-le clairement ("je n'ai pas de lecture pour lundi 5 mai") et proposez le jour disponible le plus proche. - Les questions plus anciennes (>14 jours) basculent sur "timeline.weekly" ou, à défaut, sur l'agrégat. +- Le "unit" d'un bloc (et le "distanceUnit" du bloc "workouts") est l'unité dans laquelle l'utilisateur lit ses valeurs, et chaque chiffre du bloc y est déjà. Citez les chiffres dans cette unité ; ne les convertissez jamais et ne nommez jamais une autre unité pour la même grandeur. - L'objet "scope" nomme la fenêtre choisie et les sources. Quand l'utilisateur interroge une métrique absente de scope.sources, dites qu'elle n'est pas dans le périmètre de cette conversation.`, es: `Cada métrica en el SNAPSHOT lleva un objeto "timeline" junto al bloque "aggregate". "timeline.recent" es una lista ordenada de filas-día de los últimos 14 días; cada fila lleva una "date" (YYYY-MM-DD), una etiqueta "weekday" (Lun/Mar/Mié/...) y los valores de la métrica para ese día. "timeline.weekly" agrupa los días más antiguos en medias ISO-semanales. - Ante preguntas sobre un día concreto o un día de la semana ("¿por qué la tensión estuvo más alta el lunes pasado?"), responda desde "timeline.recent" — cite la lectura real de ese día con fecha y día de la semana. - Cuando no exista fila para el día mencionado, dígalo claramente ("no tengo lectura del lunes 5 de mayo") y ofrezca el día más cercano disponible. - Las preguntas más antiguas (>14 días) acuden a "timeline.weekly" o, si no, al agregado. +- La "unit" de un bloque (y la "distanceUnit" del bloque "workouts") es la unidad en la que el usuario lee sus valores, y cada cifra del bloque ya está en ella. Cite las cifras en esa unidad; nunca las convierta ni nombre otra unidad para la misma magnitud. - El objeto "scope" nombra la ventana seleccionada y las fuentes. Si el usuario pregunta por una métrica que no está en scope.sources, indique que está fuera del alcance de esta conversación.`, it: `Ogni metrica nello SNAPSHOT porta un oggetto "timeline" accanto al blocco "aggregate". "timeline.recent" è una lista ordinata di righe-giorno per gli ultimi 14 giorni; ogni riga porta una "date" (YYYY-MM-DD), un'etichetta "weekday" (Lun/Mar/Mer/...) e i valori della metrica per quel giorno. "timeline.weekly" raggruppa i giorni più vecchi in medie ISO-settimanali. - Per domande su un giorno o giorno-della-settimana specifico ("perché la pressione era più alta lunedì scorso?"), risponda da "timeline.recent" — citi la lettura reale di quel giorno con data e giorno-della-settimana. - Quando non esiste una riga per il giorno nominato, lo dica chiaramente ("non ho una lettura per lunedì 5 maggio") e proponga il giorno più vicino disponibile. - Le domande più vecchie (>14 giorni) ripiegano su "timeline.weekly" o, in mancanza, sull'aggregato. +- La "unit" di un blocco (e la "distanceUnit" del blocco "workouts") è l'unità in cui l'utente legge i propri valori, e ogni cifra del blocco è già in essa. Citi le cifre in quell'unità; non le converta mai e non nomini mai un'altra unità per la stessa grandezza. - L'oggetto "scope" nomina la finestra scelta dall'utente e le fonti. Se l'utente chiede di una metrica non presente in scope.sources, dica che non rientra nello scopo di questa conversazione.`, pl: `Każda metryka w SNAPSHOT zawiera obiekt "timeline" obok bloku "aggregate". "timeline.recent" to uporządkowana lista wierszy-dnia za ostatnie 14 dni; każdy wiersz ma "date" (YYYY-MM-DD), etykietę "weekday" (Pn/Wt/Śr/...) i wartości metryki dla tego dnia. "timeline.weekly" grupuje starsze dni w średnie ISO-tygodniowe. - Przy pytaniach o konkretny dzień lub dzień tygodnia ("dlaczego ciśnienie było wyższe w zeszły poniedziałek?"), proszę odpowiadać z "timeline.recent" — cytować rzeczywisty odczyt z tego dnia z datą i dniem tygodnia. - Gdy nie ma wiersza dla wymienionego dnia, proszę powiedzieć to wprost ("nie mam odczytu z poniedziałku 5 maja") i zaproponować najbliższy dostępny dzień. - Starsze pytania (>14 dni) sięgają do "timeline.weekly" lub, w razie braku, do agregatu. +- "unit" bloku (oraz "distanceUnit" bloku "workouts") to jednostka, w której użytkownik odczytuje swoje wartości, i każda liczba w bloku jest już w niej podana. Proszę cytować liczby w tej jednostce; nigdy ich nie przeliczać i nigdy nie podawać innej jednostki dla tej samej wielkości. - Obiekt "scope" wymienia wybrane okno i źródła. Jeśli użytkownik pyta o metrykę nieobecną w scope.sources, proszę powiedzieć, że nie należy ona do zakresu tej rozmowy.`, }; @@ -115,7 +119,7 @@ Règles : - Chaque ligne : "