fix(recommender): remove Todoist fallback on orchestrator failure; add snooze exclusion
When fetchOrchestratorTip returned null (LiteLLM timeout, bad JSON, etc.) the recommender silently fell back to randomPolicy, serving a raw Todoist task with no rationale — explaining both reported symptoms. - Remove randomPolicy/signalToCandidate; return 204 when orchestrator fails so the UI shows "All clear" instead of a confusing Todoist task - Pass recent_tip through the stack (frontend → POST /recommend → fetchOrchestratorTip → ml/serving RecommendRequest → build_orchestrator_messages) so after snooze the LLM is instructed not to repeat the snoozed content Fixes #122 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import { eq, and, desc } from 'drizzle-orm';
|
||||
import { requireAuth, AuthenticatedRequest } from '../middleware/session.js';
|
||||
import { config } from '../config.js';
|
||||
import { bus } from '../events/bus.js';
|
||||
import type { TipCandidate, Signal } from '@oo/shared-types';
|
||||
import type { Tip, Signal } from '@oo/shared-types';
|
||||
import { todoistSource, dueAgeDays } from '../signals/todoist.js';
|
||||
export { dueAgeDays };
|
||||
import { googleHealthSource } from '../signals/google-health.js';
|
||||
@@ -26,32 +26,12 @@ export const _clearSignalCacheForTests = () => {
|
||||
googleHealthSource.clearCache();
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Signal → TipCandidate conversion
|
||||
// ---------------------------------------------------------------------------
|
||||
function signalToCandidate(signal: Signal): TipCandidate {
|
||||
return {
|
||||
id: signal.id,
|
||||
content: signal.content,
|
||||
source: signal.source as TipCandidate['source'],
|
||||
kind: signal.kind as TipCandidate['kind'],
|
||||
sourceId: (signal.metadata.todoistId as string | undefined) ?? undefined,
|
||||
createdAt: signal.timestamp,
|
||||
features: signal.features,
|
||||
};
|
||||
}
|
||||
|
||||
function randomPolicy(candidates: TipCandidate[]): TipCandidate | null {
|
||||
if (!candidates.length) return null;
|
||||
return candidates[Math.floor(Math.random() * candidates.length)];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Orchestrator: fetch agent snippets + call ml/serving /recommend
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface OrchestratorResult {
|
||||
tip: TipCandidate;
|
||||
tip: Tip;
|
||||
model: string | null;
|
||||
agentIds: string[];
|
||||
}
|
||||
@@ -72,6 +52,7 @@ async function fetchOrchestratorTip(
|
||||
hour: number,
|
||||
dayOfWeek: number,
|
||||
traceparent?: string,
|
||||
recentTip?: string,
|
||||
): Promise<OrchestratorResult | null> {
|
||||
const [allAgentRows, eligibleIds, scienceDestiny] = await Promise.all([
|
||||
getActiveAgentOutputs(userId),
|
||||
@@ -93,7 +74,7 @@ async function fetchOrchestratorTip(
|
||||
const res = await fetch(`${config.ML_SERVING_URL}/recommend`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...(traceparent ? { traceparent } : {}) },
|
||||
body: JSON.stringify({ user_id: userId, agent_outputs: agentOutputs, tasks, hour_of_day: hour, day_of_week: dayOfWeek, science_destiny: scienceDestiny ?? 50 }),
|
||||
body: JSON.stringify({ user_id: userId, agent_outputs: agentOutputs, tasks, hour_of_day: hour, day_of_week: dayOfWeek, science_destiny: scienceDestiny ?? 50, recent_tip: recentTip ?? null }),
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
@@ -110,7 +91,6 @@ async function fetchOrchestratorTip(
|
||||
kind: 'advice' as const,
|
||||
rationale: data.tip.rationale,
|
||||
createdAt: now,
|
||||
features: { is_overdue: false, task_age_days: 0, priority: 1 },
|
||||
},
|
||||
model: data.model ?? null,
|
||||
agentIds: agentOutputs.map((a) => a.agent_id),
|
||||
@@ -127,6 +107,7 @@ async function fetchOrchestratorTip(
|
||||
router.post('/recommend', requireAuth, async (req: AuthenticatedRequest, res: Response) => {
|
||||
const hour = new Date().getHours();
|
||||
const dayOfWeek = new Date().getDay();
|
||||
const { recent_tip: recentTip } = req.body as { recent_tip?: string };
|
||||
|
||||
const anyToken = await db
|
||||
.select({ id: integrationTokens.id })
|
||||
@@ -142,16 +123,16 @@ router.post('/recommend', requireAuth, async (req: AuthenticatedRequest, res: Re
|
||||
const signals = await aggregator.fetchAll(req.userId!);
|
||||
|
||||
const t0 = Date.now();
|
||||
const orchestrated = await fetchOrchestratorTip(req.userId!, signals, hour, dayOfWeek, req.traceparent);
|
||||
const orchestrated = await fetchOrchestratorTip(req.userId!, signals, hour, dayOfWeek, req.traceparent, recentTip);
|
||||
const latencyMs = Date.now() - t0;
|
||||
|
||||
const tip = orchestrated?.tip ?? randomPolicy(signals.map(signalToCandidate));
|
||||
if (!tip) {
|
||||
if (!orchestrated) {
|
||||
res.status(204).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const policy = orchestrated ? 'orchestrator' : 'random';
|
||||
const tip = orchestrated.tip;
|
||||
const policy = 'orchestrator';
|
||||
const servedAt = new Date().toISOString();
|
||||
|
||||
await db.insert(tipViews).values({ id: nanoid(), userId: req.userId!, tipId: tip.id, servedAt });
|
||||
@@ -162,16 +143,12 @@ router.post('/recommend', requireAuth, async (req: AuthenticatedRequest, res: Re
|
||||
tipId: tip.id,
|
||||
policy,
|
||||
mlScore: null,
|
||||
featuresJson: JSON.stringify(
|
||||
orchestrated
|
||||
? { agent_ids: orchestrated.agentIds, hour_of_day: hour, day_of_week: dayOfWeek }
|
||||
: { ...tip.features, hour_of_day: hour, day_of_week: dayOfWeek },
|
||||
),
|
||||
candidateCount: orchestrated ? 1 : signals.length,
|
||||
featuresJson: JSON.stringify({ agent_ids: orchestrated.agentIds, hour_of_day: hour, day_of_week: dayOfWeek }),
|
||||
candidateCount: 1,
|
||||
latencyMs,
|
||||
servedAt,
|
||||
promptVersion: orchestrated ? 'v4-orchestrator' : null,
|
||||
llmModel: orchestrated ? orchestrated.model : null,
|
||||
promptVersion: 'v4-orchestrator',
|
||||
llmModel: orchestrated.model,
|
||||
tipKind: tip.kind ?? null,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user