GET/PATCH /api/v1/settings/ai return and change provider, base_url, model, temperature and system_prompt, with read-only api_key_last4 and key_verified_at. PATCH requires a preset provider, temperature 0-1 in 0.1 steps and a system_prompt of at most 8000 characters. PUT /settings/ai/api-key seals the key with secretbox and records its last four characters; POST /settings/ai/verify calls Provider.Verify and records key_verified_at or returns the provider's typed error; GET /settings/ai/models lists the live models; POST /settings/ai/reset-prompt restores household.DefaultPersonaPrompt. The new internal/ai/aisettings service also builds the harness.Config for each skill run from the saved settings. presets.ForAccount builds a provider's ListModels and Verify without a model, so a key can be checked and a model picked before one is chosen. Tests scan every response body from these routes for the full key, and a harness run with the Fake provider uses the PATCHed model and temperature. Maestro task: 176b2a30-6e90-4339-b430-5b8967b2fd32 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
3.2 KiB
Go
95 lines
3.2 KiB
Go
package household
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/db"
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
|
|
)
|
|
|
|
// DefaultPersonaPrompt is the default AI system prompt (CTXD PROMPTS.MD §4).
|
|
// SeedAISettings stores it on first boot and aisettings.Service.ResetPrompt
|
|
// restores it.
|
|
const DefaultPersonaPrompt = `You are MealQuest, a dinner planning assistant for one household. Suggest dinners
|
|
similar to their regular rotation, nudging them to try something a little new each
|
|
week. Prefer weeknight-friendly recipes with common grocery-store ingredients.
|
|
Flag crock pot meals and anything 30 minutes or less as "quick". Keep explanations
|
|
short, warm and specific — mention the household's own regular dinners when you
|
|
explain why something fits. Never suggest a dinner they already make or have
|
|
declined.`
|
|
|
|
// Seed values for the AI settings (CTXD CONTEXT.MD). The base URL and model
|
|
// are empty until the household picks a provider preset; there is no key.
|
|
const (
|
|
SeedAIProvider = "anthropic"
|
|
SeedAIBaseURL = ""
|
|
SeedAIModel = ""
|
|
SeedAITemperature = 0.7
|
|
)
|
|
|
|
// seedNotificationPrefs are the notification preferences for a new
|
|
// household, one per event: push for every event, chat only for the plan
|
|
// and the shopping list.
|
|
var seedNotificationPrefs = []struct {
|
|
Event string
|
|
Push, Chat bool
|
|
}{
|
|
{Event: "plan_ready", Push: true, Chat: true},
|
|
{Event: "list_ready", Push: true, Chat: true},
|
|
{Event: "day_of_reminder", Push: true, Chat: false},
|
|
{Event: "new_suggestions", Push: true, Chat: false},
|
|
{Event: "rotation_review", Push: true, Chat: false},
|
|
}
|
|
|
|
// SeedAISettings creates the AI settings row with the seed values and
|
|
// DefaultPersonaPrompt if it does not exist. An existing row is never
|
|
// changed.
|
|
func SeedAISettings(ctx context.Context, st *store.Store) error {
|
|
return st.WithTx(ctx, func(tx *sql.Tx) error {
|
|
q := db.New(tx)
|
|
_, err := q.GetAISettings(ctx)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
return fmt.Errorf("household: read AI settings: %w", err)
|
|
}
|
|
now := store.Now().String()
|
|
_, err = q.UpsertAISettings(ctx, db.UpsertAISettingsParams{
|
|
Provider: SeedAIProvider,
|
|
BaseUrl: SeedAIBaseURL,
|
|
Model: SeedAIModel,
|
|
Temperature: SeedAITemperature,
|
|
SystemPrompt: sql.NullString{String: DefaultPersonaPrompt, Valid: true},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("household: seed AI settings: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// SeedNotificationPrefs creates the seed notification preference row of
|
|
// every event that has none. Existing rows are never changed.
|
|
func SeedNotificationPrefs(ctx context.Context, st *store.Store) error {
|
|
return st.WithTx(ctx, func(tx *sql.Tx) error {
|
|
now := store.Now().String()
|
|
for _, p := range seedNotificationPrefs {
|
|
_, err := tx.ExecContext(ctx,
|
|
`INSERT INTO notification_prefs (event, push, chat, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
ON CONFLICT (event) DO NOTHING`,
|
|
p.Event, p.Push, p.Chat, now, now)
|
|
if err != nil {
|
|
return fmt.Errorf("household: seed notification preference %s: %w", p.Event, err)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|