Files
proof-of-work/proof-of-work/lib/ai/systemPromptBase.ts
T
Keysat 5e291203a5 v1.1.0:4 — multi-config AI, background generation, ollama auto-detect, system prompt overhaul
User-feedback-driven release after testing v1.1.0:3. Nine themes:

1. Multi-config persistence
   - New AIConfigProfile table (per-user). Save N configs, toggle one
     active. Switching providers no longer wipes the previous setup.
   - UserPreferences gains activeAIConfigId; legacy single-config
     columns are mirrored from the active profile so existing reads
     keep working without conditional logic.
   - Idempotent boot migration lifts any existing single-config row
     into a default profile.

2. Ollama auto-detect
   - The "Add config" form probes /api/tags on the StartOS internal
     addresses (ollama.startos / ollama.embassy on :11434). If
     reachable: URL pre-fills, model field becomes a dropdown of
     installed models. Fixes the copy-paste UX.

3. Curated model dropdowns for major providers
   - Claude: Opus 4.7, Sonnet 4.6 (1M ctx), Haiku 4.5
   - OpenAI: GPT-5.5, 5.4, 5.4-mini, 5.4-nano
   - Gemini: 3.1-pro-preview, 2.5-pro, 2.5-flash, etc.
   - "Other (type your own)" stays for niche models.
   - Fixes "I tried gemini-3.0-pro and got 404."

4. Background generation
   - lib/ai/generationRunner.ts: detached runner with in-memory
     pub/sub bus. POST /api/ai/generate kicks it off and returns
     immediately. SSE stream attaches by id. The runner survives
     request cancellation; navigating away no longer kills it.
   - New AIGeneration columns: progressText (in-flight stream),
     durationMs (final wall-clock).
   - Generate UI shows a banner explaining background-safety.
   - History detail page polls progress + renders partial JSON
     live for cross-process resume (page refresh, new tab).

5. System prompt overhaul
   - lib/ai/systemPromptBase.ts: structural contract prepended to
     every template. Forces JSON-only output, library-exerciseId
     usage (kills "exerciseId doesn't belong to this user" errors),
     and per-resistance-exercise suggestedWeight (with-history vs
     without-history variants).
   - aiExerciseSchema + ProgramExercise gain suggestedWeight +
     suggestedWeightUnit. Starting a workout from a ProgramDay
     pre-populates SetLog.weight from the suggestion.

6. Test connection improvements
   - Latency in seconds (was ms — confusing for slow Ollama).
   - Stale "✓ Connected" clears on form change.
   - Per-config Test (no need to activate first).
   - Generous maxOutputTokens for thinking models.
   - Gemini surfaces finishReason on empty response (e.g. "blocked
     by safety filter") instead of generic "empty response."
   - Test endpoint accepts a draft body so you can verify before
     saving + before activating.

7. History detail view
   - Click row → full program tree + exact prompts sent. Apply from
     here without re-generating. Pending rows poll for progress.

8. Sidebar sub-navigation
   - AI: Generate / History / Templates
   - Settings: General / Password / Sessions / AI integration /
     Export / Instance (admin) / Danger zone, with anchor scroll.

9. API key UX
   - "Key saved" indicator on saved configs (was confusing to see
     an empty input after a successful save).

Schema migrations (additive, idempotent in entrypoint):
  - AIConfigProfile table created
  - UserPreferences.activeAIConfigId
  - AIGeneration.progressText + durationMs
  - ProgramExercise.suggestedWeight + suggestedWeightUnit

Tests: 16 new (systemPromptBase, modelMenu, generationRunner). 177
total pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:09:01 -05:00

78 lines
4.0 KiB
TypeScript

/**
* Base system-prompt rules prepended to every template's prompt before
* sending to the model. Centralized here so we can tighten output
* constraints in one place rather than editing every template.
*
* Two main jobs:
* 1. Force the JSON output shape (no prose, no fences, picks library
* ids only — fixes "exerciseId doesn't belong to this user" errors)
* 2. Force a suggested starting weight per resistance exercise
* (the model otherwise tends to leave it null, which leaves the
* user with no concrete target on day 1)
*
* Templates supply their *coaching philosophy* (hypertrophy = volume +
* progressive overload, conditioning = aerobic base etc); this module
* supplies the *structural contract*.
*/
export interface BaseSystemPromptOpts {
/** "lbs" | "kg" — the user's preferred weight unit, used as the default
* suggestedWeightUnit when the model omits one. */
weightUnit: 'lbs' | 'kg';
/** Whether the user's workout history is being included. Toggles a
* short instruction on how to use it. */
hasHistoryContext: boolean;
/** True when the model is local (Ollama). Local models tend to need
* shorter, blunter rules and benefit from explicit examples. */
isLocalModel: boolean;
}
export function buildBaseSystemPrompt(opts: BaseSystemPromptOpts): string {
const lines: string[] = [];
lines.push(
'# OUTPUT CONTRACT (mandatory)',
'',
'1. Reply with EXACTLY ONE JSON object. No prose before or after. No ```json fences.',
'2. Every exercise must use an `exerciseId` from the LIBRARY block at the bottom of this prompt. NEVER invent ids. If nothing in the library matches, pick the closest fit and explain the substitution in `notes`.',
`3. Every resistance exercise MUST have a \`suggestedWeight\` (a number, in ${opts.weightUnit}). Cardio, stretching, and bodyweight exercises set it to null.`,
`4. \`suggestedWeightUnit\` should be "${opts.weightUnit}" unless the exercise is conventionally tracked in the other unit (e.g. kettlebells often kg). Omit for non-loaded exercises.`,
'5. Every exercise needs `sets` and either `repsMin` (with `repsMax` if a range) or a duration note.',
'6. Use `rpe` (1-10) for working sets to communicate intensity; warmups can be lower or omitted.',
'7. `restSeconds` is required for compound lifts; optional for accessories.',
'8. Keep day volumes realistic: 4-7 exercises, 60-75 minutes total. Include warm-up sets only if they belong in the program (don\'t list mobility separately unless the user asked).',
'9. The `notes` field is for coaching cues, tempo, technique reminders — keep it short, one sentence.',
);
if (opts.hasHistoryContext) {
lines.push(
'',
'# USING THE HISTORY BLOCK',
'',
'The HISTORY block below summarizes the user\'s last 90 days. Use it to:',
'- Pick `suggestedWeight` near their current working weights, NOT round numbers from nowhere.',
'- Address any STAGNANT lifts: deload, change rep ranges, swap variations, or work at a different RPE.',
'- Respect their training frequency (don\'t prescribe 5x/week if they\'ve been training 3x).',
'- Stay in their movement vocabulary unless they asked for variety.',
);
} else {
lines.push(
'',
'# WEIGHT GUIDANCE WITHOUT HISTORY',
'',
`Without prior performance data, set conservative \`suggestedWeight\` values: 50-65% of typical 1RM for the lift at the user's stated experience level. Use round increments common in commercial gyms (5${opts.weightUnit} jumps; 2.5${opts.weightUnit} for small accessories). Always add a coaching note like "adjust to leave 2-3 reps in reserve" so the user knows it's a starting estimate.`,
);
}
if (opts.isLocalModel) {
lines.push(
'',
'# LOCAL MODEL REMINDER',
'',
'You are running locally with limited reasoning. Stick to the simplest valid program that matches the request. Do not overthink. JSON only.',
);
}
return lines.join('\n');
}