Skip to content

Commit 9b53a1b

Browse files
committed
Revert "Enable AI narration for top-ranked discoveries in insight engine"
This reverts commit 93ec2ac.
1 parent 93ec2ac commit 9b53a1b

File tree

3 files changed

+40
-113
lines changed

3 files changed

+40
-113
lines changed

supabase/functions/ai-engine/engines/pattern-spotter.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -560,19 +560,19 @@ describe('Pattern Spotter v2 — Full Pipeline', () => {
560560
// AI narrative
561561
// =========================================================================
562562

563-
it('calls AI provider for top-ranked discoveries (up to AI_NARRATION_MAX)', async () => {
563+
it('calls AI provider only for discoveries, not observations', async () => {
564564
const data = generateWhoopData(60);
565565
mockDailySummaries = data.dailySummaries;
566566
mockSleepSessions = data.sleepSessions;
567567

568568
await spotPatterns({ lookback_days: 90 }, 'user-1', mockProvider);
569569

570-
// AI narration is capped at AI_NARRATION_MAX (5) to control latency.
571-
// With 60 days of clear patterns, there should be ≥5 discoveries,
572-
// so we expect exactly 5 AI calls.
573-
const aiCallCount = mockProvider.chat.mock.calls.length;
574-
expect(aiCallCount).toBeGreaterThan(0);
575-
expect(aiCallCount).toBeLessThanOrEqual(5);
570+
const discoveryInserts = mockInsert.mock.calls.filter(
571+
(call: unknown[]) => (call[0] as Record<string, unknown>).discovery_type === 'unenrolled_pattern',
572+
);
573+
574+
// AI calls should match discovery count
575+
expect(mockProvider.chat).toHaveBeenCalledTimes(discoveryInserts.length);
576576
});
577577

578578
it('uses template narratives for observations (no AI cost)', async () => {

supabase/functions/ai-engine/engines/pattern-spotter.ts

Lines changed: 33 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,9 @@
99
*/
1010

1111
import { createClient } from '../../_shared/supabaseClient.ts';
12-
import { autoCorrectCompliance } from '../../_shared/compliance/output-validator.ts';
1312
import type { AIProvider } from '../providers/types.ts';
14-
import {
15-
buildPatternDetectionSystemPrompt,
16-
buildPatternDetectionUserPrompt,
17-
} from '../prompts/pattern-detection.ts';
1813

19-
// Shared modules
14+
// Shared modules (v2)
2015
import {
2116
METRIC_LABELS_V2,
2217
METRIC_UNITS_V2,
@@ -80,7 +75,6 @@ const DEFAULT_LOOKBACK_DAYS = 90;
8075
const MIN_GROUP_SIZE = 7;
8176
const MIN_EFFECT_SIZE_SAME_DAY = 5; // % change for same-day comparisons
8277
const MIN_EFFECT_SIZE_LAGGED = 7; // % change for lagged comparisons (stricter)
83-
const AI_NARRATION_MAX = 5; // Top N discoveries get AI narratives; rest get templates
8478

8579
// Slot limits disabled — all deduplicated candidates are surfaced
8680
// To re-enable, add slot-limit logic back in the FILTER step
@@ -1006,93 +1000,37 @@ export async function spotPatterns(
10061000
// =========================================================================
10071001

10081002
const newPatterns: Array<Record<string, unknown>> = [];
1009-
const systemPrompt = buildPatternDetectionSystemPrompt();
1010-
let aiCalls = 0;
10111003

1012-
// Top N discoveries get AI-generated narratives; rest get templates.
1013-
// This balances narrative quality with edge function latency (~2s per AI call).
1014-
for (let idx = 0; idx < discoveriesToSurface.length; idx++) {
1015-
const candidate = discoveriesToSurface[idx];
1004+
// Template narratives for discoveries (AI narration disabled to avoid
1005+
// edge function timeouts when surfacing large candidate sets)
1006+
for (const candidate of discoveriesToSurface) {
10161007
const direction = candidate.change_pct > 0 ? 'higher' : 'lower';
10171008
let title: string;
10181009
let summary: string;
1019-
let detailedNarrative: string | null = null;
1020-
let suggestedExperiment: string | null = null;
1021-
let aiModel: string | null = null;
1022-
1023-
// Attempt AI narration for top-ranked discoveries
1024-
if (idx < AI_NARRATION_MAX) {
1025-
try {
1026-
const userPrompt = buildPatternDetectionUserPrompt({
1027-
metricKey: candidate.metric_key,
1028-
metricLabel: candidate.metric_label,
1029-
segmentLabel: candidate.segment_label,
1030-
groupAMean: candidate.group_a_mean,
1031-
groupBMean: candidate.group_b_mean,
1032-
changePct: candidate.change_pct,
1033-
pValue: candidate.p_value,
1034-
effectSize: candidate.effect_size,
1035-
weeksObserved: candidate.weeks_observed,
1036-
dataPointsA: candidate.group_a_values?.length ?? 0,
1037-
dataPointsB: candidate.group_b_values?.length ?? 0,
1038-
unit: candidate.unit,
1039-
patternType: candidate.type,
1040-
});
1041-
1042-
const result = await provider.chat({
1043-
systemPrompt,
1044-
userPrompt,
1045-
temperature: 0.3,
1046-
maxTokens: 1024,
1047-
responseFormat: 'json',
1048-
});
1049-
1050-
const narrative = JSON.parse(result.content);
1051-
const { corrected: corrTitle } = autoCorrectCompliance(narrative.title ?? '');
1052-
const { corrected: corrSummary } = autoCorrectCompliance(narrative.summary ?? '');
1053-
const { corrected: corrDetailed } = autoCorrectCompliance(narrative.detailed_narrative ?? '');
1054-
1055-
title = corrTitle;
1056-
summary = corrSummary;
1057-
detailedNarrative = corrDetailed || null;
1058-
suggestedExperiment = narrative.suggested_experiment ?? null;
1059-
aiModel = result.model;
1060-
aiCalls++;
1061-
} catch (err) {
1062-
console.warn(`[PatternSpotter] AI narration failed for rank ${idx + 1}, using template: ${(err as Error).message}`);
1063-
// Fall through to template below
1064-
title = '';
1065-
}
1066-
} else {
1067-
title = ''; // Will be set by template
1068-
}
10691010

1070-
// Template fallback (used when AI wasn't attempted or failed)
1071-
if (!title) {
1072-
if (candidate.type === 'trend') {
1073-
title = `Your ${candidate.metric_label} appears to be ${candidate.direction}`;
1074-
summary = `Over the past ${candidate.weeks_observed} weeks, your ${candidate.metric_label} has ${candidate.direction === 'increasing' ? 'increased' : 'decreased'} by ${Math.abs(candidate.change_pct).toFixed(1)}%.`;
1075-
} else if (candidate.type === 'lagged_effect') {
1076-
title = `${candidate.segment_label} linked to next-day ${candidate.metric_label}`;
1077-
summary = `${candidate.segment_label}, your ${candidate.metric_label} the following day tends to be ${Math.abs(candidate.change_pct).toFixed(1)}% ${direction}.`;
1078-
} else if (candidate.type === 'temporal_distribution') {
1079-
title = `Your ${candidate.metric_label} is consistently ${direction === 'higher' ? 'higher' : 'lower'} in the ${candidate.segment_label}`;
1080-
summary = `Your ${candidate.metric_label} during ${candidate.segment_label} is ${Math.abs(candidate.change_pct).toFixed(1)}% ${direction} than your overall average.`;
1081-
} else if (candidate.type === 'excursion_cluster') {
1082-
title = `Your ${candidate.metric_label} excursions cluster in the ${candidate.segment_label}`;
1083-
summary = `Out-of-range ${candidate.metric_label} events tend to occur during ${candidate.segment_label}, significantly more than expected if evenly distributed.`;
1084-
} else if (candidate.type === 'sequential_change') {
1085-
const changeDir = candidate.change_pct > 0 ? 'rises' : 'falls';
1086-
title = `Your ${candidate.metric_label} consistently ${changeDir} ${candidate.segment_label}`;
1087-
summary = `Your ${candidate.metric_label} ${changeDir} by ${Math.abs(candidate.change_pct).toFixed(1)}% ${candidate.segment_label}, observed consistently across the analysis period.`;
1088-
} else if (candidate.type === 'stability_trend') {
1089-
const stabilityDir = candidate.change_pct < 0 ? 'stabilizing' : 'becoming more variable';
1090-
title = `Your ${candidate.metric_label} is ${stabilityDir}`;
1091-
summary = `The variability of your ${candidate.metric_label} has ${candidate.change_pct < 0 ? 'decreased' : 'increased'} by ${Math.abs(candidate.change_pct).toFixed(1)}% over the analysis period.`;
1092-
} else {
1093-
title = `${candidate.segment_label} linked to ${candidate.metric_label}`;
1094-
summary = `${candidate.segment_label} is associated with ${Math.abs(candidate.change_pct).toFixed(1)}% ${direction} ${candidate.metric_label} (p=${candidate.p_value.toFixed(3)}, d=${candidate.effect_size.toFixed(2)}).`;
1095-
}
1011+
if (candidate.type === 'trend') {
1012+
title = `Your ${candidate.metric_label} appears to be ${candidate.direction}`;
1013+
summary = `Over the past ${candidate.weeks_observed} weeks, your ${candidate.metric_label} has ${candidate.direction === 'increasing' ? 'increased' : 'decreased'} by ${Math.abs(candidate.change_pct).toFixed(1)}%.`;
1014+
} else if (candidate.type === 'lagged_effect') {
1015+
title = `${candidate.segment_label} linked to next-day ${candidate.metric_label}`;
1016+
summary = `${candidate.segment_label}, your ${candidate.metric_label} the following day tends to be ${Math.abs(candidate.change_pct).toFixed(1)}% ${direction}.`;
1017+
} else if (candidate.type === 'temporal_distribution') {
1018+
title = `Your ${candidate.metric_label} is consistently ${direction === 'higher' ? 'higher' : 'lower'} in the ${candidate.segment_label}`;
1019+
summary = `Your ${candidate.metric_label} during ${candidate.segment_label} is ${Math.abs(candidate.change_pct).toFixed(1)}% ${direction} than your overall average.`;
1020+
} else if (candidate.type === 'excursion_cluster') {
1021+
title = `Your ${candidate.metric_label} excursions cluster in the ${candidate.segment_label}`;
1022+
summary = `Out-of-range ${candidate.metric_label} events tend to occur during ${candidate.segment_label}, significantly more than expected if evenly distributed.`;
1023+
} else if (candidate.type === 'sequential_change') {
1024+
const changeDir = candidate.change_pct > 0 ? 'rises' : 'falls';
1025+
title = `Your ${candidate.metric_label} consistently ${changeDir} ${candidate.segment_label}`;
1026+
summary = `Your ${candidate.metric_label} ${changeDir} by ${Math.abs(candidate.change_pct).toFixed(1)}% ${candidate.segment_label}, observed consistently across the analysis period.`;
1027+
} else if (candidate.type === 'stability_trend') {
1028+
const stabilityDir = candidate.change_pct < 0 ? 'stabilizing' : 'becoming more variable';
1029+
title = `Your ${candidate.metric_label} is ${stabilityDir}`;
1030+
summary = `The variability of your ${candidate.metric_label} has ${candidate.change_pct < 0 ? 'decreased' : 'increased'} by ${Math.abs(candidate.change_pct).toFixed(1)}% over the analysis period.`;
1031+
} else {
1032+
title = `${candidate.segment_label} linked to ${candidate.metric_label}`;
1033+
summary = `${candidate.segment_label} is associated with ${Math.abs(candidate.change_pct).toFixed(1)}% ${direction} ${candidate.metric_label} (p=${candidate.p_value.toFixed(3)}, d=${candidate.effect_size.toFixed(2)}).`;
10961034
}
10971035

10981036
newPatterns.push({
@@ -1101,20 +1039,17 @@ export async function spotPatterns(
11011039
type: candidate.type,
11021040
title,
11031041
summary,
1104-
detailed_narrative: detailedNarrative,
11051042
change_pct: candidate.change_pct,
11061043
p_value: candidate.p_value,
11071044
effect_size: candidate.effect_size,
11081045
composite_score: candidate.composite_score,
1109-
suggested_experiment: suggestedExperiment,
1110-
ai_model: aiModel,
1046+
suggested_experiment: null,
11111047
});
11121048
}
11131049

11141050
diagnostics.narrate = {
1115-
ai_calls: aiCalls,
1116-
template_narratives: discoveriesToSurface.length - aiCalls + observationsToSurface.length,
1117-
ai_narration_max: AI_NARRATION_MAX,
1051+
ai_calls: 0,
1052+
template_narratives: discoveriesToSurface.length + observationsToSurface.length,
11181053
};
11191054

11201055
console.log(`[PatternSpotter] Narratives generated`, JSON.stringify(diagnostics.narrate));
@@ -1133,7 +1068,7 @@ export async function spotPatterns(
11331068
discovery_type: 'unenrolled_pattern' as const,
11341069
title: patternInfo.title as string,
11351070
summary: patternInfo.summary as string,
1136-
detailed_analysis: (patternInfo.detailed_narrative as string) ?? null,
1071+
detailed_analysis: null,
11371072
metrics_impact: [{
11381073
metric_key: candidate.metric_key,
11391074
metric_label: candidate.metric_label,
@@ -1149,8 +1084,8 @@ export async function spotPatterns(
11491084
confidence: candidate.p_value < 0.01 ? 'strong' : 'moderate',
11501085
confounders_noted: null,
11511086
suggested_experiment_id: null,
1152-
ai_model: (patternInfo.ai_model as string) ?? null,
1153-
ai_prompt_version: patternInfo.ai_model ? '4.0' : null,
1087+
ai_model: null,
1088+
ai_prompt_version: null,
11541089
status: 'new' as const,
11551090
};
11561091
});

supabase/functions/ai-engine/prompts/pattern-detection.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,11 @@ ${COMPLIANCE_GUIDELINES}
2626
- segment_comparison: A same-day association between a behavior and an outcome
2727
- lagged_effect: A behavior on day N associated with an outcome on day N+1
2828
- trend: A metric trending up or down over weeks
29-
- temporal_distribution: A metric is consistently higher or lower at a specific time of day
30-
- excursion_cluster: Out-of-range events (spikes or dips) cluster at a specific time of day
31-
- sequential_change: A metric consistently rises or falls between two time periods each day
32-
- stability_trend: The variability of a metric is changing over time (stabilizing or destabilizing)
3329
3430
Adapt the narrative style to the pattern type:
3531
- For segment_comparison: "On days when you [behavior], your [outcome] tends to be [direction]"
3632
- For lagged_effect: "After days when you [behavior], your [outcome] the next day tends to be [direction]"
3733
- For trend: "Over the past [N] weeks, your [metric] has been [direction]"
38-
- For temporal_distribution: "Your [metric] tends to be [higher/lower] during [time period]"
39-
- For excursion_cluster: "Your [metric] spikes/dips tend to cluster around [time period]"
40-
- For sequential_change: "Your [metric] consistently [rises/falls] between [time A] and [time B]"
41-
- For stability_trend: "Your [metric] variability is [stabilizing/destabilizing] over time"
4234
4335
## Suggested Experiment
4436

0 commit comments

Comments
 (0)