describe_garden: list each plop's position, so a move can keep the layout #128

Merged
steve merged 2 commits from fix/describe-plop-coordinates into main 2026-08-23 04:43:48 +00:00
3 changed files with 28 additions and 11 deletions
+7 -5
View File
@@ -33,13 +33,15 @@ func NewToolbox(svc *service.Service, actorID int64, today string) *llm.Toolbox
"Summarize a garden: its dimensions, objects (with sizes/positions/version), and each "+ "Summarize a garden: its dimensions, objects (with sizes/positions/version), and each "+
"object's active plantings grouped by plant — how many, roughly where, when they went in, "+ "object's active plantings grouped by plant — how many, roughly where, when they went in, "+
"and days to maturity when known. A small group lists its plops individually (id + "+ "and days to maturity when known. A small group lists its plops individually (id + "+
"version, for move_planting/remove_planting); a large one (a grid-filled bed) does not — "+ "version for move_planting/remove_planting, and xCm/yCm in the object's local frame so a "+
"use list_plantings for those ids, or act on the whole group with remove_plantings.", "move can keep their layout); a large one (a grid-filled bed) does not — use "+
"list_plantings for those, or act on the whole group with remove_plantings.",
a.describeGarden), a.describeGarden),
llm.DefineTool("list_plantings", llm.DefineTool("list_plantings",
"List one object's active plops one by one, each with its id, version, location, count and "+ "List one object's active plops one by one, each with its id, version, position (xCm/yCm "+
"planting date — the detail describe_garden leaves out for a large group. Narrow to one "+ "in the object's local frame), location, count and planting date — the detail "+
"plant with plantId. Use it only when you need to address individual plops.", "describe_garden leaves out for a large group. Narrow to one plant with plantId. Use it "+
"only when you need to address individual plops.",
a.listPlantings), a.listPlantings),
llm.DefineTool("create_object", llm.DefineTool("create_object",
"Add an object (bed, grow_bag, container, in_ground, tree, path, structure) to a garden, positioned by its center in garden cm.", "Add an object (bed, grow_bag, container, in_ground, tree, path, structure) to a garden, positioned by its center in garden cm.",
+11 -6
View File
@@ -605,20 +605,24 @@ type DescribeGroup struct {
// DaysToMaturity is the plant's, when the catalog knows it — with PlantedAt, // DaysToMaturity is the plant's, when the catalog knows it — with PlantedAt,
// enough to say when the harvest is due. // enough to say when the harvest is due.
DaysToMaturity *int `json:"daysToMaturity,omitempty"` DaysToMaturity *int `json:"daysToMaturity,omitempty"`
// Each lists the plops individually (id, version, location) only when the // Each lists the plops individually (id, version, position, location) only
// group has at most maxListedPlops of them. // when the group has at most maxListedPlops of them.
Each []DescribePlanting `json:"each,omitempty"` Each []DescribePlanting `json:"each,omitempty"`
} }
// DescribePlanting is one plop with a rough compass location. ID + Version let // DescribePlanting is one plop with its position and a rough compass location.
// an agent address a single plop — remove it or move it — the same way // ID + Version let an agent address a single plop — remove it or move it — the
// DescribeObject.Version lets it edit an object. // same way DescribeObject.Version lets it edit an object. XCM/YCM are in the
// object's local frame: they are what lets a move keep the layout the plops
// had, which the compass word alone ("north", "south") cannot.
type DescribePlanting struct { type DescribePlanting struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Version int64 `json:"version"` Version int64 `json:"version"`
PlantID int64 `json:"plantId"` PlantID int64 `json:"plantId"`
Plant string `json:"plant"` Plant string `json:"plant"`
Count int `json:"count"` Count int `json:"count"`
XCM float64 `json:"xCm"`
YCM float64 `json:"yCm"`
Location string `json:"location"` Location string `json:"location"`
RadiusCM float64 `json:"radiusCm"` RadiusCM float64 `json:"radiusCm"`
PlantedAt string `json:"plantedAt,omitempty"` PlantedAt string `json:"plantedAt,omitempty"`
@@ -736,7 +740,8 @@ func describeGroups(o *domain.GardenObject, plops []domain.Planting, plantByID m
func describePlanting(pl domain.Planting, plantName string) DescribePlanting { func describePlanting(pl domain.Planting, plantName string) DescribePlanting {
d := DescribePlanting{ d := DescribePlanting{
ID: pl.ID, Version: pl.Version, PlantID: pl.PlantID, Plant: plantName, ID: pl.ID, Version: pl.Version, PlantID: pl.PlantID, Plant: plantName,
Count: effectiveCount(pl), Location: describeLocation(pl.XCM, pl.YCM), RadiusCM: pl.RadiusCM, Count: effectiveCount(pl), XCM: pl.XCM, YCM: pl.YCM,
Location: describeLocation(pl.XCM, pl.YCM), RadiusCM: pl.RadiusCM,
} }
if pl.PlantedAt != nil { if pl.PlantedAt != nil {
d.PlantedAt = *pl.PlantedAt d.PlantedAt = *pl.PlantedAt
+10
View File
@@ -628,10 +628,20 @@ func TestDescribeGardenGroupsByPlant(t *testing.T) {
if want := 2*derivedCount(20, 25) + 3; ba.Plants != want { if want := 2*derivedCount(20, 25) + 3; ba.Plants != want {
t.Errorf("basil plants = %d, want %d", ba.Plants, want) t.Errorf("basil plants = %d, want %d", ba.Plants, want)
} }
// The position is what lets a move keep the layout; "south" alone can't. The
// three basil plops were placed at exactly these local points.
placedAt := map[[2]float64]bool{{-100, 100}: true, {0, 150}: true, {100, 100}: true}
for _, e := range ba.Each { for _, e := range ba.Each {
if e.PlantedAt == "" || e.Version == 0 || e.ID == 0 { if e.PlantedAt == "" || e.Version == 0 || e.ID == 0 {
t.Errorf("listed plop %+v is missing id, version or date", e) t.Errorf("listed plop %+v is missing id, version or date", e)
} }
if !placedAt[[2]float64{e.XCM, e.YCM}] {
t.Errorf("listed plop %+v is not at a position a basil was placed at", e)
}
delete(placedAt, [2]float64{e.XCM, e.YCM})
}
if len(placedAt) != 0 {
t.Errorf("positions never listed: %v", placedAt)
} }
// The big group's ids are a call away, narrowed to one plant. // The big group's ids are a call away, narrowed to one plant.