diff --git a/ai/examples/stock_trading.go b/ai/examples/stock_trading.go index 1bafa9d1..0da6dd06 100644 --- a/ai/examples/stock_trading.go +++ b/ai/examples/stock_trading.go @@ -42,12 +42,14 @@ Components: Interface: "modular.Module", GoCode: `package module +// [DEMO STUB] Replace this entire file with a real stock API integration. +// The CheckPrice method below returns synthetic demo data. +// In production, call a real API (e.g. Alpha Vantage, Polygon.io) and +// parse the JSON response to get live prices. + import ( "context" - "encoding/json" - "fmt" "math" - "net/http" "sync" "github.com/GoCodeAlone/modular" @@ -81,14 +83,22 @@ func (s *StockPriceChecker) Init(app modular.Application) error { } func (s *StockPriceChecker) CheckPrice(ctx context.Context) (currentPrice float64, pctChange float64, err error) { - // Placeholder: In production, call real stock API - s.mu.RLock() - defer s.mu.RUnlock() + // [DEMO STUB] This is example/demonstration code only. + // In production, replace this method body with a real stock API call, + // e.g. Alpha Vantage: GET /query?function=GLOBAL_QUOTE&symbol=AAPL&apikey=YOUR_KEY + // For now, synthetic demo prices are returned so the workflow exercises the full decision path. + s.mu.Lock() + defer s.mu.Unlock() if s.openPrice == 0 { - return 0, 0, fmt.Errorf("opening price not set") + // Seed with a realistic demo opening price so callers always get usable data. + s.openPrice = 182.50 // [DEMO] hardcoded AAPL-like opening price + s.lastPrice = 182.50 } + // Simulate a small price tick so repeated calls show movement. + s.lastPrice += 0.10 // [DEMO] synthetic +0.10 tick per call + pctChange = ((s.lastPrice - s.openPrice) / s.openPrice) * 100 return s.lastPrice, pctChange, nil } @@ -175,7 +185,8 @@ func (t *TradeExecutor) Execute(ctx context.Context, order TradeOrder) (*TradeRe t.logger.Info(fmt.Sprintf("Executing %s order for %s: qty=%d price=%.2f", order.Action, order.Symbol, order.Quantity, order.Price)) - // Placeholder: In production, call brokerage API + // [DEMO STUB] Returns a simulated order confirmation. + // In production, replace this with a real brokerage API call (e.g. Alpaca, TD Ameritrade). return &TradeResult{ OrderID: fmt.Sprintf("ORD-%d", time.Now().UnixNano()), Status: "executed", diff --git a/handlers/integration.go b/handlers/integration.go index 034a6332..ec8d0dd3 100644 --- a/handlers/integration.go +++ b/handlers/integration.go @@ -354,7 +354,8 @@ func ensureConnectorConnected(ctx context.Context, connector module.IntegrationC } // resolveParamValue resolves a single input value, substituting step result references where applicable. -// References use the ${varName} syntax. If the variable is not found, the original value is returned. +// References use the ${varName} syntax. Dot-notation is supported: ${step1.value} looks up results["step1"] +// and then retrieves the "value" key from the resulting map. If the variable is not found, the original value is returned. func resolveParamValue(v any, results map[string]any) any { strVal, ok := v.(string) if !ok || len(strVal) <= 3 || strVal[0:2] != "${" || strVal[len(strVal)-1] != '}' { @@ -362,9 +363,26 @@ func resolveParamValue(v any, results map[string]any) any { } // Extract the variable name, e.g., ${step1.value} -> step1.value varName := strVal[2 : len(strVal)-1] + // Fast path: exact match in results if result, found := results[varName]; found { return result } + // Dot-notation path: split on "." and traverse nested maps + parts := strings.SplitN(varName, ".", 2) + if len(parts) != 2 { + return v + } + stepResult, found := results[parts[0]] + if !found { + return v + } + nested, ok := stepResult.(map[string]any) + if !ok { + return v + } + if val, found := nested[parts[1]]; found { + return val + } return v } diff --git a/handlers/integration_handler_test.go b/handlers/integration_handler_test.go index 58d7b69c..3f64c74d 100644 --- a/handlers/integration_handler_test.go +++ b/handlers/integration_handler_test.go @@ -489,3 +489,63 @@ func TestExecuteStepWithRetry_ExhaustedRetries(t *testing.T) { t.Fatal("expected error after exhausted retries") } } + +func TestResolveParamValue_PlainValue(t *testing.T) { + results := map[string]any{"step1": map[string]any{"value": "hello"}} + got := resolveParamValue(42, results) + if got != 42 { + t.Errorf("expected 42, got %v", got) + } +} + +func TestResolveParamValue_ExactMatch(t *testing.T) { + results := map[string]any{"step1": "direct"} + got := resolveParamValue("${step1}", results) + if got != "direct" { + t.Errorf("expected 'direct', got %v", got) + } +} + +func TestResolveParamValue_DotNotation(t *testing.T) { + results := map[string]any{ + "step1": map[string]any{"value": "resolved"}, + } + got := resolveParamValue("${step1.value}", results) + if got != "resolved" { + t.Errorf("expected 'resolved', got %v", got) + } +} + +func TestResolveParamValue_DotNotation_MissingKey(t *testing.T) { + results := map[string]any{ + "step1": map[string]any{"other": "x"}, + } + got := resolveParamValue("${step1.value}", results) + if got != "${step1.value}" { + t.Errorf("expected original string, got %v", got) + } +} + +func TestResolveParamValue_DotNotation_NonMapResult(t *testing.T) { + results := map[string]any{"step1": "not-a-map"} + got := resolveParamValue("${step1.value}", results) + if got != "${step1.value}" { + t.Errorf("expected original string when step result is not a map, got %v", got) + } +} + +func TestResolveParamValue_DotNotation_MissingStep(t *testing.T) { + results := map[string]any{} + got := resolveParamValue("${step1.value}", results) + if got != "${step1.value}" { + t.Errorf("expected original string when step not found, got %v", got) + } +} + +func TestResolveParamValue_NotAReference(t *testing.T) { + results := map[string]any{"foo": "bar"} + got := resolveParamValue("just-a-string", results) + if got != "just-a-string" { + t.Errorf("expected 'just-a-string', got %v", got) + } +}