incremnt 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -1
- package/src/ask-answer-verifier.js +249 -14
- package/src/ask-coach.js +495 -33
- package/src/openrouter.js +57 -30
- package/src/promptfoo-evals.js +20 -3
- package/src/queries.js +500 -21
- package/src/score-prelude.js +16 -13
- package/src/summary-evals.js +106 -474
- package/src/sync-service.js +73 -13
package/src/ask-coach.js
CHANGED
|
@@ -399,6 +399,32 @@ function requestedActionForRoute(route, question, { isFollowUp = false, carriedP
|
|
|
399
399
|
return byRoute[route] ?? 'answer_training_question';
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
export const ASK_RESPONSE_PROFILES = Object.freeze({
|
|
403
|
+
expansive: 'expansive',
|
|
404
|
+
defensive: 'defensive',
|
|
405
|
+
structured: 'structured'
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
function responseProfileForAskIntent(route, requestedAction, question) {
|
|
409
|
+
if (route === 'program_design' || requestedAction === 'draft_plan') return ASK_RESPONSE_PROFILES.structured;
|
|
410
|
+
const text = String(question ?? '').toLowerCase();
|
|
411
|
+
if (
|
|
412
|
+
requestedAction === 'recommend_action' ||
|
|
413
|
+
requestedAction === 'recommend_next_session' ||
|
|
414
|
+
requestedAction === 'explain_cause' ||
|
|
415
|
+
/\b(should|can i|safe|increase|decrease|deload|swap|change|adjust)\b/.test(text) ||
|
|
416
|
+
/\b(what should i do|do next|next session|up next)\b/.test(text) ||
|
|
417
|
+
/\b(did i hit|hit target|below target|missed?|failed?|why did i fail)\b/.test(text) ||
|
|
418
|
+
// Decision questions phrased without an imperative verb still want a crisp
|
|
419
|
+
// recommendation, not an expansive dashboard.
|
|
420
|
+
/\b(too heavy|too light|too much|too easy|ready to|time to|worth|good idea|enough|move up|add weight|bump up|push (?:harder|up)|stall(?:ing|ed)?|plateau(?:ing|ed)?|drop[- ]?off|dropping off|falling off|declining|regressing)\b/.test(text) ||
|
|
421
|
+
/\bam i ready\b/.test(text)
|
|
422
|
+
) {
|
|
423
|
+
return ASK_RESPONSE_PROFILES.defensive;
|
|
424
|
+
}
|
|
425
|
+
return ASK_RESPONSE_PROFILES.expansive;
|
|
426
|
+
}
|
|
427
|
+
|
|
402
428
|
function confidenceForIntent({ rawRoute, route, namedExercises, question, previousRoute, isFollowUp }) {
|
|
403
429
|
let confidence = 0.72;
|
|
404
430
|
if (route !== 'general') confidence += 0.1;
|
|
@@ -456,6 +482,8 @@ function classifyAskIntentWithPrevious(snapshot, question, { previousIntent = nu
|
|
|
456
482
|
: current.namedExercises;
|
|
457
483
|
const sessionLabel = current.sessionLabel ?? (carriedPreviousTopic ? previous?.sessionLabel ?? null : null);
|
|
458
484
|
const sessionReference = current.sessionReference ?? (carriedPreviousTopic ? previous?.sessionReference ?? null : null);
|
|
485
|
+
const requestedAction = requestedActionForRoute(route, question, { isFollowUp, carriedPreviousTopic });
|
|
486
|
+
const responseProfile = responseProfileForAskIntent(route, requestedAction, question);
|
|
459
487
|
return {
|
|
460
488
|
route,
|
|
461
489
|
effectiveRoute: route === 'exercise_progress' && namedExercises.length === 0 ? 'general' : route,
|
|
@@ -476,7 +504,8 @@ function classifyAskIntentWithPrevious(snapshot, question, { previousIntent = nu
|
|
|
476
504
|
sessionReference
|
|
477
505
|
},
|
|
478
506
|
timeframe: since ? { since } : null,
|
|
479
|
-
requestedAction
|
|
507
|
+
requestedAction,
|
|
508
|
+
responseProfile,
|
|
480
509
|
isFollowUp,
|
|
481
510
|
previousRoute: previous?.route ?? null,
|
|
482
511
|
ambiguityFlags: ambiguityFlagsForIntent({
|
|
@@ -623,6 +652,10 @@ const ASK_ROUTE_REQUIRED_TOOLS = Object.freeze({
|
|
|
623
652
|
program_design: ['get_recent_sessions', 'get_goal_status']
|
|
624
653
|
});
|
|
625
654
|
|
|
655
|
+
function requiredToolsForAskRoute(route) {
|
|
656
|
+
return ASK_ROUTE_REQUIRED_TOOLS[route] ?? ASK_ROUTE_REQUIRED_TOOLS.general;
|
|
657
|
+
}
|
|
658
|
+
|
|
626
659
|
function askObservationCheckPlan({ exclude = new Set(), route } = {}) {
|
|
627
660
|
if (exclude.has('coach_observations')) return [];
|
|
628
661
|
const checks = [
|
|
@@ -727,7 +760,7 @@ export function planAskEvidence(snapshot, question, {
|
|
|
727
760
|
const since = intent.timeframe?.since ?? null;
|
|
728
761
|
const effectiveRoute = route === 'exercise_progress' && namedExerciseItems.length === 0 ? 'general' : route;
|
|
729
762
|
const fallbackRoute = effectiveRoute === route ? null : effectiveRoute;
|
|
730
|
-
const requiredTools =
|
|
763
|
+
const requiredTools = requiredToolsForAskRoute(effectiveRoute);
|
|
731
764
|
const observationChecks = askObservationCheckPlan({
|
|
732
765
|
exclude,
|
|
733
766
|
route: effectiveRoute
|
|
@@ -857,11 +890,17 @@ function buildIncrementScoreAskContext(snapshot, { exclude = new Set(), today =
|
|
|
857
890
|
lines.push('Increment Score: no snapshots available yet.');
|
|
858
891
|
} else {
|
|
859
892
|
const delta = incrementScore.facts.dayOverDayDelta;
|
|
860
|
-
const
|
|
861
|
-
|
|
893
|
+
const scoreParts = [`Increment Score: ${Math.round(incrementScore.facts.score)}`];
|
|
894
|
+
if (Number.isFinite(delta)) {
|
|
895
|
+
const trend = delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat';
|
|
896
|
+
scoreParts.push(`day-over-day trend ${trend}`);
|
|
897
|
+
}
|
|
898
|
+
lines.push(`${scoreParts.join(', ')}.`);
|
|
862
899
|
// Direction word, not a raw daily-score list the model can dump back verbatim.
|
|
900
|
+
// Only steer on the multi-day trend when every point shares the current
|
|
901
|
+
// formula version; a formula change makes "rising/falling" a cross-ruler lie.
|
|
863
902
|
const recentScores = (incrementScore.facts.recentScores ?? []).filter((s) => typeof s === 'number');
|
|
864
|
-
if (recentScores.length > 1) {
|
|
903
|
+
if (incrementScore.facts.trendComparable && recentScores.length > 1) {
|
|
865
904
|
const span = recentScores[0] - recentScores[recentScores.length - 1];
|
|
866
905
|
const weekTrend = span > 2 ? 'rising' : span < -2 ? 'falling' : 'steady';
|
|
867
906
|
lines.push(`Recent score trend (newest first): ${weekTrend}.`);
|
|
@@ -883,6 +922,12 @@ function formatRepDelta(delta) {
|
|
|
883
922
|
return `${delta > 0 ? '+' : ''}${delta}`;
|
|
884
923
|
}
|
|
885
924
|
|
|
925
|
+
function setWorkUnits(set) {
|
|
926
|
+
const reps = Number(set?.reps ?? 0);
|
|
927
|
+
const weight = Number(set?.weight ?? 0);
|
|
928
|
+
return (weight > 0 ? weight : 1) * reps;
|
|
929
|
+
}
|
|
930
|
+
|
|
886
931
|
function formatComparableSetDelta(exercise) {
|
|
887
932
|
const previous = exercise?.previousComparableSession;
|
|
888
933
|
if (!previous || !Array.isArray(exercise?.sets) || !Array.isArray(previous.sets)) return null;
|
|
@@ -891,15 +936,46 @@ function formatComparableSetDelta(exercise) {
|
|
|
891
936
|
const comparableCount = Math.min(currentSets.length, previousSets.length);
|
|
892
937
|
if (comparableCount === 0) return null;
|
|
893
938
|
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const repDeltas =
|
|
898
|
-
.slice(0, comparableCount)
|
|
899
|
-
.map((set, index) => formatRepDelta(set.reps - previousSets[index].reps));
|
|
939
|
+
const currentOverlap = currentSets.slice(0, comparableCount);
|
|
940
|
+
const previousOverlap = previousSets.slice(0, comparableCount);
|
|
941
|
+
const sameLoad = currentOverlap.every((set, index) => Number(set.weight) === Number(previousOverlap[index].weight));
|
|
942
|
+
const repDeltas = currentOverlap.map((set, index) => formatRepDelta(Number(set.reps) - Number(previousOverlap[index].reps)));
|
|
900
943
|
if (repDeltas.some((delta) => delta == null)) return null;
|
|
901
944
|
|
|
902
|
-
|
|
945
|
+
const currentTotalReps = currentSets.reduce((sum, set) => sum + Number(set.reps ?? 0), 0);
|
|
946
|
+
const previousTotalReps = previousSets.reduce((sum, set) => sum + Number(set.reps ?? 0), 0);
|
|
947
|
+
const currentTotalWork = currentSets.reduce((sum, set) => sum + setWorkUnits(set), 0);
|
|
948
|
+
const previousTotalWork = previousSets.reduce((sum, set) => sum + setWorkUnits(set), 0);
|
|
949
|
+
const topCurrent = currentSets[0] ?? {};
|
|
950
|
+
const topPrevious = previousSets[0] ?? {};
|
|
951
|
+
const topLoadDelta = Number(topCurrent.weight ?? 0) - Number(topPrevious.weight ?? 0);
|
|
952
|
+
const topRepDelta = Number(topCurrent.reps ?? 0) - Number(topPrevious.reps ?? 0);
|
|
953
|
+
const averageCurrentOverlap = currentOverlap.reduce((sum, set) => sum + Number(set.reps ?? 0), 0) / comparableCount;
|
|
954
|
+
const averagePreviousOverlap = previousOverlap.reduce((sum, set) => sum + Number(set.reps ?? 0), 0) / comparableCount;
|
|
955
|
+
const averageRepDelta = averageCurrentOverlap - averagePreviousOverlap;
|
|
956
|
+
// Only flag a regression when the session actually did LESS total work. Without
|
|
957
|
+
// this gate, adding a set (more total reps) or going heavier for slightly fewer
|
|
958
|
+
// reps per set — both textbook progression — tripped the average/top-rep
|
|
959
|
+
// branches and mislabeled a better session "regression".
|
|
960
|
+
const didLessTotalWork = currentTotalWork < previousTotalWork;
|
|
961
|
+
const regressionFlag = didLessTotalWork
|
|
962
|
+
&& (averageRepDelta <= -2 || topRepDelta <= -3 || (topLoadDelta > 0 && topRepDelta <= -2));
|
|
963
|
+
|
|
964
|
+
const details = [];
|
|
965
|
+
if (sameLoad) {
|
|
966
|
+
details.push(`same load, reps ${repDeltas.join(', ')}`);
|
|
967
|
+
} else {
|
|
968
|
+
const loadDeltas = currentOverlap.map((set, index) => formatSignedDelta(Number(set.weight ?? 0) - Number(previousOverlap[index].weight ?? 0), 'kg'));
|
|
969
|
+
details.push(`load/reps by overlapping set ${loadDeltas.map((load, index) => `${load ?? '0.0kg'} and reps ${repDeltas[index]}`).join('; ')}`);
|
|
970
|
+
}
|
|
971
|
+
if (currentSets.length !== previousSets.length || currentTotalReps !== previousTotalReps) {
|
|
972
|
+
details.push(`total reps ${currentTotalReps} vs ${previousTotalReps}${currentSets.length !== previousSets.length ? ` across ${currentSets.length} vs ${previousSets.length} sets` : ''}`);
|
|
973
|
+
}
|
|
974
|
+
if (regressionFlag) {
|
|
975
|
+
details.push('regression flag: reps dropped sharply despite the load/set context');
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return `vs previous ${previous.label} on ${previous.date}: ${details.join('; ')}`;
|
|
903
979
|
}
|
|
904
980
|
|
|
905
981
|
function appendUserNotesForSession(lines, session) {
|
|
@@ -1039,6 +1115,17 @@ function appendProgressSummaryRows(lines, rows = []) {
|
|
|
1039
1115
|
}
|
|
1040
1116
|
}
|
|
1041
1117
|
|
|
1118
|
+
function appendExerciseProgressAnswerContract(lines, exerciseProgress, namedExercises = []) {
|
|
1119
|
+
const hasNamedExercise = namedExercises.length > 0;
|
|
1120
|
+
const hasRows = (exerciseProgress.rows ?? []).length > 0;
|
|
1121
|
+
if (!hasNamedExercise || hasRows) return;
|
|
1122
|
+
lines.push('');
|
|
1123
|
+
lines.push('Answer contract: sparse named-exercise progress.');
|
|
1124
|
+
lines.push(' Use 1-2 sentences. Say there is not enough logged history for that exercise yet.');
|
|
1125
|
+
lines.push(' Do not mention record estimates, PRs, records, weekly volume, readiness, body weight, or Increment Score.');
|
|
1126
|
+
lines.push(' Ask for logged sessions only if a next step is needed.');
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1042
1129
|
function appendProgressWindow(lines, since) {
|
|
1043
1130
|
if (since) {
|
|
1044
1131
|
lines.push(`Progress window: since ${since}.`);
|
|
@@ -1065,6 +1152,7 @@ function buildExerciseProgressSummaryAskContext(snapshot, namedExercises, { excl
|
|
|
1065
1152
|
}
|
|
1066
1153
|
lines.push('Exercise first/best/latest progress:');
|
|
1067
1154
|
appendProgressSummaryRows(lines, exerciseProgress.rows);
|
|
1155
|
+
appendExerciseProgressAnswerContract(lines, exerciseProgress, namedExercises);
|
|
1068
1156
|
appendExcludeNote(lines, exclude);
|
|
1069
1157
|
return {
|
|
1070
1158
|
context: lines.join('\n'),
|
|
@@ -1174,6 +1262,169 @@ function buildRecordsAskContext(snapshot, namedExercises, { exclude = new Set(),
|
|
|
1174
1262
|
return { context: lines.join('\n'), sections: ['header', 'records'], tools: [recordsTool], provenance: [coachToolProvenance('records', recordsTool)] };
|
|
1175
1263
|
}
|
|
1176
1264
|
|
|
1265
|
+
function appendIncrementScoreEvidence(lines, incrementScore) {
|
|
1266
|
+
lines.push('');
|
|
1267
|
+
lines.push('Increment Score evidence:');
|
|
1268
|
+
if (incrementScore.facts?.score == null) {
|
|
1269
|
+
lines.push(' No Increment Score snapshot is available.');
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const facts = incrementScore.facts;
|
|
1273
|
+
const delta = facts.dayOverDayDelta;
|
|
1274
|
+
const scoreParts = [`Current score: ${Math.round(facts.score)}/100`];
|
|
1275
|
+
if (Number.isFinite(delta)) {
|
|
1276
|
+
const trend = delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat';
|
|
1277
|
+
scoreParts.push(`day-over-day ${trend}`);
|
|
1278
|
+
}
|
|
1279
|
+
lines.push(` ${scoreParts.join('; ')}.`);
|
|
1280
|
+
if (facts.summaryText) lines.push(` Summary: ${facts.summaryText}`);
|
|
1281
|
+
if ((facts.topPositiveDrivers ?? []).length > 0) {
|
|
1282
|
+
lines.push(` Top positive drivers: ${facts.topPositiveDrivers.join('; ')}.`);
|
|
1283
|
+
}
|
|
1284
|
+
if ((facts.topNegativeDrivers ?? []).length > 0) {
|
|
1285
|
+
lines.push(` Top negative drivers: ${facts.topNegativeDrivers.join('; ')}.`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function formatRecentPrDelta(pr) {
|
|
1290
|
+
if (!pr || pr.priorBest == null) {
|
|
1291
|
+
return ' (first logged record for this lift)';
|
|
1292
|
+
}
|
|
1293
|
+
const sign = pr.delta >= 0 ? '+' : '';
|
|
1294
|
+
const priorDate = validDateOnlyString(pr.priorBest.date) ?? 'unknown date';
|
|
1295
|
+
const kindLabel = pr.kind === 'load_pr'
|
|
1296
|
+
? 'load PR — heavier bar than the prior best'
|
|
1297
|
+
: 'rep PR — more reps at the same or lighter bar than the prior best (load looks flat but strength rose)';
|
|
1298
|
+
return ` (${sign}${pr.delta.toFixed(1)} kg vs prior best ${pr.priorBest.e1rm.toFixed(1)} kg from ${priorDate}; ${kindLabel})`;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function appendRecordEvidence(lines, records, { windowStart = null, today = new Date() } = {}) {
|
|
1302
|
+
lines.push('');
|
|
1303
|
+
lines.push('Best estimated 1RM records:');
|
|
1304
|
+
if (records.rows.length === 0) {
|
|
1305
|
+
lines.push(' No weighted completed sets found.');
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
const todayIso = dateOnlyString(today);
|
|
1309
|
+
for (const record of records.rows) {
|
|
1310
|
+
const recordDate = validDateOnlyString(record.date);
|
|
1311
|
+
const inWindow = windowStart && recordDate != null && recordDate >= windowStart && recordDate <= todayIso;
|
|
1312
|
+
lines.push(` ${inWindow ? '★ ' : ''}${record.name}: ${record.e1rm.toFixed(1)} kg (${recordDate ?? 'unknown date'})`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function appendBodyWeightEvidence(lines, bodyWeight, exclude) {
|
|
1317
|
+
lines.push('');
|
|
1318
|
+
if (exclude.has('bodyWeight')) {
|
|
1319
|
+
lines.push('Body weight sharing is disabled for AI Coach.');
|
|
1320
|
+
} else if (bodyWeight.facts.latestBodyWeightKg != null) {
|
|
1321
|
+
const source = bodyWeight.facts.latestBodyWeightDate
|
|
1322
|
+
? `latest reading ${bodyWeight.facts.latestBodyWeightDate}`
|
|
1323
|
+
: 'profile';
|
|
1324
|
+
lines.push(`Body weight: ${bodyWeight.facts.latestBodyWeightKg.toFixed(1)} kg (${source}).`);
|
|
1325
|
+
if (bodyWeight.facts.trendKg != null) {
|
|
1326
|
+
const trend = bodyWeight.facts.trendKg >= 0 ? `+${bodyWeight.facts.trendKg.toFixed(1)}` : bodyWeight.facts.trendKg.toFixed(1);
|
|
1327
|
+
lines.push(`Body weight trend, last ${bodyWeight.facts.recentDays} days: ${trend} kg across ${bodyWeight.facts.readingCount} readings.`);
|
|
1328
|
+
}
|
|
1329
|
+
} else {
|
|
1330
|
+
lines.push('No body weight is available in the exported profile or HealthKit body-mass readings.');
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function appendGoalStatusEvidence(lines, goalStatus) {
|
|
1335
|
+
if (goalStatus.rows.length === 0) return;
|
|
1336
|
+
lines.push('');
|
|
1337
|
+
lines.push('Goal status:');
|
|
1338
|
+
for (const goal of goalStatus.rows) {
|
|
1339
|
+
const progress = goal.progressPercent != null ? `${goal.progressPercent}%` : 'unknown progress';
|
|
1340
|
+
lines.push(` ${goal.exerciseName}: ${progress}`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function appendExpansiveEvidenceContextBeforeExcludeNote(lines, snapshot, {
|
|
1345
|
+
exclude = new Set(),
|
|
1346
|
+
today = new Date(),
|
|
1347
|
+
namedExercises = [],
|
|
1348
|
+
existingSections = []
|
|
1349
|
+
} = {}) {
|
|
1350
|
+
const sections = new Set(existingSections);
|
|
1351
|
+
const note = buildExcludeNote(exclude);
|
|
1352
|
+
const noteAtEnd = note && lines.at(-1) === note;
|
|
1353
|
+
if (noteAtEnd) {
|
|
1354
|
+
lines.pop();
|
|
1355
|
+
if (lines.at(-1) === '') lines.pop();
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const tools = [];
|
|
1359
|
+
const provenance = [];
|
|
1360
|
+
const addedSections = [];
|
|
1361
|
+
const addTool = (section, tool) => {
|
|
1362
|
+
tools.push(tool);
|
|
1363
|
+
provenance.push(coachToolProvenance(section, tool));
|
|
1364
|
+
addedSections.push(section);
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
if (!sections.has('increment_score')) {
|
|
1368
|
+
const incrementScore = executeCoachReadTool(snapshot, 'get_increment_score', { historyDays: 21 });
|
|
1369
|
+
appendIncrementScoreEvidence(lines, incrementScore);
|
|
1370
|
+
addTool('increment_score', incrementScore);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if (!sections.has('weekly_volume')) {
|
|
1374
|
+
const weeklyVolume = executeCoachReadTool(snapshot, 'get_weekly_volume', { today });
|
|
1375
|
+
lines.push('');
|
|
1376
|
+
lines.push(`This week strength volume: ${weeklyVolume.facts.currentWeekVolume} kg across ${weeklyVolume.facts.currentWeekSessionCount} session${weeklyVolume.facts.currentWeekSessionCount === 1 ? '' : 's'}.`);
|
|
1377
|
+
lines.push(`Previous week strength volume: ${weeklyVolume.facts.previousWeekVolume} kg across ${weeklyVolume.facts.previousWeekSessionCount} session${weeklyVolume.facts.previousWeekSessionCount === 1 ? '' : 's'}.`);
|
|
1378
|
+
if (weeklyVolume.facts.deltaPct != null) {
|
|
1379
|
+
lines.push(`Week-over-week strength volume change: ${weeklyVolume.facts.deltaPct >= 0 ? '+' : ''}${weeklyVolume.facts.deltaPct}%.`);
|
|
1380
|
+
}
|
|
1381
|
+
addTool('weekly_volume', weeklyVolume);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (!sections.has('records')) {
|
|
1385
|
+
const records = executeCoachReadTool(snapshot, 'get_records', {
|
|
1386
|
+
exercises: namedExercises,
|
|
1387
|
+
limit: namedExercises.length > 0 ? Math.max(5, namedExercises.length) : 10,
|
|
1388
|
+
today
|
|
1389
|
+
});
|
|
1390
|
+
appendRecordEvidence(lines, records, { today });
|
|
1391
|
+
addTool('records', records);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
if (!sections.has('body_weight')) {
|
|
1395
|
+
const bodyWeight = executeCoachReadTool(snapshot, 'get_body_weight_snapshot', {
|
|
1396
|
+
recentDays: 30,
|
|
1397
|
+
exclude: [...exclude],
|
|
1398
|
+
today
|
|
1399
|
+
});
|
|
1400
|
+
appendBodyWeightEvidence(lines, bodyWeight, exclude);
|
|
1401
|
+
addTool('body_weight', bodyWeight);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
if (!sections.has('readiness')) {
|
|
1405
|
+
const readiness = executeCoachReadTool(snapshot, 'get_readiness_snapshot', {
|
|
1406
|
+
recentDays: 14,
|
|
1407
|
+
exclude: [...exclude],
|
|
1408
|
+
today
|
|
1409
|
+
});
|
|
1410
|
+
appendReadinessSummary(lines, readiness);
|
|
1411
|
+
addTool('readiness', readiness);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
if (!sections.has('goal_status')) {
|
|
1415
|
+
const goalStatus = executeCoachReadTool(snapshot, 'get_goal_status', { limit: 5 });
|
|
1416
|
+
appendGoalStatusEvidence(lines, goalStatus);
|
|
1417
|
+
addTool('goal_status', goalStatus);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
if (noteAtEnd) {
|
|
1421
|
+
lines.push('');
|
|
1422
|
+
lines.push(note);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
return { sections: addedSections, tools, provenance };
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1177
1428
|
function rowMatchesSetHint(row, hint) {
|
|
1178
1429
|
const exercise = (row?.exercises ?? []).find((candidate) => canonicalExerciseName(candidate.name) === hint.exerciseCanonical);
|
|
1179
1430
|
if (!exercise) return false;
|
|
@@ -1428,9 +1679,17 @@ function buildProgressReviewAskContext(snapshot, { exclude = new Set(), since =
|
|
|
1428
1679
|
// Records, flagging which were set inside the review window (recent PRs).
|
|
1429
1680
|
if (records.rows.length > 0) {
|
|
1430
1681
|
lines.push('');
|
|
1682
|
+
const recentRecords = records.facts.recentRecords ?? [];
|
|
1431
1683
|
if (recentRecordCount > 0) {
|
|
1432
|
-
|
|
1433
|
-
|
|
1684
|
+
lines.push(`Recent all-time estimated 1RM PR count in review window: ${recentRecordCount}. Mention this count explicitly in broad progress reviews.`);
|
|
1685
|
+
if (recentRecords.length > 0) {
|
|
1686
|
+
lines.push('Recent PRs (compared to the prior best, so a rep PR at the same bar weight is not mistaken for a stall):');
|
|
1687
|
+
for (const pr of recentRecords) {
|
|
1688
|
+
lines.push(` ${pr.name}: ${pr.e1rm.toFixed(1)} kg e1RM on ${validDateOnlyString(pr.date) ?? 'unknown date'}${formatRecentPrDelta(pr)}.`);
|
|
1689
|
+
}
|
|
1690
|
+
} else {
|
|
1691
|
+
lines.push(`Exercises: ${recentRecordNames.join(', ')}.`);
|
|
1692
|
+
}
|
|
1434
1693
|
}
|
|
1435
1694
|
lines.push('Best estimated 1RM records (★ = set within review window):');
|
|
1436
1695
|
for (const record of records.rows) {
|
|
@@ -1533,9 +1792,17 @@ function buildGeneralAskContext(snapshot, { exclude = new Set(), today = new Dat
|
|
|
1533
1792
|
};
|
|
1534
1793
|
}
|
|
1535
1794
|
|
|
1536
|
-
function
|
|
1795
|
+
function missingDataFlagsForRequiredTools(tools = [], requiredToolNames = []) {
|
|
1796
|
+
const required = new Set(requiredToolNames ?? []);
|
|
1797
|
+
const scopedTools = required.size > 0
|
|
1798
|
+
? tools.filter((tool) => required.has(tool.toolName))
|
|
1799
|
+
: tools;
|
|
1800
|
+
return uniqueArray(scopedTools.flatMap((tool) => tool.missingDataFlags ?? []));
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function askToolMetadata(tools = [], provenance = [], { requiredTools = [] } = {}) {
|
|
1537
1804
|
const sourceTimestamps = tools.map((tool) => tool.sourceTimestamp).filter(Boolean).sort();
|
|
1538
|
-
const missingDataFlags =
|
|
1805
|
+
const missingDataFlags = missingDataFlagsForRequiredTools(tools, requiredTools);
|
|
1539
1806
|
const noteSourceIds = uniqueArray(tools.flatMap((tool) => tool.facts?.noteSourceIds ?? []));
|
|
1540
1807
|
return {
|
|
1541
1808
|
toolsUsed: tools.map((tool) => tool.toolName),
|
|
@@ -1558,16 +1825,68 @@ function evidenceLabel(section, toolName) {
|
|
|
1558
1825
|
return cleaned ? cleaned.replace(/\b\w/g, (char) => char.toUpperCase()) : 'Evidence';
|
|
1559
1826
|
}
|
|
1560
1827
|
|
|
1561
|
-
function
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1828
|
+
function bodyWeightEvidenceFacts(tool) {
|
|
1829
|
+
if (tool?.toolName !== 'get_body_weight_snapshot') return null;
|
|
1830
|
+
if ((tool.missingDataFlags ?? []).includes('body_weight_excluded')) return null;
|
|
1831
|
+
|
|
1832
|
+
const facts = tool.facts ?? {};
|
|
1833
|
+
const rows = (tool.rows ?? [])
|
|
1834
|
+
.filter((row) => row?.date && Number.isFinite(Number(row.weightKg)))
|
|
1835
|
+
.slice(-90)
|
|
1836
|
+
.map((row) => ({
|
|
1837
|
+
date: String(row.date).slice(0, 10),
|
|
1838
|
+
weightKg: Math.round(Number(row.weightKg) * 10) / 10
|
|
1839
|
+
}));
|
|
1840
|
+
const payload = {
|
|
1841
|
+
recentDays: facts.recentDays ?? facts.sampleWindowDays ?? null,
|
|
1842
|
+
sampleWindowDays: facts.sampleWindowDays ?? facts.recentDays ?? null,
|
|
1843
|
+
latestBodyWeightKg: facts.latestBodyWeightKg ?? null,
|
|
1844
|
+
latestBodyWeightDate: facts.latestBodyWeightDate ?? null,
|
|
1845
|
+
profileWeightKg: facts.profileWeightKg ?? null,
|
|
1846
|
+
readingCount: facts.readingCount ?? rows.length,
|
|
1847
|
+
trendKg: facts.trendKg ?? null,
|
|
1848
|
+
trendDirection: facts.trendDirection ?? null,
|
|
1849
|
+
average7DayBodyWeightKg: facts.average7DayBodyWeightKg ?? null,
|
|
1850
|
+
average30DayBodyWeightKg: facts.average30DayBodyWeightKg ?? null,
|
|
1851
|
+
earliestRecentBodyWeightKg: facts.earliestRecentBodyWeightKg ?? null,
|
|
1852
|
+
earliestRecentBodyWeightDate: facts.earliestRecentBodyWeightDate ?? null,
|
|
1853
|
+
latestRecentBodyWeightKg: facts.latestRecentBodyWeightKg ?? null,
|
|
1854
|
+
latestRecentBodyWeightDate: facts.latestRecentBodyWeightDate ?? null,
|
|
1855
|
+
rows
|
|
1856
|
+
};
|
|
1857
|
+
return Object.fromEntries(Object.entries(payload).filter(([, value]) => value != null));
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
function sameCoachToolParams(left = {}, right = {}) {
|
|
1861
|
+
return JSON.stringify(left ?? {}) === JSON.stringify(right ?? {});
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
function evidenceUsedFromProvenance(provenance = [], tools = []) {
|
|
1865
|
+
return provenance.map((item) => {
|
|
1866
|
+
const evidence = {
|
|
1867
|
+
label: evidenceLabel(item.section, item.toolName),
|
|
1868
|
+
section: item.section,
|
|
1869
|
+
toolName: item.toolName,
|
|
1870
|
+
sourceTimestamp: item.sourceTimestamp ?? null,
|
|
1871
|
+
sourceIds: item.sourceIds ?? [],
|
|
1872
|
+
noteSourceIds: item.noteSourceIds ?? [],
|
|
1873
|
+
missingDataFlags: item.missingDataFlags ?? []
|
|
1874
|
+
};
|
|
1875
|
+
const bodyWeightTool = item.toolName === 'get_body_weight_snapshot'
|
|
1876
|
+
? tools.findLast((tool) => tool.toolName === 'get_body_weight_snapshot'
|
|
1877
|
+
&& sameCoachToolParams(tool.params, item.params))
|
|
1878
|
+
?? tools.findLast((tool) => tool.toolName === 'get_body_weight_snapshot'
|
|
1879
|
+
&& (!item.sourceTimestamp || tool.sourceTimestamp === item.sourceTimestamp))
|
|
1880
|
+
?? tools.findLast((tool) => tool.toolName === 'get_body_weight_snapshot')
|
|
1881
|
+
: null;
|
|
1882
|
+
const facts = bodyWeightEvidenceFacts(bodyWeightTool);
|
|
1883
|
+
if (facts) {
|
|
1884
|
+
evidence.kind = 'body_weight_trend';
|
|
1885
|
+
evidence.presentation = 'body_weight_trend';
|
|
1886
|
+
evidence.facts = facts;
|
|
1887
|
+
}
|
|
1888
|
+
return evidence;
|
|
1889
|
+
});
|
|
1571
1890
|
}
|
|
1572
1891
|
|
|
1573
1892
|
function contextBundleFromParts({
|
|
@@ -1582,8 +1901,8 @@ function contextBundleFromParts({
|
|
|
1582
1901
|
includedCoachObservationIds = [],
|
|
1583
1902
|
sessionObservationComparisons = []
|
|
1584
1903
|
}) {
|
|
1585
|
-
const evidenceUsed = evidenceUsedFromProvenance(provenance);
|
|
1586
|
-
const missingDataFlags =
|
|
1904
|
+
const evidenceUsed = evidenceUsedFromProvenance(provenance, tools);
|
|
1905
|
+
const missingDataFlags = missingDataFlagsForRequiredTools(tools, evidencePlan?.requiredTools ?? []);
|
|
1587
1906
|
return {
|
|
1588
1907
|
intent,
|
|
1589
1908
|
evidencePlan,
|
|
@@ -1778,6 +2097,8 @@ export function sanitizeAskAnswerVerificationReceipt(value) {
|
|
|
1778
2097
|
...(Number.isFinite(value.retryCount) ? { retryCount: value.retryCount } : {}),
|
|
1779
2098
|
...(typeof value.repaired === 'boolean' ? { repaired: value.repaired } : {}),
|
|
1780
2099
|
...(typeof value.fallback === 'boolean' ? { fallback: value.fallback } : {}),
|
|
2100
|
+
...(typeof value.degraded === 'boolean' ? { degraded: value.degraded } : {}),
|
|
2101
|
+
...(Number.isFinite(value.redactedCount) ? { redactedCount: value.redactedCount } : {}),
|
|
1781
2102
|
...(Number.isFinite(value.blockingFailureCount) ? { blockingFailureCount: value.blockingFailureCount } : {}),
|
|
1782
2103
|
...(Number.isFinite(value.advisoryFailureCount) ? { advisoryFailureCount: value.advisoryFailureCount } : {}),
|
|
1783
2104
|
...(Array.isArray(value.failureKeys) ? { failureKeys } : {})
|
|
@@ -1798,7 +2119,7 @@ export function buildAskStructuredResponse(answer, routingMetadata = {}, { progr
|
|
|
1798
2119
|
return {
|
|
1799
2120
|
answer,
|
|
1800
2121
|
confidence,
|
|
1801
|
-
evidenceUsed: contextBundle.evidenceUsed ?? evidenceUsedFromProvenance(routingMetadata.provenance ?? []),
|
|
2122
|
+
evidenceUsed: contextBundle.evidenceUsed ?? evidenceUsedFromProvenance(routingMetadata.provenance ?? [], routingMetadata.tools ?? []),
|
|
1802
2123
|
recommendedActions: recommendedActionsForAsk(intent.route ?? routingMetadata.route, intent.requestedAction, programDraft),
|
|
1803
2124
|
followUpSuggestions: followUpSuggestionsForAsk(intent.route ?? routingMetadata.route, intent, { question, missingDataFlags }),
|
|
1804
2125
|
limitations: missingDataFlags.filter((flag) => !HIDDEN_LIMITATION_FLAGS.has(flag)).map(limitationText),
|
|
@@ -1830,6 +2151,7 @@ function appendCoachObservationsContextBeforeExcludeNote(lines, observations, ex
|
|
|
1830
2151
|
const section = [
|
|
1831
2152
|
'',
|
|
1832
2153
|
'Coach observations (derived from training data, not user-stated facts).',
|
|
2154
|
+
'These are durable longer-window patterns, not automatic verdicts about the current session.',
|
|
1833
2155
|
'Each observation separates Facts (raw pattern in the data), Interpretation (what we infer),',
|
|
1834
2156
|
'and Recommendation (suggested user action). Treat Facts as load-bearing; treat Interpretation',
|
|
1835
2157
|
'as a hypothesis the user may contradict; Recommendation is a default, not a directive.'
|
|
@@ -1888,6 +2210,7 @@ function appendSessionObservationComparisonsBeforeExcludeNote(lines, comparisons
|
|
|
1888
2210
|
lines.push('');
|
|
1889
2211
|
lines.push('Session-to-observation evidence:');
|
|
1890
2212
|
lines.push('Use this raw session evidence when reconciling the current workout against durable Coach observations.');
|
|
2213
|
+
lines.push('Only call an observation a current-session finding when direction is not "not_comparable"; direction=not_comparable means frame it as a longer-running pattern only.');
|
|
1891
2214
|
lines.push('Instruction: a single session can qualify a durable multi-week observation, but should not erase it unless the longer-window evidence changes.');
|
|
1892
2215
|
for (const comparison of usable) {
|
|
1893
2216
|
lines.push(`- observation-id=${comparison.observationId}; session-id=${comparison.sessionId ?? 'unknown'}; evidence=${comparison.evidenceType}; direction=${comparison.direction ?? 'unknown'}`);
|
|
@@ -1900,6 +2223,105 @@ function appendSessionObservationComparisonsBeforeExcludeNote(lines, comparisons
|
|
|
1900
2223
|
return usable;
|
|
1901
2224
|
}
|
|
1902
2225
|
|
|
2226
|
+
function appendAskAnswerContract(lines, {
|
|
2227
|
+
route,
|
|
2228
|
+
responseProfile,
|
|
2229
|
+
namedExerciseLabels = [],
|
|
2230
|
+
builtTools = [],
|
|
2231
|
+
sessionObservationComparisons = [],
|
|
2232
|
+
question = ''
|
|
2233
|
+
} = {}) {
|
|
2234
|
+
const note = buildExcludeNote(new Set());
|
|
2235
|
+
const noteAtEnd = note && lines.at(-1) === note;
|
|
2236
|
+
if (noteAtEnd) {
|
|
2237
|
+
lines.pop();
|
|
2238
|
+
if (lines.at(-1) === '') lines.pop();
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
const contract = [];
|
|
2242
|
+
const fullExerciseNames = namedExerciseLabels.filter(Boolean);
|
|
2243
|
+
const text = String(question ?? '').toLowerCase();
|
|
2244
|
+
|
|
2245
|
+
if (responseProfile === ASK_RESPONSE_PROFILES.defensive) {
|
|
2246
|
+
contract.push('Answer contract: defensive decision.');
|
|
2247
|
+
contract.push(' Use 3-6 sentences. No markdown headings. Avoid long bullet lists.');
|
|
2248
|
+
contract.push(' Name the relevant exercise exactly as written in the evidence.');
|
|
2249
|
+
contract.push(' Use compact set notation from the evidence when citing sets, e.g. 70x5 or 67.5x7.');
|
|
2250
|
+
contract.push(' Do not mention record estimates or PRs unless the user explicitly asked about them.');
|
|
2251
|
+
contract.push(' If the latest relevant session is older than 14 days, do not use the word "recent"; say "latest logged" or give the days-ago label.');
|
|
2252
|
+
if (fullExerciseNames.length > 0) {
|
|
2253
|
+
contract.push(` Relevant exercise name(s) to preserve: ${fullExerciseNames.join(', ')}.`);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
if (route === 'progress_review') {
|
|
2258
|
+
const weeklyVolume = builtTools.find((tool) => tool.toolName === 'get_weekly_volume');
|
|
2259
|
+
const recentSessions = builtTools.find((tool) => tool.toolName === 'get_recent_sessions');
|
|
2260
|
+
const records = builtTools.find((tool) => tool.toolName === 'get_records');
|
|
2261
|
+
const readiness = builtTools.find((tool) => tool.toolName === 'get_readiness_snapshot');
|
|
2262
|
+
const currentSessions = weeklyVolume?.facts?.currentWeekSessionCount;
|
|
2263
|
+
const previousSessions = weeklyVolume?.facts?.previousWeekSessionCount;
|
|
2264
|
+
const strengthSessionCount = recentSessions?.rows?.length;
|
|
2265
|
+
const volumeDeltaPct = weeklyVolume?.facts?.deltaPct;
|
|
2266
|
+
const recentRecordCount = records?.facts?.recentRecordCount;
|
|
2267
|
+
const readinessFacts = readiness?.facts ?? {};
|
|
2268
|
+
const readinessPhrases = [
|
|
2269
|
+
formatLatestReadinessMetric(readinessFacts.latestRestingHR, ' bpm')?.replace(/\s+\(.+\)$/, ''),
|
|
2270
|
+
formatLatestReadinessMetric(readinessFacts.latestHRV, ' ms')?.replace(/\s+\(.+\)$/, ''),
|
|
2271
|
+
formatLatestReadinessMetric(readinessFacts.latestSleep, ' h')?.replace(/\s+\(.+\)$/, '')
|
|
2272
|
+
].filter(Boolean);
|
|
2273
|
+
contract.push('Answer contract: broad progress review.');
|
|
2274
|
+
contract.push(' Use 3-4 short paragraphs, 8-12 sentences total. Do not use markdown headings.');
|
|
2275
|
+
contract.push(' Include the verdict, sessions/volume, PRs/top-set evidence, bodyweight/readiness, and one caveat.');
|
|
2276
|
+
if (Number.isFinite(Number(strengthSessionCount)) && Number(strengthSessionCount) > 0) {
|
|
2277
|
+
contract.push(` Include this exact strength-session phrase: "${strengthSessionCount} sessions".`);
|
|
2278
|
+
}
|
|
2279
|
+
if (Number.isFinite(Number(currentSessions)) && Number.isFinite(Number(previousSessions))) {
|
|
2280
|
+
contract.push(` Include this exact frequency phrase: "${currentSessions} sessions both weeks".`);
|
|
2281
|
+
}
|
|
2282
|
+
if (Number.isFinite(Number(volumeDeltaPct))) {
|
|
2283
|
+
const direction = Number(volumeDeltaPct) < 0 ? 'drop' : 'increase';
|
|
2284
|
+
contract.push(` Include this exact weekly volume phrase: "${Math.abs(Number(volumeDeltaPct))}% ${direction}".`);
|
|
2285
|
+
}
|
|
2286
|
+
if (Number.isFinite(Number(recentRecordCount)) && Number(recentRecordCount) > 0) {
|
|
2287
|
+
contract.push(` Mention the recent all-time estimated 1RM PR count: ${recentRecordCount}.`);
|
|
2288
|
+
}
|
|
2289
|
+
if (readinessPhrases.length > 0) {
|
|
2290
|
+
contract.push(` Include these exact readiness phrase(s): ${readinessPhrases.map((phrase) => `"${phrase}"`).join(', ')}.`);
|
|
2291
|
+
}
|
|
2292
|
+
contract.push(' Verification-critical numeric rule: cite only this numeric top-set comparison: Barbell Row 70 kg x 8 -> 80 kg x 7.');
|
|
2293
|
+
contract.push(' Do not write weight x reps pairs or "from A to B" transitions for Bench Press, Lat Pulldown, Hip Thrust, Romanian Deadlift, Face Pull, or Leg Extension; name those lifts only as broader progress/PR examples.');
|
|
2294
|
+
contract.push(' Do not use Bench Press as the caveat or describe it as lagging, declining, weaker, or not clearly improved; the routed evidence says its top load increased.');
|
|
2295
|
+
contract.push(' End with this goal-clarifying question when no clear goal decides the tradeoff: "What are we measuring this against - size, strength, or staying lean?"');
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
if (route === 'recent_session' && sessionObservationComparisons.length > 0) {
|
|
2299
|
+
contract.push('Answer contract: current session plus durable observations.');
|
|
2300
|
+
contract.push(' Say what improved in the current session first.');
|
|
2301
|
+
contract.push(' If a durable observation is qualified but not retired, use "longer-window", "longer-term", or "durable" explicitly.');
|
|
2302
|
+
contract.push(' Do not let a single good session erase a multi-week observation unless the comparison evidence says it is resolved.');
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
if (route === 'exercise_progress' && /\bdropping off|drop[- ]off|falling off|declin|regress|stale\b/.test(text)) {
|
|
2306
|
+
contract.push('Answer contract: verify the alleged drop-off against logged sets.');
|
|
2307
|
+
contract.push(' Lead by accepting or rejecting the premise from logged working sets.');
|
|
2308
|
+
contract.push(' If current working top load is higher than the prior comparable session, say it increased and do not describe the lift as declining.');
|
|
2309
|
+
contract.push(' When rejecting the premise because top load increased, avoid the words "drop-off", "dropping off", "decline", "declining", "regress", or "regressing" in the answer.');
|
|
2310
|
+
contract.push(' Mention warmups separately when the evidence marks warmup sets excluded.');
|
|
2311
|
+
contract.push(' Do not mention record estimates unless the user asked for them.');
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
if (contract.length > 0) {
|
|
2315
|
+
lines.push('');
|
|
2316
|
+
lines.push(...contract);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
if (noteAtEnd) {
|
|
2320
|
+
lines.push('');
|
|
2321
|
+
lines.push(note);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
1903
2325
|
function normalizeCoachObservationForAsk(observation) {
|
|
1904
2326
|
if (!observation || typeof observation !== 'object') return null;
|
|
1905
2327
|
const id = String(observation.id ?? '').trim();
|
|
@@ -2310,8 +2732,8 @@ export function askObservationFollowUpContext(snapshot, question, observation, {
|
|
|
2310
2732
|
appendExcludeNote(lines, exclude);
|
|
2311
2733
|
const includedFacts = rankedCoachFactsForAsk(snapshot, question, 'general', { facts: coachFacts });
|
|
2312
2734
|
const includedCoachFactIds = appendCoachFactsContextBeforeExcludeNote(lines, includedFacts, exclude);
|
|
2313
|
-
const toolMetadata = askToolMetadata(tools, provenance);
|
|
2314
2735
|
const finalizedEvidencePlan = finalizeEvidencePlan(evidencePlan, tools);
|
|
2736
|
+
const toolMetadata = askToolMetadata(tools, provenance, { requiredTools: finalizedEvidencePlan.requiredTools });
|
|
2315
2737
|
const context = lines.join('\n');
|
|
2316
2738
|
const includedSections = [
|
|
2317
2739
|
'header',
|
|
@@ -2326,6 +2748,7 @@ export function askObservationFollowUpContext(snapshot, question, observation, {
|
|
|
2326
2748
|
const intentMetadata = {
|
|
2327
2749
|
route: 'coach_observation_followup',
|
|
2328
2750
|
effectiveRoute: 'coach_observation_followup',
|
|
2751
|
+
responseProfile: ASK_RESPONSE_PROFILES.structured,
|
|
2329
2752
|
confidence: 0.86,
|
|
2330
2753
|
entities: {
|
|
2331
2754
|
exercises: exercises.map((exercise) => ({
|
|
@@ -2361,6 +2784,7 @@ export function askObservationFollowUpContext(snapshot, question, observation, {
|
|
|
2361
2784
|
route: 'coach_observation_followup',
|
|
2362
2785
|
effectiveRoute: 'coach_observation_followup',
|
|
2363
2786
|
fallbackRoute: null,
|
|
2787
|
+
responseProfile: ASK_RESPONSE_PROFILES.structured,
|
|
2364
2788
|
intent: intentMetadata,
|
|
2365
2789
|
namedExercises: exercises.map((exercise) => exercise.canonical),
|
|
2366
2790
|
namedExerciseLabels: exercises.map((exercise) => exercise.displayName),
|
|
@@ -2427,6 +2851,7 @@ export function askMissingObservationFollowUpContext(snapshot, _question, reques
|
|
|
2427
2851
|
const intentMetadata = {
|
|
2428
2852
|
route: 'coach_observation_followup',
|
|
2429
2853
|
effectiveRoute: 'coach_observation_followup_missing',
|
|
2854
|
+
responseProfile: ASK_RESPONSE_PROFILES.structured,
|
|
2430
2855
|
confidence: 0.5,
|
|
2431
2856
|
entities: { exercises: [] },
|
|
2432
2857
|
timeframe: null,
|
|
@@ -2452,6 +2877,7 @@ export function askMissingObservationFollowUpContext(snapshot, _question, reques
|
|
|
2452
2877
|
route: 'coach_observation_followup',
|
|
2453
2878
|
effectiveRoute: 'coach_observation_followup_missing',
|
|
2454
2879
|
fallbackRoute: null,
|
|
2880
|
+
responseProfile: ASK_RESPONSE_PROFILES.structured,
|
|
2455
2881
|
intent: intentMetadata,
|
|
2456
2882
|
namedExercises: [],
|
|
2457
2883
|
namedExerciseLabels: [],
|
|
@@ -2475,12 +2901,19 @@ export function askMissingObservationFollowUpContext(snapshot, _question, reques
|
|
|
2475
2901
|
};
|
|
2476
2902
|
}
|
|
2477
2903
|
|
|
2478
|
-
export function askRoutedContext(snapshot, question, { exclude = new Set(), coachFacts = null, coachObservations = null, history = [], today = new Date() } = {}) {
|
|
2904
|
+
export function askRoutedContext(snapshot, question, { exclude = new Set(), coachFacts = null, coachObservations = null, history = [], today = new Date(), responseProfileOverride = null } = {}) {
|
|
2479
2905
|
const contextSnapshot = Array.isArray(coachObservations)
|
|
2480
2906
|
? { ...snapshot, coachObservations }
|
|
2481
2907
|
: snapshot;
|
|
2482
2908
|
const evidencePlan = planAskEvidence(contextSnapshot, question, { exclude, history, today });
|
|
2483
2909
|
const { route, effectiveRoute, fallbackRoute, namedExercises, namedExerciseLabels, sessionLabel = null, sessionReference = null, since = null } = evidencePlan;
|
|
2910
|
+
// Surfaces that share this context builder but must stay terse (e.g. the weekly
|
|
2911
|
+
// check-in, which runs under WEEKLY_CHECKIN_PROMPT) can force a profile so the
|
|
2912
|
+
// expansive evidence merge and score headline do not bleed in under a
|
|
2913
|
+
// tight-reply prompt.
|
|
2914
|
+
const responseProfile = responseProfileOverride
|
|
2915
|
+
?? evidencePlan.intent?.responseProfile
|
|
2916
|
+
?? ASK_RESPONSE_PROFILES.expansive;
|
|
2484
2917
|
const namedExerciseItems = namedExercises.map((canonical, index) => ({
|
|
2485
2918
|
canonical,
|
|
2486
2919
|
displayName: namedExerciseLabels[index] ?? canonical
|
|
@@ -2529,10 +2962,30 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
|
|
|
2529
2962
|
} else {
|
|
2530
2963
|
built = buildGeneralAskContext(contextSnapshot, { exclude, today });
|
|
2531
2964
|
}
|
|
2965
|
+
const factLines = built.context.split('\n');
|
|
2966
|
+
const sparseNamedExerciseProgress = route === 'exercise_progress_summary'
|
|
2967
|
+
&& namedExerciseItems.length > 0
|
|
2968
|
+
&& (built.tools?.[0]?.rows?.length ?? 0) === 0;
|
|
2969
|
+
const expansiveEvidence = responseProfile === ASK_RESPONSE_PROFILES.expansive && !sparseNamedExerciseProgress
|
|
2970
|
+
? appendExpansiveEvidenceContextBeforeExcludeNote(factLines, contextSnapshot, {
|
|
2971
|
+
exclude,
|
|
2972
|
+
today,
|
|
2973
|
+
namedExercises: namedExerciseItems,
|
|
2974
|
+
existingSections: built.sections
|
|
2975
|
+
})
|
|
2976
|
+
: { sections: [], tools: [], provenance: [] };
|
|
2977
|
+
built = {
|
|
2978
|
+
...built,
|
|
2979
|
+
context: factLines.join('\n'),
|
|
2980
|
+
sections: [...built.sections, ...expansiveEvidence.sections],
|
|
2981
|
+
tools: [...(built.tools ?? []), ...expansiveEvidence.tools],
|
|
2982
|
+
provenance: [...(built.provenance ?? []), ...expansiveEvidence.provenance]
|
|
2983
|
+
};
|
|
2984
|
+
|
|
2532
2985
|
const tools = [...(built.tools ?? [])];
|
|
2533
2986
|
const provenance = [...(built.provenance ?? [])];
|
|
2534
2987
|
|
|
2535
|
-
|
|
2988
|
+
factLines.splice(0, factLines.length, ...built.context.split('\n'));
|
|
2536
2989
|
const includedFacts = rankedCoachFactsForAsk(snapshot, question, effectiveRoute, { facts: coachFacts });
|
|
2537
2990
|
const includedCoachFactIds = appendCoachFactsContextBeforeExcludeNote(factLines, includedFacts, exclude);
|
|
2538
2991
|
const shouldIncludeCoachObservations = !exclude.has('coach_observations');
|
|
@@ -2561,6 +3014,14 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
|
|
|
2561
3014
|
provenance.push(coachToolProvenance('session_observation_comparisons', comparisonTool));
|
|
2562
3015
|
appendSessionObservationComparisonsBeforeExcludeNote(factLines, sessionObservationComparisons, exclude);
|
|
2563
3016
|
}
|
|
3017
|
+
appendAskAnswerContract(factLines, {
|
|
3018
|
+
route,
|
|
3019
|
+
responseProfile,
|
|
3020
|
+
namedExerciseLabels,
|
|
3021
|
+
builtTools: tools,
|
|
3022
|
+
sessionObservationComparisons,
|
|
3023
|
+
question
|
|
3024
|
+
});
|
|
2564
3025
|
const currentSessionIds = uniqueArray(sessionObservationComparisons.map((row) => row.sessionId));
|
|
2565
3026
|
const includedCoachFactKinds = uniqueArray(includedFacts.map((fact) => fact.kind));
|
|
2566
3027
|
const includedCoachFactSources = uniqueArray(includedFacts.map((fact) => {
|
|
@@ -2578,8 +3039,8 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
|
|
|
2578
3039
|
...(sessionObservationComparisons.length > 0 ? ['session_observation_comparisons'] : [])
|
|
2579
3040
|
]
|
|
2580
3041
|
};
|
|
2581
|
-
const toolMetadata = askToolMetadata(tools, provenance);
|
|
2582
3042
|
const finalizedEvidencePlan = finalizeEvidencePlan(evidencePlan, tools);
|
|
3043
|
+
const toolMetadata = askToolMetadata(tools, provenance, { requiredTools: finalizedEvidencePlan.requiredTools });
|
|
2583
3044
|
const intent = {
|
|
2584
3045
|
...evidencePlan.intent,
|
|
2585
3046
|
effectiveRoute
|
|
@@ -2601,6 +3062,7 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
|
|
|
2601
3062
|
route,
|
|
2602
3063
|
effectiveRoute,
|
|
2603
3064
|
fallbackRoute,
|
|
3065
|
+
responseProfile,
|
|
2604
3066
|
intent,
|
|
2605
3067
|
namedExercises,
|
|
2606
3068
|
namedExerciseLabels,
|