Add manual context field for AI insights
Free-text setting fed directly into every briefing's prompt as ground truth (e.g. "Joseph Trice-Rolph is on a flexi rota, has no fixed rota shifts — account for that in overspend"), so known context the data itself can't show doesn't get flagged as an anomaly. New ai_insights_manual_context config key, a textarea in Settings, and a top-of-prompt section in the job with an explicit instruction to treat it as ground truth. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
c09b31c41e
commit
d36655c02c
4 changed files with 38 additions and 6 deletions
|
|
@ -111,7 +111,8 @@ export async function initDb() {
|
||||||
('ai_insights_enabled', 'false'),
|
('ai_insights_enabled', 'false'),
|
||||||
('ai_insights_model', 'claude-haiku-4-5-20251001'),
|
('ai_insights_model', 'claude-haiku-4-5-20251001'),
|
||||||
('ai_insights_schedule_time', '07:15'),
|
('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
|
ON CONFLICT (key) DO NOTHING
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,17 +37,19 @@ async function getCostCol() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAiInsightsConfig() {
|
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_enabled'),
|
||||||
getConfig('ai_insights_model'),
|
getConfig('ai_insights_model'),
|
||||||
getConfig('ai_insights_schedule_time'),
|
getConfig('ai_insights_schedule_time'),
|
||||||
getConfig('ai_insights_daily_token_budget'),
|
getConfig('ai_insights_daily_token_budget'),
|
||||||
|
getConfig('ai_insights_manual_context'),
|
||||||
])
|
])
|
||||||
return {
|
return {
|
||||||
enabled: enabled === 'true',
|
enabled: enabled === 'true',
|
||||||
model: model || DEFAULT_MODEL,
|
model: model || DEFAULT_MODEL,
|
||||||
scheduleTime: scheduleTime || '07:15',
|
scheduleTime: scheduleTime || '07:15',
|
||||||
dailyTokenBudget: parseInt(dailyTokenBudget, 10) || DEFAULT_DAILY_TOKEN_BUDGET,
|
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 =
|
const systemMsg =
|
||||||
"You are an AI assistant for a wage cost controller in a UK hospitality business. Analyze the data below and " +
|
"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 " +
|
"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. " +
|
"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 " +
|
"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" +
|
"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 " +
|
"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."
|
"what's changed, what's resolved, and what's still an open issue. Don't just repeat the same points verbatim."
|
||||||
|
|
||||||
const lines = []
|
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) {
|
if (recentInsights.length && toISODate(new Date()) <= METHOD_CHANGE_NOTE_UNTIL) {
|
||||||
lines.push('## Methodology Note (25/07/2026)')
|
lines.push('## Methodology Note (25/07/2026)')
|
||||||
lines.push(
|
lines.push(
|
||||||
|
|
@ -513,7 +524,7 @@ export async function generateInsight(triggeredBy = 'scheduler') {
|
||||||
getRecentInsights(3),
|
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
|
let result
|
||||||
try {
|
try {
|
||||||
|
|
@ -527,7 +538,7 @@ export async function generateInsight(triggeredBy = 'scheduler') {
|
||||||
model: result.model,
|
model: result.model,
|
||||||
input_tokens: result.input_tokens,
|
input_tokens: result.input_tokens,
|
||||||
output_tokens: result.output_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,
|
triggered_by: triggeredBy,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { pool, getConfig, setConfig } from '../db.js'
|
||||||
const ALLOWED_KEYS = new Set([
|
const ALLOWED_KEYS = new Set([
|
||||||
'forecasting_url', 'forecasting_api_key', 'show_oncosts', 'departments', 'dept_budget_pcts', 'forecast_method',
|
'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_enabled', 'ai_insights_model', 'ai_insights_schedule_time', 'ai_insights_daily_token_budget',
|
||||||
|
'ai_insights_manual_context',
|
||||||
])
|
])
|
||||||
|
|
||||||
export async function settingsRoutes(fastify) {
|
export async function settingsRoutes(fastify) {
|
||||||
|
|
@ -15,7 +16,8 @@ export async function settingsRoutes(fastify) {
|
||||||
WHERE key IN (
|
WHERE key IN (
|
||||||
'forecasting_url','forecasting_api_key','show_oncosts','departments','forecast_method',
|
'forecasting_url','forecasting_api_key','show_oncosts','departments','forecast_method',
|
||||||
'sync_last_at','backfill_last_at',
|
'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`
|
ORDER BY key`
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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_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_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_daily_token_budget', value: settings.ai_insights_daily_token_budget ?? '5000' },
|
||||||
|
{ key: 'ai_insights_manual_context', value: settings.ai_insights_manual_context ?? '' },
|
||||||
])
|
])
|
||||||
setSaved(true)
|
setSaved(true)
|
||||||
setTimeout(() => setSaved(false), 2000)
|
setTimeout(() => setSaved(false), 2000)
|
||||||
|
|
@ -344,6 +345,23 @@ export default function SettingsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<label style={{ display: 'block', fontSize: 12, fontWeight: 500, marginBottom: 4, color: 'var(--text-muted)' }}>
|
||||||
|
Additional Context
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
style={{ width: '100%', resize: 'vertical', fontFamily: 'inherit' }}
|
||||||
|
value={settings.ai_insights_manual_context ?? ''}
|
||||||
|
onChange={e => handleChange('ai_insights_manual_context', e.target.value)}
|
||||||
|
placeholder="e.g. Joseph Trice-Rolph is on a flexi rota and has no fixed rota shifts — account for that when flagging overspend."
|
||||||
|
/>
|
||||||
|
<p style={{ margin: '4px 0 0', fontSize: 12, color: 'var(--text-muted)' }}>
|
||||||
|
Free text fed directly into every briefing as known context — use it for things the data can't show
|
||||||
|
(contract quirks, known one-off causes, planned changes) so they don't get flagged as anomalies.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'flex-start' }}>
|
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'flex-start' }}>
|
||||||
<div>
|
<div>
|
||||||
<button className="btn btn-secondary" onClick={handleTestConnection} disabled={testStatus === 'testing'}>
|
<button className="btn btn-secondary" onClick={handleTestConnection} disabled={testStatus === 'testing'}>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue