updates on the ai and agent and all thse things awith onboarding
This commit is contained in:
178
internal/ai/playground/tools.go
Normal file
178
internal/ai/playground/tools.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package playground
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// The read tools the backend serves itself. Every other read tool in the
|
||||
// registry runs inside AI_engine (decide_*), calls an external service
|
||||
// (sequence_stops, simulate_pricing_quote) or an engine-only internal route
|
||||
// (list_express_*), and answers "unavailable" in the playground.
|
||||
//
|
||||
// Each executor selects named, non-personal columns only — never addresses,
|
||||
// names, phones or notes — and Redact runs over the result as a backstop.
|
||||
// Coordinates are rounded to 2 decimals (about 1 km).
|
||||
|
||||
const (
|
||||
scanDefaultLimit = 20
|
||||
scanMaxLimit = 50
|
||||
nearbyMaxKm = 10.0
|
||||
nearbyMaxCount = 20
|
||||
)
|
||||
|
||||
// bookingRow is the only projection of a booking the playground exposes.
|
||||
type bookingRow struct {
|
||||
Bookingid int `json:"bookingid"`
|
||||
Bookingno string `json:"bookingno"`
|
||||
Tenantid *int `json:"tenantid"`
|
||||
Status string `json:"status"`
|
||||
Pickuppincode string `json:"pickuppincode"`
|
||||
Deliverypincode string `json:"deliverypincode"`
|
||||
Deliverycity string `json:"deliverycity"`
|
||||
Pickuplatitude float64 `json:"pickuplat"`
|
||||
Pickuplongitude float64 `json:"pickuplon"`
|
||||
Deliverylatitude float64 `json:"deliverylat"`
|
||||
Deliverylongitude float64 `json:"deliverylon"`
|
||||
Assignedmileruserid *int `json:"assignedmileruserid"`
|
||||
Routekm *float64 `json:"routekm"`
|
||||
Createdat *time.Time `json:"-"`
|
||||
Createdatist string `json:"createdat_ist,omitempty" gorm:"-"`
|
||||
}
|
||||
|
||||
const bookingColumns = "bookingid, bookingno, tenantid, status, pickuppincode, deliverypincode, deliverycity, " +
|
||||
"pickuplatitude, pickuplongitude, deliverylatitude, deliverylongitude, assignedmileruserid, routekm, createdat"
|
||||
|
||||
func round2(f float64) float64 { return math.Round(f*100) / 100 }
|
||||
|
||||
func (r *bookingRow) tidy() {
|
||||
r.Pickuplatitude, r.Pickuplongitude = round2(r.Pickuplatitude), round2(r.Pickuplongitude)
|
||||
r.Deliverylatitude, r.Deliverylongitude = round2(r.Deliverylatitude), round2(r.Deliverylongitude)
|
||||
if r.Createdat != nil {
|
||||
// pickupbookings.createdat is timestamp WITHOUT zone holding IST digits.
|
||||
r.Createdatist = r.Createdat.Format("2006-01-02 15:04")
|
||||
}
|
||||
}
|
||||
|
||||
// Executors returns the read tools this backend can serve. A nil db or rdb
|
||||
// just leaves the tools that need it out (they then answer "unavailable").
|
||||
func Executors(db *gorm.DB, rdb *redis.Client) map[string]Executor {
|
||||
execs := map[string]Executor{}
|
||||
if db != nil {
|
||||
execs["get_booking_cache"] = getBooking(db)
|
||||
execs["scan_bookings"] = scanBookings(db)
|
||||
}
|
||||
if rdb != nil {
|
||||
execs["nearby_milers"] = nearbyMilers(rdb)
|
||||
}
|
||||
return execs
|
||||
}
|
||||
|
||||
func decode(input json.RawMessage, into any) error {
|
||||
if len(input) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(input, into); err != nil {
|
||||
return fmt.Errorf("invalid input: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getBooking(db *gorm.DB) Executor {
|
||||
return func(ctx context.Context, input json.RawMessage) (any, error) {
|
||||
var in struct {
|
||||
BookingID int `json:"booking_id"`
|
||||
}
|
||||
if err := decode(input, &in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.BookingID <= 0 {
|
||||
return nil, errors.New("booking_id (a positive integer) is required")
|
||||
}
|
||||
var row bookingRow
|
||||
res := db.WithContext(ctx).Table("pickupbookings").Select(bookingColumns).
|
||||
Where("bookingid = ?", in.BookingID).Limit(1).Scan(&row)
|
||||
if res.Error != nil {
|
||||
return nil, errors.New("booking lookup failed")
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return map[string]any{"found": false, "booking_id": in.BookingID}, nil
|
||||
}
|
||||
row.tidy()
|
||||
return map[string]any{"found": true, "booking": row, "source": "pickupbookings table"}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func scanBookings(db *gorm.DB) Executor {
|
||||
return func(ctx context.Context, input json.RawMessage) (any, error) {
|
||||
var in struct {
|
||||
Status string `json:"status"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
if err := decode(input, &in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.Limit <= 0 {
|
||||
in.Limit = scanDefaultLimit
|
||||
}
|
||||
if in.Limit > scanMaxLimit {
|
||||
in.Limit = scanMaxLimit
|
||||
}
|
||||
q := db.WithContext(ctx).Table("pickupbookings").Select(bookingColumns)
|
||||
if s := strings.TrimSpace(in.Status); s != "" {
|
||||
q = q.Where("status = ?", s)
|
||||
}
|
||||
var rows []bookingRow
|
||||
if err := q.Order("bookingid DESC").Limit(in.Limit).Scan(&rows).Error; err != nil {
|
||||
return nil, errors.New("booking scan failed")
|
||||
}
|
||||
byStatus := map[string]int{}
|
||||
for i := range rows {
|
||||
rows[i].tidy()
|
||||
byStatus[rows[i].Status]++
|
||||
}
|
||||
return map[string]any{"count": len(rows), "bystatus": byStatus, "bookings": rows, "order": "newest first"}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func nearbyMilers(rdb *redis.Client) Executor {
|
||||
return func(ctx context.Context, input json.RawMessage) (any, error) {
|
||||
var in struct {
|
||||
Lat *float64 `json:"lat"`
|
||||
Lon *float64 `json:"lon"`
|
||||
RadiusKm float64 `json:"radius_km"`
|
||||
}
|
||||
if err := decode(input, &in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.Lat == nil || in.Lon == nil || math.Abs(*in.Lat) > 90 || math.Abs(*in.Lon) > 180 {
|
||||
return nil, errors.New("lat and lon (decimal degrees) are required")
|
||||
}
|
||||
if in.RadiusKm <= 0 || in.RadiusKm > nearbyMaxKm {
|
||||
in.RadiusKm = 5
|
||||
}
|
||||
locs, err := rdb.GeoSearchLocation(ctx, "milers:locations", &redis.GeoSearchLocationQuery{
|
||||
GeoSearchQuery: redis.GeoSearchQuery{
|
||||
Longitude: *in.Lon, Latitude: *in.Lat,
|
||||
Radius: in.RadiusKm, RadiusUnit: "km", Sort: "ASC", Count: nearbyMaxCount,
|
||||
},
|
||||
WithDist: true,
|
||||
}).Result()
|
||||
if err != nil {
|
||||
return nil, errors.New("live rider positions are unavailable")
|
||||
}
|
||||
riders := make([]map[string]any, 0, len(locs))
|
||||
for _, l := range locs {
|
||||
riders = append(riders, map[string]any{"miler": l.Name, "distancekm": math.Round(l.Dist*100) / 100})
|
||||
}
|
||||
return map[string]any{"count": len(riders), "radiuskm": in.RadiusKm, "riders": riders}, nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user