Examples 04 — Weather-Aware Outfit Advisor
Example 04

Weather-Aware Outfit Advisor

Fetches live weather from wttr.in (or an offline fixture), uses AI to parse raw number strings, classify conditions into multiple labels, then computes a deterministic temperature band and boolean wet/windy flags. All signals are packed into a single description and passed to an AI for an outfit recommendation. An orthogonal probe appends an extreme-weather warning when conditions are unusual.

What this example teaches

  • AIParseNumberOp for extracting floats from raw string fields — handles "10", "10°C", "10.0" and any formatted variant without special-casing
  • AIClassifyMultiLabelOp for tagging a description with zero or more labels (rain, snow, fog, sun, cloud, storm) — one call, multiple outputs
  • Deterministic threshold flags: IfFloatGtOp compares the parsed float against a constant wire, producing a boolean without AI
  • A temperature band pattern using three predicate-gated ContextValOp vertices merged by CoalesceNStringOp — exactly one string fires
  • A custom packing op (PackOutfitInputsOp) that assembles five input wires into a formatted summary string, decoupling data extraction from prompt construction
  • The orthogonal probe pattern: an independent AIBoolOp check that appends a warning suffix without blocking the main advice path
View source on GitHub
The workflow

Pipeline structure

AI handles the parts the API returns as opaque strings; deterministic ops compute the flags and bands; a custom packing op bundles everything before the final AI call.

HTTPGetOp
wttr.in JSON
JSONExtractOp ×4
string fields
AIParseNumber ×3
AIClassifyMultiLabel
band + flags
det thresholds
PackOutfitOp
assemble prompt
AIComputeOp
+ probe → concat
det op

Deterministic steps

HTTPGetOp + JSONExtractOp (×4)

Fetches the wttr.in j1 format and pulls the four relevant string fields with dot-path selectors. All four extractions run in parallel once the HTTP body arrives.

ContextValOp (cold/mild/hot) + predicate gates + CoalesceNStringOp

Three ContextValOp vertices each read a band label from context and carry a numeric predicate. Exactly one fires; CoalesceNStringOp collapses the three wires into one band wire. No AI — just threshold comparisons on the parsed temp.

IfFloatGtOp (×2) — wet, windy

Compares precip_mm > 0.1 and wind_kph > 25 against constant wires. Returns a bool wire. No AI — the thresholds are your policy, expressed as data.

PackOutfitInputsOp

Custom op that accepts five input wires (band, wet, windy, temp_c, conditions) and formats them into: "Temperature: 10.0°C (mild), precipitation: dry, wind: calm, conditions: cloud". Keeps prompt construction out of the AI op config.

SelectStringOp + StringConcatOp

If the unusual-weather probe is true, selects a warning suffix; otherwise selects an empty string. Appends to the outfit advice. Same optional-suffix pattern as example 03.

AI op

AI steps

AIParseNumberOp (×3) — temp, precip, wind

The wttr.in API returns numbers as strings in the JSON (e.g. "10"). AIParseNumberOp converts each to a float64, handling any unit suffix or format variation. All three run concurrently.

AIClassifyMultiLabelOp — conditions

Classifies the weather description string into zero or more of: rain, snow, fog, sun, cloud, storm. Returns []string. One AI call; multiple labels can be present simultaneously.

AIComputeStringToStringOp — outfit advice

Reads the packed description string and writes exactly two sentences recommending specific clothing items. Runs after all deterministic signals are assembled.

AIBoolOp — unusual_flag (orthogonal probe)

Independently asks "is this weather unusual or extreme?" of the raw description string. Runs in parallel with the outfit advice op — neither blocks the other. Its output feeds the warning selection, not the main advice path.

Implementation

Walkthrough

1. JSON extraction + parallel number parsing

The wttr.in response stores numeric fields as JSON strings. JSONExtractOp pulls them out as Go strings; AIParseNumberOp converts each to a float64. The four extractions share the body wire and run concurrently, as do the three number parsers.

main.go — Stages 2 & 3
// Stage 2: extract four string fields in parallel.
for _, f := range []struct{ path, wire string }{
    {"current_condition.0.temp_C",        "temp_str"},
    {"current_condition.0.precipMM",      "precip_str"},
    {"current_condition.0.windspeedKmph", "wind_str"},
    {"current_condition.0.weatherDesc.0.value", "desc_str"},
} {
    b.Vertex("extract_"+f.wire).Op("JSONExtractOp").
        Input("JSON", "body").
        Input("Path", f.wire+"_path").
        Output("Value", f.wire)
}

// Stage 3: three AIParseNumberOps run concurrently.
b.Vertex("parse_temp").Op("AIParseNumberOp").
    Params(map[string]string{"operation": "extract temperature as plain number"}).
    Input("Input", "temp_str").
    Output("Result", "temp_c").

2. Deterministic temperature band with gated constants

