Skip to content

Commit 21cdd42

Browse files
michaelmcneesclaudecoderabbitai[bot]
authored
feat(cel): custom data transformation functions (#14) (#21)
* docs: add spec for data transformation CEL functions (#14) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add implementation plan for data transformation CEL functions (#14) 12-task plan: macro tests, string/type/collection/JSON/time functions, obj() construction, docs updates, and example workflows. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(cel): add coverage for built-in map/filter/exists/all macros * feat(cel): add toLower, toUpper, trim string functions Registers custom string member overloads via cel.Lib and wires them into NewEvaluator through a central customFunctions() registration point. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(cel): add replace and split string functions * feat(cel): add parseInt, parseFloat, toString type coercion functions * feat(cel): add obj() map construction function * feat(cel): add default() null coalescing and flatten() functions * feat(cel): add jsonEncode and jsonDecode functions * feat(cel): add timestamp and formatTimestamp date/time functions * docs: add custom CEL functions and macros to expressions reference (#14) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add data transformation and AI enrichment example workflows (#14) * docs: add data transformation patterns guide (#14) Covers three patterns — CEL-only structural transforms, AI-powered semantic transforms, and hybrid workflows that combine both. Includes a decision guide and a quick-reference table of all available CEL extension functions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address CodeRabbit review — null handling, date formats, docs, tests (#14) - default(): fall through to rhs when lhs is null (types.NullValue) - parseTimestamp(): try RFC3339Nano, bare ISO date, US date, and named-month formats before erroring - expressions.md: remove undeclared `now` variable from two examples - data-transformations.md: add missing exists_one row to list-macros table - data-transform-api-to-db.yaml: simplify to single-user fetch+insert (fixes broken batch param shape) - functions_test.go: add null/fallback tests for default(), edge-case tests for flatten(), and new format tests for parseTimestamp() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address PR #21 review — non-strict default, jsonDecode precision, docs accuracy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR #21 review round 3 — test coverage, trailing JSON, docs accuracy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR #21 review round 4 — EOF trailing check, docs, plan cleanup - jsonDecode: replace dec.More() with dec.Decode(&trailing) EOF check to catch all trailing data (e.g., "{}]", "1}") - Add trailing_bracket and trailing_brace regression tests - Update parseTimestamp description in data-transformations.md to list all accepted formats - Plan: rename TestFunc_Timestamp to TestFunc_ParseTimestamp, add golangci-lint to final validation checklist Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR #21 review round 5 — precision, flatten, plan accuracy - normalizeJSONNumbers: only use float64 for decimals/exponents, preserve overflow integers as strings to avoid silent precision loss - flatten: initialize result as make([]any, 0) so empty lists return non-nil empty slice - Add large_integer_preserved test for jsonDecode - Plan: remove dead split() variable, replace variadic obj() with fixed-arity overloads, add jsonDecode EOF check, fix timestamp refs - data-transformations.md: note obj() 5-pair limit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(plan): correct test selector to TestFunc_ParseTimestamp Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove force-added superpowers docs from tracking These internal process docs (specs, plans) are already in .gitignore but were force-added during development. Files remain on disk but are no longer tracked by git. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 📝 CodeRabbit Chat: Add generated unit tests --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent db71d00 commit 21cdd42

10 files changed

Lines changed: 2095 additions & 1157 deletions

File tree

docs/superpowers/plans/2026-03-24-init-connection-recovery.md

Lines changed: 0 additions & 981 deletions
This file was deleted.

docs/superpowers/specs/2026-03-24-init-connection-recovery-design.md

Lines changed: 0 additions & 174 deletions
This file was deleted.

examples/ai-data-enrichment.yaml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: ai-data-enrichment
2+
description: >
3+
Fetches support tickets, uses an AI model to classify priority and
4+
extract key entities, then stores the enriched data. Demonstrates
5+
using AI for transforms that require interpretation rather than
6+
simple structural mapping.
7+
8+
inputs:
9+
ticket_api_url:
10+
type: string
11+
description: URL to fetch support tickets from
12+
13+
steps:
14+
- name: fetch-tickets
15+
action: http/request
16+
timeout: "15s"
17+
params:
18+
method: GET
19+
url: "{{ inputs.ticket_api_url }}"
20+
headers:
21+
Accept: "application/json"
22+
23+
- name: classify
24+
action: ai/completion
25+
credential: openai
26+
timeout: "60s"
27+
params:
28+
model: gpt-4o
29+
system_prompt: >
30+
You are a support ticket classifier. Given a ticket, determine
31+
the priority (critical, high, medium, low), category, and extract
32+
any mentioned product names or error codes.
33+
prompt: "Classify this ticket: {{ steps['fetch-tickets'].output.body }}"
34+
output_schema:
35+
type: object
36+
properties:
37+
priority:
38+
type: string
39+
enum: [critical, high, medium, low]
40+
category:
41+
type: string
42+
products:
43+
type: array
44+
items:
45+
type: string
46+
error_codes:
47+
type: array
48+
items:
49+
type: string
50+
required: [priority, category, products, error_codes]
51+
additionalProperties: false
52+
53+
- name: store-enriched
54+
action: postgres/query
55+
credential: app-db
56+
if: "steps.classify.output.json.priority == 'critical' || steps.classify.output.json.priority == 'high'"
57+
params:
58+
query: >
59+
INSERT INTO urgent_tickets (priority, category, products, raw_body)
60+
VALUES ($1, $2, $3, $4)
61+
args:
62+
- "{{ steps.classify.output.json.priority }}"
63+
- "{{ steps.classify.output.json.category }}"
64+
- "{{ jsonEncode(steps.classify.output.json.products) }}"
65+
- "{{ steps['fetch-tickets'].output.body }}"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: data-transform-api-to-db
2+
description: >
3+
Fetches a user from an API, transforms the record using CEL expressions
4+
to match a database schema, and inserts the normalized data into Postgres.
5+
Demonstrates toLower() and string functions without requiring an AI model.
6+
7+
steps:
8+
- name: fetch-user
9+
action: http/request
10+
timeout: "15s"
11+
params:
12+
method: GET
13+
url: "https://jsonplaceholder.typicode.com/users/1"
14+
headers:
15+
Accept: "application/json"
16+
17+
- name: store-user
18+
action: postgres/query
19+
credential: app-db
20+
params:
21+
query: "INSERT INTO users (username, email, city) VALUES ($1, $2, $3)"
22+
args:
23+
- "{{ steps['fetch-user'].output.json.username.toLower() }}"
24+
- "{{ steps['fetch-user'].output.json.email.toLower() }}"
25+
- "{{ steps['fetch-user'].output.json.address.city }}"

internal/cel/cel.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ type Evaluator struct {
2828

2929
// NewEvaluator creates a CEL evaluator with the standard Mantle expression environment.
3030
func NewEvaluator() (*Evaluator, error) {
31-
env, err := cel.NewEnv(
31+
opts := []cel.EnvOption{
3232
cel.Variable("steps", cel.MapType(cel.StringType, cel.DynType)),
3333
cel.Variable("inputs", cel.MapType(cel.StringType, cel.DynType)),
3434
cel.Variable("env", cel.MapType(cel.StringType, cel.StringType)),
3535
cel.Variable("trigger", cel.MapType(cel.StringType, cel.DynType)),
36-
)
36+
}
37+
opts = append(opts, customFunctions()...)
38+
39+
env, err := cel.NewEnv(opts...)
3740
if err != nil {
3841
return nil, fmt.Errorf("creating CEL environment: %w", err)
3942
}

0 commit comments

Comments
 (0)