//go:build majordomo package agent import ( "context" "gitea.stevedudenhoeffer.com/steve/majordomo/llm" "gitea.stevedudenhoeffer.com/steve/pansy/internal/service" ) // NewToolbox builds a majordomo toolbox over pansy's service layer, bound to a // single acting user. Every tool call runs as actorID, so pansy's permission // checks (requireGardenRole / objectForRole) apply unchanged. Construct one per // authenticated agent session: // // box := agent.NewToolbox(svc, session.UserID) // agent.Run(ctx, model, box, "fill the NE corner with garlic") func NewToolbox(svc *service.Service, actorID int64) *llm.Toolbox { a := &adapter{svc: svc, actor: actorID} return llm.NewToolbox("pansy", llm.DefineTool("list_gardens", "List the gardens the user can see (owned and shared), with the user's role on each.", a.listGardens), llm.DefineTool("describe_garden", "Summarize a garden: its dimensions, objects (with sizes/positions/version), and each object's active plantings with a rough compass location.", a.describeGarden), 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.", a.createObject), llm.DefineTool("move_object", "Move an object to a new center position (garden cm). Needs the object's current version from describe_garden.", a.moveObject), llm.DefineTool("place_planting", "Place one plop of a plant inside a plantable object, positioned in the object's LOCAL frame (0,0 = object center, -y = north).", a.placePlanting), llm.DefineTool("fill_region", "Fill a named region of a plantable object with a plant, hex-packed. Region is one of nw/ne/sw/se (corners), north/south/east/west or top/bottom/left/right (halves), or all.", a.fillRegion), llm.DefineTool("clear_object", "Remove all plants from an object (soft-remove; history is kept).", a.clearObject), ) } // adapter carries the service and the acting user for the tool handlers. type adapter struct { svc *service.Service actor int64 } func (a *adapter) listGardens(ctx context.Context, _ struct{}) (any, error) { return a.svc.ListGardens(ctx, a.actor) } func (a *adapter) describeGarden(ctx context.Context, args struct { GardenID int64 `json:"gardenId" description:"id of the garden to describe"` }) (any, error) { return a.svc.DescribeGarden(ctx, a.actor, args.GardenID) } func (a *adapter) createObject(ctx context.Context, args struct { GardenID int64 `json:"gardenId" description:"garden to add the object to"` Kind string `json:"kind" description:"bed | grow_bag | container | in_ground | tree | path | structure"` Name string `json:"name" description:"optional label for the object"` Shape string `json:"shape" description:"rect or circle (default rect)"` XCM float64 `json:"xCm" description:"center x in garden cm"` YCM float64 `json:"yCm" description:"center y in garden cm"` WidthCM float64 `json:"widthCm" description:"width in cm (a circle's diameter)"` HeightCM float64 `json:"heightCm" description:"height in cm"` }) (any, error) { return a.svc.CreateObject(ctx, a.actor, args.GardenID, service.ObjectInput{ Kind: args.Kind, Name: args.Name, Shape: args.Shape, XCM: args.XCM, YCM: args.YCM, WidthCM: args.WidthCM, HeightCM: args.HeightCM, }) } func (a *adapter) moveObject(ctx context.Context, args struct { ObjectID int64 `json:"objectId" description:"object to move"` XCM float64 `json:"xCm" description:"new center x in garden cm"` YCM float64 `json:"yCm" description:"new center y in garden cm"` Version int64 `json:"version" description:"the object's current version (from describe_garden)"` }) (any, error) { return a.svc.UpdateObject(ctx, a.actor, args.ObjectID, service.ObjectPatch{XCM: &args.XCM, YCM: &args.YCM}, args.Version) } func (a *adapter) placePlanting(ctx context.Context, args struct { ObjectID int64 `json:"objectId" description:"plantable object to plant in"` PlantID int64 `json:"plantId" description:"plant to place"` XCM float64 `json:"xCm" description:"center x in the object's local frame (cm; 0,0 = center, -y = north)"` YCM float64 `json:"yCm" description:"center y in the object's local frame (cm)"` RadiusCM float64 `json:"radiusCm" description:"plop radius in cm"` Count *int `json:"count" description:"optional explicit plant count; omit to derive from area ÷ spacing²"` }) (any, error) { return a.svc.CreatePlanting(ctx, a.actor, args.ObjectID, service.PlantingInput{ PlantID: args.PlantID, XCM: args.XCM, YCM: args.YCM, RadiusCM: args.RadiusCM, Count: args.Count, }) } func (a *adapter) fillRegion(ctx context.Context, args struct { ObjectID int64 `json:"objectId" description:"plantable object to fill"` Region string `json:"region" description:"nw|ne|sw|se corner, north|south|east|west (or top|bottom|left|right) half, or all"` PlantID int64 `json:"plantId" description:"plant to fill with"` SpacingOverride *float64 `json:"spacingOverrideCm" description:"optional in-row spacing override in cm; omit to use the plant's spacing"` }) (any, error) { return a.svc.FillNamedRegion(ctx, a.actor, args.ObjectID, args.Region, args.PlantID, args.SpacingOverride) } func (a *adapter) clearObject(ctx context.Context, args struct { ObjectID int64 `json:"objectId" description:"object to remove all plants from"` }) (any, error) { n, err := a.svc.ClearObject(ctx, a.actor, args.ObjectID) if err != nil { return nil, err } return map[string]int{"cleared": n}, nil }