diff --git a/docs/select-syntax.md b/docs/select-syntax.md new file mode 100644 index 0000000..b22c50f --- /dev/null +++ b/docs/select-syntax.md @@ -0,0 +1,36 @@ +The `select` operation chooses between two values based on a simple +conditions. It enables basic control flow within Gendo pipelines by selecting +either a `trueValue` or a `falseValue` depending on whether the `condition` +string exactly matches `"true"` (case-insensitive). + +The syntax of the `select` operation is as follows: + + select [destination] value trueValue falseValue + +The `destination` identifier is optional. If omitted, the result is bound to +the special slot `_`. The `condition` is required and must be a +string—typically the result of a previous model call or logic step. If +`condition` equals `"true"` (case-insensitive), the `trueValue` is chosen. +Otherwise, the `falseValue` is chosen. The selected value is then stored in +`destination` or in `_` if no destination is provided. + +An example with an explicit destination: + + select outcome "true" "Proceed" "Abort" + +In this example, the string `"Proceed"` is assigned to `outcome` because the +condition matches `"true"`. + +Another example using a prior model result as the condition: + + prompt isValid "Is the previous input acceptable? Reply 'true' or 'false'." + select validationMessage isValid "Input is acceptable." "Input is not acceptable." + +Here, the pipeline sends a prompt to the model. The response, assumed to be +`"true"` or `"false"`, is stored in `isValid`. The `select` instruction then +chooses an appropriate message and binds it to `validationMessage`. + +The `select` operation requires all arguments to be present and in order. It +performs no transformation beyond the selection logic, and it produces exactly +one value. All bindings follow the single-assignment rule: once a name is used +as a destination, it cannot be reassigned later in the same pipeline. diff --git a/pkg/primitives/select.go b/pkg/primitives/select.go new file mode 100644 index 0000000..1559eb1 --- /dev/null +++ b/pkg/primitives/select.go @@ -0,0 +1,34 @@ +package primitives + +import ( + "fmt" + + "github.com/hyperifyio/gnd/pkg/primitive_services" +) + +// Select represents the select primitive +type Select struct{} + +func (s *Select) Name() string { + return "/gnd/select" +} + +func (s *Select) Execute(args []interface{}) (interface{}, error) { + if len(args) != 3 { + return nil, fmt.Errorf("select expects exactly 3 arguments (condition, trueValue, falseValue), got %d", len(args)) + } + + condition := fmt.Sprintf("%v", args[0]) + trueValue := args[1] + falseValue := args[2] + + // Case-sensitive comparison with "true" + if condition == "true" { + return trueValue, nil + } + return falseValue, nil +} + +func init() { + primitive_services.RegisterPrimitive(&Select{}) +} diff --git a/pkg/primitives/select_test.go b/pkg/primitives/select_test.go new file mode 100644 index 0000000..d78e048 --- /dev/null +++ b/pkg/primitives/select_test.go @@ -0,0 +1,86 @@ +package primitives + +import ( + "testing" +) + +func TestSelect(t *testing.T) { + tests := []struct { + name string + args []interface{} + expected interface{} + wantErr bool + }{ + { + name: "true condition", + args: []interface{}{"true", "Proceed", "Abort"}, + expected: "Proceed", + wantErr: false, + }, + { + name: "false condition", + args: []interface{}{"false", "Proceed", "Abort"}, + expected: "Abort", + wantErr: false, + }, + { + name: "uppercase true", + args: []interface{}{"TRUE", "Proceed", "Abort"}, + expected: "Abort", + wantErr: false, + }, + { + name: "uppercase false", + args: []interface{}{"FALSE", "Proceed", "Abort"}, + expected: "Abort", + wantErr: false, + }, + { + name: "mixed case true", + args: []interface{}{"True", "Proceed", "Abort"}, + expected: "Abort", + wantErr: false, + }, + { + name: "mixed case false", + args: []interface{}{"False", "Proceed", "Abort"}, + expected: "Abort", + wantErr: false, + }, + { + name: "too few args", + args: []interface{}{"true", "Proceed"}, + expected: nil, + wantErr: true, + }, + { + name: "too many args", + args: []interface{}{"true", "Proceed", "Abort", "Extra"}, + expected: nil, + wantErr: true, + }, + } + + selectPrim := &Select{} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := selectPrim.Execute(tt.args) + if (err != nil) != tt.wantErr { + t.Errorf("Select.Execute() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != tt.expected { + t.Errorf("Select.Execute() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestSelectName(t *testing.T) { + selectPrim := &Select{} + expected := "/gnd/select" + if got := selectPrim.Name(); got != expected { + t.Errorf("Select.Name() = %v, want %v", got, expected) + } +}