Agent: catalog and garden tools, and a ready date on every describe group
- update_seed_lot / delete_seed_lot: correct or drop a recorded purchase
("it was three packets, not two"); the plant a lot is for stays fixed.
- delete_plant: remove a duplicate from the user's catalog. The service
already refuses while plantings (past seasons included) or a lot reference
it; the tool turns that sentinel into words the model can pass on, and
tells it not to clear those references to get its way.
- create_garden: a new place, with the service's defaults; the prompt says a
plan is still a copy_garden.
- describe_garden groups carry readyAround — planting date plus days to
maturity for the plops still in the ground — so "what can I pick this
week?" is a lookup rather than arithmetic the model got wrong live.
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -2,6 +2,7 @@ package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -211,6 +212,29 @@ func newToolbox(svc *service.Service, actorID int64, today string) (*llm.Toolbox
|
||||
"frost is usually around May 10\"), add it here. notes replaces the WHOLE text: take the "+
|
||||
"current notes from describe_garden, add the new line, and pass all of it.",
|
||||
a.updateGarden),
|
||||
llm.DefineTool("create_garden",
|
||||
"Start a NEW garden for the user — a different place (the front yard, the allotment), "+
|
||||
"sized in cm and owned by them. Not for a season plan: a plan is a copy_garden of the "+
|
||||
"real garden. The new garden opens from the gardens list; this conversation stays with "+
|
||||
"the one it started in.",
|
||||
a.createGarden),
|
||||
llm.DefineTool("update_seed_lot",
|
||||
"Correct a seed lot the user recorded — quantity, unit, vendor, link, purchase date, "+
|
||||
"packed-for year, germination rate or notes: \"it was three packets, not two\". Needs "+
|
||||
"the lot's id and version from list_seed_lots. Only the fields you pass change; the "+
|
||||
"plant a lot is for cannot change (record a new lot instead).",
|
||||
a.updateSeedLot),
|
||||
llm.DefineTool("delete_seed_lot",
|
||||
"Delete a seed lot the user recorded, by its id from list_seed_lots. Plantings attributed "+
|
||||
"to it stay in the garden, just no longer linked to a purchase. Permanent — seed lots "+
|
||||
"are not in the undo history — so delete only the lot the user pointed at.",
|
||||
a.deleteSeedLot),
|
||||
llm.DefineTool("delete_plant",
|
||||
"Delete a plant from the user's own catalog — a duplicate or a mistake. Refused while "+
|
||||
"anything is planted with it (in any garden, past seasons included) or a seed lot "+
|
||||
"records it; say so rather than removing those to make it deletable. Built-in plants "+
|
||||
"can't be deleted. Permanent — the catalog is not in the undo history.",
|
||||
a.deletePlant),
|
||||
), a
|
||||
}
|
||||
|
||||
@@ -834,3 +858,76 @@ func (a *adapter) updateGarden(ctx context.Context, args struct {
|
||||
}
|
||||
return a.svc.UpdateGarden(ctx, a.actor, args.GardenID, in, args.Version)
|
||||
}
|
||||
|
||||
func (a *adapter) createGarden(ctx context.Context, args struct {
|
||||
Name string `json:"name" description:"the garden's name"`
|
||||
WidthCM float64 `json:"widthCm" description:"optional width in cm (default 1000)"`
|
||||
HeightCM float64 `json:"heightCm" description:"optional height in cm (default 1000)"`
|
||||
UnitPref string `json:"units" description:"optional: metric (default) | imperial — how the gardener wants lengths shown"`
|
||||
Notes string `json:"notes" description:"optional standing notes about the place — zone, frost dates, soil"`
|
||||
GridSizeCM float64 `json:"gridSizeCm" description:"optional editor grid spacing in cm (default 100)"`
|
||||
SnapToGrid bool `json:"snapToGrid" description:"optional: snap objects to that grid"`
|
||||
}) (any, error) {
|
||||
return a.svc.CreateGarden(ctx, a.actor, service.GardenInput{
|
||||
Name: args.Name, WidthCM: args.WidthCM, HeightCM: args.HeightCM,
|
||||
UnitPref: strings.ToLower(strings.TrimSpace(args.UnitPref)), Notes: args.Notes,
|
||||
GridSizeCM: args.GridSizeCM, SnapToGrid: args.SnapToGrid,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *adapter) updateSeedLot(ctx context.Context, args struct {
|
||||
LotID int64 `json:"lotId" description:"seed lot to correct (its id from list_seed_lots)"`
|
||||
Version int64 `json:"version" description:"the lot's current version (from list_seed_lots)"`
|
||||
Quantity *float64 `json:"quantity" description:"optional corrected quantity, in the lot's unit"`
|
||||
Unit *string `json:"unit" description:"optional unit: seeds | grams | ounces | packets | bulbs | plants"`
|
||||
Vendor *string `json:"vendor" description:"optional vendor name"`
|
||||
SourceURL *string `json:"sourceUrl" description:"optional http(s) link to where it was bought; empty clears it"`
|
||||
PurchasedAt *string `json:"purchasedAt" description:"optional purchase date, YYYY-MM-DD"`
|
||||
PackedForYear *int `json:"packedForYear" description:"optional 'packed for' year from the packet"`
|
||||
GerminationPct *float64 `json:"germinationPct" description:"optional germination rate, 0–100"`
|
||||
Notes *string `json:"notes" description:"optional replacement notes"`
|
||||
}) (any, error) {
|
||||
if args.Quantity == nil && args.Unit == nil && args.Vendor == nil && args.SourceURL == nil &&
|
||||
args.PurchasedAt == nil && args.PackedForYear == nil && args.GerminationPct == nil && args.Notes == nil {
|
||||
return nil, fmt.Errorf("%w: say what to change about the lot", domain.ErrInvalidInput)
|
||||
}
|
||||
purchased := args.PurchasedAt
|
||||
if purchased != nil {
|
||||
on, err := parseDay(*purchased)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
purchased = &on
|
||||
}
|
||||
patch := service.SeedLotPatch{
|
||||
Vendor: args.Vendor, SourceURL: args.SourceURL, Quantity: args.Quantity, Unit: args.Unit, Notes: args.Notes,
|
||||
SetPurchasedAt: purchased != nil, PurchasedAt: purchased,
|
||||
SetPackedForYear: args.PackedForYear != nil, PackedForYear: args.PackedForYear,
|
||||
SetGerminationPct: args.GerminationPct != nil, GerminationPct: args.GerminationPct,
|
||||
}
|
||||
return a.svc.UpdateSeedLot(ctx, a.actor, args.LotID, patch, args.Version)
|
||||
}
|
||||
|
||||
func (a *adapter) deleteSeedLot(ctx context.Context, args struct {
|
||||
LotID int64 `json:"lotId" description:"seed lot to delete (its id from list_seed_lots)"`
|
||||
}) (any, error) {
|
||||
if err := a.svc.DeleteSeedLot(ctx, a.actor, args.LotID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"deleted": args.LotID}, nil
|
||||
}
|
||||
|
||||
func (a *adapter) deletePlant(ctx context.Context, args struct {
|
||||
PlantID int64 `json:"plantId" description:"plant to delete from the user's catalog (from find_plant)"`
|
||||
}) (any, error) {
|
||||
err := a.svc.DeletePlant(ctx, a.actor, args.PlantID)
|
||||
if errors.Is(err, domain.ErrPlantInUse) {
|
||||
// The sentinel's text is for a log line; the model needs to know what
|
||||
// to tell the person, and what not to do about it.
|
||||
return nil, fmt.Errorf("%w: the plant is still used — by plantings (past seasons count) or a seed lot — so it stays; tell the user rather than removing those", domain.ErrPlantInUse)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"deleted": args.PlantID}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user