package basic import ( "encoding/json" "errors" "fmt" "math" ) // Sentinel errors returned by the basic tools. Use errors.Is to test for them. var ( // ErrInvalidArgument is returned when a tool is given a missing required // argument or an argument of the wrong type. ErrInvalidArgument = errors.New("basic: invalid argument") // ErrUnsupported is returned when an operation is not in the tool allowlist. ErrUnsupported = errors.New("basic: unsupported operation") // ErrEvaluation is returned when a calculator expression cannot be parsed or // evaluated. ErrEvaluation = errors.New("basic: evaluation failed") ) // asMap asserts args is a nil value or a map[string]any — the shape the // dispatcher passes when a tool's Parameters declares an object schema. func asMap(args any) (map[string]any, error) { if args == nil { return map[string]any{}, nil } m, ok := args.(map[string]any) if !ok { return nil, fmt.Errorf("basic: arguments must be an object: %w", ErrInvalidArgument) } return m, nil } // hasKey reports whether m contains key. func hasKey(m map[string]any, key string) bool { _, ok := m[key] return ok } // reqString returns the string argument under key, erroring when it is absent or // not a string. func reqString(m map[string]any, key string) (string, error) { v, ok := m[key] if !ok { return "", fmt.Errorf("basic: missing required argument %q: %w", key, ErrInvalidArgument) } s, ok := v.(string) if !ok { return "", fmt.Errorf("basic: argument %q must be a string: %w", key, ErrInvalidArgument) } return s, nil } // optString returns the string argument under key, or def when absent. A present // non-string is an error. func optString(m map[string]any, key, def string) (string, error) { if !hasKey(m, key) { return def, nil } return reqString(m, key) } // reqInt returns the integer argument under key, erroring when it is absent, not // a number, or not integral. func reqInt(m map[string]any, key string) (int, error) { v, ok := m[key] if !ok { return 0, fmt.Errorf("basic: missing required argument %q: %w", key, ErrInvalidArgument) } n, err := asInt(v) if err != nil { return 0, fmt.Errorf("basic: argument %q: %w", key, err) } return n, nil } // optInt returns the integer argument under key, or def when absent. func optInt(m map[string]any, key string, def int) (int, error) { if !hasKey(m, key) { return def, nil } return reqInt(m, key) } // optBool returns the boolean argument under key, or def when absent. func optBool(m map[string]any, key string, def bool) (bool, error) { if !hasKey(m, key) { return def, nil } v, ok := m[key].(bool) if !ok { return false, fmt.Errorf("basic: argument %q must be a boolean: %w", key, ErrInvalidArgument) } return v, nil } // reqStringMap returns the object argument under key coerced to map[string]string, // erroring when a value is not a string. func reqStringMap(m map[string]any, key string) (map[string]string, error) { v, ok := m[key] if !ok { return nil, fmt.Errorf("basic: missing required argument %q: %w", key, ErrInvalidArgument) } raw, ok := v.(map[string]any) if !ok { return nil, fmt.Errorf("basic: argument %q must be an object: %w", key, ErrInvalidArgument) } out := make(map[string]string, len(raw)) for k, val := range raw { s, ok := val.(string) if !ok { return nil, fmt.Errorf("basic: value %q must be a string: %w", k, ErrInvalidArgument) } out[k] = s } return out, nil } // strSliceArg returns the array-of-strings argument under key. func strSliceArg(m map[string]any, key string) ([]string, error) { v, ok := m[key] if !ok { return nil, fmt.Errorf("basic: missing required argument %q: %w", key, ErrInvalidArgument) } raw, ok := v.([]any) if !ok { return nil, fmt.Errorf("basic: argument %q must be an array: %w", key, ErrInvalidArgument) } out := make([]string, len(raw)) for i, item := range raw { s, ok := item.(string) if !ok { return nil, fmt.Errorf("basic: argument %q item %d must be a string: %w", key, i, ErrInvalidArgument) } out[i] = s } return out, nil } // strSliceSliceArg returns the array-of-arrays-of-strings argument under key. func strSliceSliceArg(m map[string]any, key string) ([][]string, error) { v, ok := m[key] if !ok { return nil, fmt.Errorf("basic: missing required argument %q: %w", key, ErrInvalidArgument) } raw, ok := v.([]any) if !ok { return nil, fmt.Errorf("basic: argument %q must be an array: %w", key, ErrInvalidArgument) } out := make([][]string, len(raw)) for i, item := range raw { row, ok := item.([]any) if !ok { return nil, fmt.Errorf("basic: argument %q item %d must be an array: %w", key, i, ErrInvalidArgument) } cells := make([]string, len(row)) for j, c := range row { s, ok := c.(string) if !ok { return nil, fmt.Errorf("basic: argument %q item %d cell %d must be a string: %w", key, i, j, ErrInvalidArgument) } cells[j] = s } out[i] = cells } return out, nil } // asInt converts a numeric-typed value (the types the dispatcher decodes JSON // into) to an int, rejecting non-integral floats. func asInt(v any) (int, error) { switch x := v.(type) { case int: return x, nil case int64: return int(x), nil case float64: if x != math.Trunc(x) || x < math.MinInt64 || x > math.MaxInt64 { return 0, fmt.Errorf("must be an integer: %w", ErrInvalidArgument) } return int(x), nil case json.Number: n, err := x.Int64() if err != nil { return 0, fmt.Errorf("must be an integer: %w", ErrInvalidArgument) } return int(n), nil default: return 0, fmt.Errorf("must be a number: %w", ErrInvalidArgument) } }