Add ideas_from_ingredients skill and job
CI / ci (push) Successful in 5m51s
CI / ci (pull_request) Successful in 5m45s

Implement the ideas_from_ingredients v1 skill on the AI harness: it asks
for 8 ideas {name, uses[], missing[], est_minutes, method, difficulty,
search_query}, keeps only "uses" entries whose harness.Normalize form is
one of the household's listed ingredients (written as the household wrote
them, in its order), drops ideas using none or repeating another, and
computes the summary line from the corrected lists.

Add internal/ideas: an ideas_from_ingredients job that grounds the ideas
with Grounder.GroundMany in the model's order until 5 are kept, drops the
ungroundable ones and, for quick_only, those whose grounded recipe is not
a Quick dinner (crockpot or minutes_total <= 30), publishing the result
as each idea is kept. POST /api/v1/ideas/from-ingredients answers 202
{jobId}.

Maestro task c3c5f98e-7f7d-4c6b-a1ab-d45bb8e497f9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 19:01:03 -04:00
co-authored by Claude Opus 5.5
parent cb451baed4
commit b566be7ddc
24 changed files with 1817 additions and 25 deletions
+21 -2
View File
@@ -23,6 +23,7 @@ import (
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/frontend"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/gcal"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/household"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ideas"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/jobs"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/plan"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/recipes"
@@ -138,6 +139,11 @@ func run(cfg config.Config, logger *slog.Logger) error {
return err
}
ideasSvc, err := ideas.NewService(grounder, ideaFinder(ai, guard), runner, logger)
if err != nil {
return err
}
sessions, err := auth.NewSessionManager(db, cfg.SessionIdle, cfg.SessionMax)
if err != nil {
return err
@@ -154,7 +160,7 @@ func run(cfg config.Config, logger *slog.Logger) error {
return err
}
router, err := server.NewRouter(logger, version, cfg.TrustedProxy, authFlow, sessions, settings, search, ai, dinnerSvc, suggestSvc, planSvc, shopSvc, googleSvc, runner, photos, frontend.Assets())
router, err := server.NewRouter(logger, version, cfg.TrustedProxy, authFlow, sessions, settings, search, ai, dinnerSvc, suggestSvc, planSvc, shopSvc, googleSvc, ideasSvc, runner, photos, frontend.Assets())
if err != nil {
return err
}
@@ -280,7 +286,8 @@ func newJobRunner(db *store.Store, logger *slog.Logger) (*jobs.Runner, *jobKinds
// service registers its own kinds when it is built, before the runner
// starts (dinners.NewService registers dinners.KindGroundDinner and
// suggest.NewService suggest.KindSuggestionBatch, plan.NewService
// plan.KindDraftWeek, shopping.NewService shopping.KindTitleSupplyRun).
// plan.KindDraftWeek, shopping.NewService shopping.KindTitleSupplyRun,
// ideas.NewService ideas.KindFromIngredients).
func registerJobKinds(runner *jobs.Runner) (*jobKinds, error) {
builtins, err := jobs.RegisterBuiltins(runner)
if err != nil {
@@ -337,6 +344,18 @@ func archiveTitler(ai *aisettings.Service, guard *harness.Guard) shopping.Titler
}
}
// ideaFinder runs ideas_from_ingredients with the AI settings current at
// each run, sharing guard.
func ideaFinder(ai *aisettings.Service, guard *harness.Guard) ideas.Generator {
return func(ctx context.Context, scope string, in skills.IdeasRequest) (skills.Ideas, error) {
cfg, err := ai.HarnessConfig(ctx, guard)
if err != nil {
return skills.Ideas{}, err
}
return skills.IdeasFromIngredients(ctx, cfg, scope, in)
}
}
// logListening logs the startup line. Only the public URL's origin is logged
// so no credential-bearing URL component can reach the log.
func logListening(logger *slog.Logger, cfg config.Config) {