diff --git a/backend/src/db.js b/backend/src/db.js index 445431d..3f45688 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -111,7 +111,8 @@ export async function initDb() { ('ai_insights_enabled', 'false'), ('ai_insights_model', 'claude-haiku-4-5-20251001'), ('ai_insights_schedule_time', '07:15'), - ('ai_insights_daily_token_budget', '5000') + ('ai_insights_daily_token_budget', '5000'), + ('ai_insights_manual_context', '') ON CONFLICT (key) DO NOTHING `) } diff --git a/backend/src/jobs/ai-insights.js b/backend/src/jobs/ai-insights.js index f4a6ab9..8d82e82 100644 --- a/backend/src/jobs/ai-insights.js +++ b/backend/src/jobs/ai-insights.js @@ -37,17 +37,19 @@ async function getCostCol() { } export async function getAiInsightsConfig() { - const [enabled, model, scheduleTime, dailyTokenBudget] = await Promise.all([ + const [enabled, model, scheduleTime, dailyTokenBudget, manualContext] = await Promise.all([ getConfig('ai_insights_enabled'), getConfig('ai_insights_model'), getConfig('ai_insights_schedule_time'), getConfig('ai_insights_daily_token_budget'), + getConfig('ai_insights_manual_context'), ]) return { enabled: enabled === 'true', model: model || DEFAULT_MODEL, scheduleTime: scheduleTime || '07:15', dailyTokenBudget: parseInt(dailyTokenBudget, 10) || DEFAULT_DAILY_TOKEN_BUDGET, + manualContext: manualContext || '', } } @@ -350,7 +352,7 @@ export async function gatherRevenueCorrelation(monthProgress) { } } -export function buildPrompt(monthProgress, priorPeriod, rotaVsActual, forecast, anomalies, revenueCorrelation, recentInsights = []) { +export function buildPrompt(monthProgress, priorPeriod, rotaVsActual, forecast, anomalies, revenueCorrelation, recentInsights = [], manualContext = '') { const systemMsg = "You are an AI assistant for a wage cost controller in a UK hospitality business. Analyze the data below and " + "provide a concise daily briefing (3-5 bullet points). Focus on: how this month's wage cost is tracking " + @@ -358,11 +360,20 @@ export function buildPrompt(monthProgress, priorPeriod, rotaVsActual, forecast, "what might be causing it, individual pay anomalies worth a look, and wage cost as a percentage of revenue. " + "Use UK date format (DD/MM/YYYY) and GBP (£) for all monetary values. Be specific with department/employee " + "names and numbers. Keep it actionable — no fluff or generic advice.\n\n" + + "A 'Manual Context' section may be included below, written by a human who knows things the data can't show " + + "(e.g. an employee's contract type, a known one-off cause, a planned change). Treat it as ground truth and " + + "apply it directly — don't flag something as an anomaly or overspend if this context already explains it.\n\n" + "A 'Recent Previous Insights' section may be included below — compare against them explicitly: call out " + "what's changed, what's resolved, and what's still an open issue. Don't just repeat the same points verbatim." const lines = [] + if (manualContext.trim()) { + lines.push('## Manual Context (from the wage cost controller — treat as ground truth)') + lines.push(manualContext.trim()) + lines.push('') + } + if (recentInsights.length && toISODate(new Date()) <= METHOD_CHANGE_NOTE_UNTIL) { lines.push('## Methodology Note (25/07/2026)') lines.push( @@ -513,7 +524,7 @@ export async function generateInsight(triggeredBy = 'scheduler') { getRecentInsights(3), ]) - const { systemMsg, userMsg } = buildPrompt(monthProgress, priorPeriod, rotaVsActual, forecast, anomalies, revenueCorrelation, recentInsights) + const { systemMsg, userMsg } = buildPrompt(monthProgress, priorPeriod, rotaVsActual, forecast, anomalies, revenueCorrelation, recentInsights, config.manualContext) let result try { @@ -527,7 +538,7 @@ export async function generateInsight(triggeredBy = 'scheduler') { model: result.model, input_tokens: result.input_tokens, output_tokens: result.output_tokens, - data_snapshot: { monthProgress, priorPeriod, rotaVsActual, forecast, anomalies, revenueCorrelation }, + data_snapshot: { monthProgress, priorPeriod, rotaVsActual, forecast, anomalies, revenueCorrelation, manualContext: config.manualContext }, triggered_by: triggeredBy, }) diff --git a/backend/src/routes/settings.js b/backend/src/routes/settings.js index 17a36b5..551f475 100644 --- a/backend/src/routes/settings.js +++ b/backend/src/routes/settings.js @@ -4,6 +4,7 @@ import { pool, getConfig, setConfig } from '../db.js' const ALLOWED_KEYS = new Set([ 'forecasting_url', 'forecasting_api_key', 'show_oncosts', 'departments', 'dept_budget_pcts', 'forecast_method', 'ai_insights_enabled', 'ai_insights_model', 'ai_insights_schedule_time', 'ai_insights_daily_token_budget', + 'ai_insights_manual_context', ]) export async function settingsRoutes(fastify) { @@ -15,7 +16,8 @@ export async function settingsRoutes(fastify) { WHERE key IN ( 'forecasting_url','forecasting_api_key','show_oncosts','departments','forecast_method', 'sync_last_at','backfill_last_at', - 'ai_insights_enabled','ai_insights_model','ai_insights_schedule_time','ai_insights_daily_token_budget' + 'ai_insights_enabled','ai_insights_model','ai_insights_schedule_time','ai_insights_daily_token_budget', + 'ai_insights_manual_context' ) ORDER BY key` ) diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index baa40e3..15432be 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -62,6 +62,7 @@ export default function SettingsPage() { { key: 'ai_insights_model', value: settings.ai_insights_model ?? 'claude-haiku-4-5-20251001' }, { key: 'ai_insights_schedule_time', value: settings.ai_insights_schedule_time ?? '07:15' }, { key: 'ai_insights_daily_token_budget', value: settings.ai_insights_daily_token_budget ?? '5000' }, + { key: 'ai_insights_manual_context', value: settings.ai_insights_manual_context ?? '' }, ]) setSaved(true) setTimeout(() => setSaved(false), 2000) @@ -344,6 +345,23 @@ export default function SettingsPage() { +
+ +