Three ContextValOp vertices each read a band label from context and carry a numeric predicate. The predicates compare temp_c against thresholds. Exactly one fires per execution; CoalesceNStringOp merges the three wires into a single band string. Zero AI tokens for this categorisation.

main.go — Stage 4 (temperature band)
for _, band := range []struct{ name, cond string }{
    {"cold", "temp_is_cold"},  // temp < 10°C
    {"mild", "temp_is_mild"},  // 10 ≤ temp < 22°C
    {"hot",  "temp_is_hot"},   // temp ≥ 22°C
} {
    // Registered: builtin.ContextValFactory[string](coldBandKey{} / mildBandKey{} / hotBandKey{})
    // Injected:   ctx = context.WithValue(ctx, coldBandKey{}, "cold") etc.
    b.Vertex(band.name+"_const").Op(band.name+"_const").
        Condition(band.cond).
        ConditionInput("temp_c").
        Output("Result", band.name+"_band")
}
b.Vertex("band_merge").Op("CoalesceNStringOp").
    Params(map[string]int{"n": 3}).
    Merge(config.MergeCoalesce).
    Input("Input0", "cold_band").
    Input("Input1", "mild_band").
    Input("Input2", "hot_band").
    Output("Result", "band")

3. Threshold-based boolean flags

IfFloatGtOp compares two float64 wires and emits a boolean. The thresholds (0.1 mm precip, 25 km/h wind) are constant wires — they can be changed by editing the graph, not the library code.

main.go — Stage 5 (wet / windy flags)
b.Vertex("precip_thresh").Op("ConstFloat64Op").
    Params(map[string]string{"Value": "0.1"}).
    Output("Result", "precip_thresh")
b.Vertex("wet_check").Op("IfFloatGtOp").
    Input("A", "precip_mm").
    Input("B", "precip_thresh").
    Output("Match", "wet")

b.Vertex("wind_thresh").Op("ConstFloat64Op").
    Params(map[string]string{"Value": "25.0"}).
    Output("Result", "wind_thresh")
b.Vertex("windy_check").Op("IfFloatGtOp").
    Input("A", "wind_kph").
    Input("B", "wind_thresh").
    Output("Match", "windy")

4. The orthogonal probe and warning suffix

The unusual-weather probe reads from desc_str — the same raw description string used earlier — without depending on any of the downstream signal wires. This means it can run in parallel with the number parsers. Its boolean output feeds a SelectStringOp that chooses between a warning string and an empty string; StringConcatOp appends the result to the outfit advice.

main.go — Stage 9 (orthogonal probe)
// Runs in parallel with parse_temp/parse_precip/parse_wind and classify.
b.Vertex("unusual_check").Op("AIBoolOp").
    Params(map[string]string{"predicate": "Is the described weather unusual or extreme?"}).
    Input("Input", "desc_str").
    Output("Result", "unusual_flag")

b.Vertex("warning_select").Op("SelectStringOp").
    Input("Cond", "unusual_flag").
    Input("IfTrue", "warning_str").  // "  ⚠ unusual weather"
    Input("IfFalse", "empty_str").
    Output("Result", "warning_suffix")

b.Vertex("final_concat").Op("StringConcatOp").
    Input("A", "outfit_advice").
    Input("B", "warning_suffix").
    Output("Result", "final_advice")

Run it

Three fixtures cover all three temperature bands — London (mild), Reykjavik (cold, likely unusual), Singapore (hot).

shell
export CLAUDE_API_KEY=<your key>

# Live API
go run ./examples/weather-advisor --city London
go run ./examples/weather-advisor --city Reykjavik
go run ./examples/weather-advisor --city Singapore

# Offline fixtures
go run ./examples/weather-advisor --fixture examples/weather-advisor/testdata/weather/london.json
go run ./examples/weather-advisor --fixture examples/weather-advisor/testdata/weather/reykjavik.json

Run it as an MCP server

The same binary is dual-mode. Pass --mcp and instead of running once and exiting it speaks the Model Context Protocol over stdin/stdout, exposing the entire workflow as a single MCP tool, advise_outfit. The Go SDK derives the tool's input schema from the workflow's UserInput struct and validates every tools/call request against it, so an agent runs the whole fetch → bands/flags → classify → recommend pipeline with one tool call.

shell
# Speak MCP over stdin/stdout instead of running once
go run ./examples/weather-advisor --mcp

Register it with any MCP client by pointing the client at that command:

.mcp.json
{
  "mcpServers": {
    "weather-advisor": {
      "command": "go",
      "args": ["run", "./examples/weather-advisor", "--mcp"]
    }
  }
}

Token usage

Measured on testdata/weather/london.json. The weather payload is tiny, making this the lowest-cost example. Four AIComputeOp calls cover the three number parsers and the final outfit writer.

Op Calls Input tokens Output tokens Total tokens
AIBoolOp 1 61 4 65
AIClassifyMultiLabelOp 1 84 4 88
AIComputeOp 4 303 77 380
Total 6 448 85 533
← Example 03 Next: HN Topic Brief →