Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions ai/examples/stock_trading.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment on lines 50 to 53
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The net/http import was removed, but the code still references http.Client on lines 65 and 73 within the embedded GoCode string. This will cause a compilation error when the generated code is used. Either keep the net/http import or remove the httpClient field from the struct.

Copilot uses AI. Check for mistakes.

"github.com/GoCodeAlone/modular"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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",
Expand Down
20 changes: 19 additions & 1 deletion handlers/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,17 +354,35 @@ 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] != '}' {
return v
}
// 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
}
Comment on lines 356 to 387
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR includes significant unrelated changes to the integration workflow handler (adding dot-notation support for parameter resolution) that are not mentioned in the PR description. The PR description states it "fixes stock trading stub with demo data" (issue #73) but these integration changes appear to be a separate feature. Consider splitting this into two PRs: one for the stock trading fix and one for the dot-notation feature.

Copilot uses AI. Check for mistakes.
Comment on lines +370 to 387
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dot-notation implementation only supports single-level nesting (e.g., ${step1.value}) but doesn't handle deeper nesting (e.g., ${step1.nested.deep}). For example, if a reference like ${step1.a.b} is used, it will attempt to look up results["step1"] and then access the key "a.b" (literal string with a dot) rather than traversing step1["a"]["b"]. Consider adding a test case to document this limitation, or extend the implementation to support arbitrary depth using recursive traversal or a loop.

Copilot uses AI. Check for mistakes.

Expand Down
60 changes: 60 additions & 0 deletions handlers/integration_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading