Skip to content

Commit 94b45f0

Browse files
Prompt generation for AI-assisted authoring based on a CEL environment (#1160)
* AI authoring prompt generation
1 parent 535d561 commit 94b45f0

File tree

12 files changed

+880
-12
lines changed

12 files changed

+880
-12
lines changed

cel/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ go_library(
1818
"optimizer.go",
1919
"options.go",
2020
"program.go",
21+
"prompt.go",
2122
"validator.go",
2223
],
24+
embedsrcs = ["//cel/templates"],
2325
importpath = "github.com/google/cel-go/cel",
2426
visibility = ["//visibility:public"],
2527
deps = [
@@ -65,6 +67,7 @@ go_test(
6567
"io_test.go",
6668
"inlining_test.go",
6769
"optimizer_test.go",
70+
"prompt_test.go",
6871
"validator_test.go",
6972
],
7073
data = [
@@ -73,6 +76,9 @@ go_test(
7376
embed = [
7477
":go_default_library",
7578
],
79+
embedsrcs = [
80+
"//cel/testdata:prompts",
81+
],
7682
deps = [
7783
"//common/operators:go_default_library",
7884
"//common/overloads:go_default_library",

cel/decls.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,14 @@ func Variable(name string, t *Type) EnvOption {
148148
}
149149
}
150150

151+
// VariableWithDoc creates an instance of a variable declaration with a variable name, type, and doc string.
152+
func VariableWithDoc(name string, t *Type, doc string) EnvOption {
153+
return func(e *Env) (*Env, error) {
154+
e.variables = append(e.variables, decls.NewVariableWithDoc(name, t, doc))
155+
return e, nil
156+
}
157+
}
158+
151159
// VariableDecls configures a set of fully defined cel.VariableDecl instances in the environment.
152160
func VariableDecls(vars ...*decls.VariableDecl) EnvOption {
153161
return func(e *Env) (*Env, error) {
@@ -239,6 +247,13 @@ func FunctionDecls(funcs ...*decls.FunctionDecl) EnvOption {
239247
// FunctionOpt defines a functional option for configuring a function declaration.
240248
type FunctionOpt = decls.FunctionOpt
241249

250+
// FunctionDocs provides a general usage documentation for the function.
251+
//
252+
// Use OverloadExamples to provide example usage instructions for specific overloads.
253+
func FunctionDocs(docs ...string) FunctionOpt {
254+
return decls.FunctionDocs(docs...)
255+
}
256+
242257
// SingletonUnaryBinding creates a singleton function definition to be used for all function overloads.
243258
//
244259
// Note, this approach works well if operand is expected to have a specific trait which it implements,
@@ -312,6 +327,11 @@ func MemberOverload(overloadID string, args []*Type, resultType *Type, opts ...O
312327
// OverloadOpt is a functional option for configuring a function overload.
313328
type OverloadOpt = decls.OverloadOpt
314329

330+
// OverloadExamples configures an example of how to invoke the overload.
331+
func OverloadExamples(docs ...string) OverloadOpt {
332+
return decls.OverloadExamples(docs...)
333+
}
334+
315335
// UnaryBinding provides the implementation of a unary overload. The provided function is protected by a runtime
316336
// type-guard which ensures runtime type agreement between the overload signature and runtime argument types.
317337
func UnaryBinding(binding functions.UnaryOp) OverloadOpt {

cel/env.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -553,14 +553,27 @@ func (e *Env) HasFunction(functionName string) bool {
553553
return ok
554554
}
555555

556-
// Functions returns map of Functions, keyed by function name, that have been configured in the environment.
556+
// Functions returns a shallow copy of the Functions, keyed by function name, that have been configured in the environment.
557557
func (e *Env) Functions() map[string]*decls.FunctionDecl {
558-
return e.functions
558+
shallowCopy := make(map[string]*decls.FunctionDecl, len(e.functions))
559+
for nm, fn := range e.functions {
560+
shallowCopy[nm] = fn
561+
}
562+
return shallowCopy
559563
}
560564

561-
// Variables returns the set of variables associated with the environment.
565+
// Variables returns a shallow copy of the variables associated with the environment.
562566
func (e *Env) Variables() []*decls.VariableDecl {
563-
return e.variables[:]
567+
shallowCopy := make([]*decls.VariableDecl, len(e.variables))
568+
copy(shallowCopy, e.variables)
569+
return shallowCopy
570+
}
571+
572+
// Macros returns a shallow copy of macros associated with the environment.
573+
func (e *Env) Macros() []Macro {
574+
shallowCopy := make([]Macro, len(e.macros))
575+
copy(shallowCopy, e.macros)
576+
return shallowCopy
564577
}
565578

566579
// HasValidator returns whether a specific ASTValidator has been configured in the environment.

cel/macro.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,24 +142,38 @@ type MacroExprHelper interface {
142142
NewError(exprID int64, message string) *Error
143143
}
144144

145+
// MacroOpt defines a functional option for configuring macro behavior.
146+
type MacroOpt = parser.MacroOpt
147+
148+
// MacroDocs configures a list of strings into a multiline description for the macro.
149+
func MacroDocs(docs ...string) MacroOpt {
150+
return parser.MacroDocs(docs...)
151+
}
152+
153+
// MacroExamples configures a list of examples, either as a string or common.MultilineString,
154+
// into an example set to be provided with the macro Documentation() call.
155+
func MacroExamples(examples ...string) MacroOpt {
156+
return parser.MacroExamples(examples...)
157+
}
158+
145159
// GlobalMacro creates a Macro for a global function with the specified arg count.
146-
func GlobalMacro(function string, argCount int, factory MacroFactory) Macro {
147-
return parser.NewGlobalMacro(function, argCount, factory)
160+
func GlobalMacro(function string, argCount int, factory MacroFactory, opts ...MacroOpt) Macro {
161+
return parser.NewGlobalMacro(function, argCount, factory, opts...)
148162
}
149163

150164
// ReceiverMacro creates a Macro for a receiver function matching the specified arg count.
151-
func ReceiverMacro(function string, argCount int, factory MacroFactory) Macro {
152-
return parser.NewReceiverMacro(function, argCount, factory)
165+
func ReceiverMacro(function string, argCount int, factory MacroFactory, opts ...MacroOpt) Macro {
166+
return parser.NewReceiverMacro(function, argCount, factory, opts...)
153167
}
154168

155169
// GlobalVarArgMacro creates a Macro for a global function with a variable arg count.
156-
func GlobalVarArgMacro(function string, factory MacroFactory) Macro {
157-
return parser.NewGlobalVarArgMacro(function, factory)
170+
func GlobalVarArgMacro(function string, factory MacroFactory, opts ...MacroOpt) Macro {
171+
return parser.NewGlobalVarArgMacro(function, factory, opts...)
158172
}
159173

160174
// ReceiverVarArgMacro creates a Macro for a receiver function matching a variable arg count.
161-
func ReceiverVarArgMacro(function string, factory MacroFactory) Macro {
162-
return parser.NewReceiverVarArgMacro(function, factory)
175+
func ReceiverVarArgMacro(function string, factory MacroFactory, opts ...MacroOpt) Macro {
176+
return parser.NewReceiverVarArgMacro(function, factory, opts...)
163177
}
164178

165179
// NewGlobalMacro creates a Macro for a global function with the specified arg count.

cel/prompt.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cel
16+
17+
import (
18+
_ "embed"
19+
"sort"
20+
"strings"
21+
"text/template"
22+
23+
"github.com/google/cel-go/common"
24+
"github.com/google/cel-go/common/operators"
25+
"github.com/google/cel-go/common/overloads"
26+
)
27+
28+
//go:embed templates/authoring.tmpl
29+
var authoringPrompt string
30+
31+
// AuthoringPrompt creates a prompt template from a CEL environment for the purpose of AI-assisted authoring.
32+
func AuthoringPrompt(env *Env) (*Prompt, error) {
33+
funcMap := template.FuncMap{
34+
"split": func(str string) []string { return strings.Split(str, "\n") },
35+
}
36+
tmpl := template.New("cel").Funcs(funcMap)
37+
tmpl, err := tmpl.Parse(authoringPrompt)
38+
if err != nil {
39+
return nil, err
40+
}
41+
return &Prompt{
42+
Persona: defaultPersona,
43+
FormatRules: defaultFormatRules,
44+
GeneralUsage: defaultGeneralUsage,
45+
tmpl: tmpl,
46+
env: env,
47+
}, nil
48+
}
49+
50+
// Prompt represents the core components of an LLM prompt based on a CEL environment.
51+
//
52+
// All fields of the prompt may be overwritten / modified with support for rendering the
53+
// prompt to a human-readable string.
54+
type Prompt struct {
55+
// Persona indicates something about the kind of user making the request
56+
Persona string
57+
58+
// FormatRules indicate how the LLM should generate its output
59+
FormatRules string
60+
61+
// GeneralUsage specifies additional context on how CEL should be used.
62+
GeneralUsage string
63+
64+
// tmpl is the text template base-configuration for rendering text.
65+
tmpl *template.Template
66+
67+
// env reference used to collect variables, functions, and macros available to the prompt.
68+
env *Env
69+
}
70+
71+
type promptInst struct {
72+
*Prompt
73+
74+
Variables []*common.Doc
75+
Macros []*common.Doc
76+
Functions []*common.Doc
77+
UserPrompt string
78+
}
79+
80+
// Render renders the user prompt with the associated context from the prompt template
81+
// for use with LLM generators.
82+
func (p *Prompt) Render(userPrompt string) string {
83+
var buffer strings.Builder
84+
vars := make([]*common.Doc, len(p.env.Variables()))
85+
for i, v := range p.env.Variables() {
86+
vars[i] = v.Documentation()
87+
}
88+
sort.SliceStable(vars, func(i, j int) bool {
89+
return vars[i].Name < vars[j].Name
90+
})
91+
macs := make([]*common.Doc, len(p.env.Macros()))
92+
for i, m := range p.env.Macros() {
93+
macs[i] = m.(common.Documentor).Documentation()
94+
}
95+
funcs := make([]*common.Doc, 0, len(p.env.Functions()))
96+
for _, f := range p.env.Functions() {
97+
if _, hidden := hiddenFunctions[f.Name()]; hidden {
98+
continue
99+
}
100+
funcs = append(funcs, f.Documentation())
101+
}
102+
sort.SliceStable(funcs, func(i, j int) bool {
103+
return funcs[i].Name < funcs[j].Name
104+
})
105+
inst := &promptInst{
106+
Prompt: p,
107+
Variables: vars,
108+
Macros: macs,
109+
Functions: funcs,
110+
UserPrompt: userPrompt}
111+
p.tmpl.Execute(&buffer, inst)
112+
return buffer.String()
113+
}
114+
115+
const (
116+
defaultPersona = `You are a software engineer with expertise in networking and application security
117+
authoring boolean Common Expression Language (CEL) expressions to ensure firewall,
118+
networking, authentication, and data access is only permitted when all conditions
119+
are satisified.`
120+
121+
defaultFormatRules = `Output your response as a CEL expression.
122+
123+
Write the expression with the comment on the first line and the expression on the
124+
subsequent lines. Format the expression using 80-character line limits commonly
125+
found in C++ or Java code.`
126+
127+
defaultGeneralUsage = `CEL supports Protocol Buffer and JSON types, as well as simple types and aggregate types.
128+
129+
Simple types include bool, bytes, double, int, string, and uint:
130+
131+
* double literals must always include a decimal point: 1.0, 3.5, -2.2
132+
* uint literals must be positive values suffixed with a 'u': 42u
133+
* byte literals are strings prefixed with a 'b': b'1235'
134+
* string literals can use either single quotes or double quotes: 'hello', "world"
135+
* string literals can also be treated as raw strings that do not require any
136+
escaping within the string by using the 'R' prefix: R"""quote: "hi" """
137+
138+
Aggregate types include list and map:
139+
140+
* list literals consist of zero or more values between brackets: "['a', 'b', 'c']"
141+
* map literal consist of colon-separated key-value pairs within braces: "{'key1': 1, 'key2': 2}"
142+
* Only int, uint, string, and bool types are valid map keys.
143+
* Maps containing HTTP headers must always use lower-cased string keys.
144+
145+
Comments start with two-forward slashes followed by text and a newline.`
146+
)
147+
148+
var (
149+
hiddenFunctions = map[string]bool{
150+
overloads.DeprecatedIn: true,
151+
operators.OldIn: true,
152+
operators.OldNotStrictlyFalse: true,
153+
operators.NotStrictlyFalse: true,
154+
}
155+
)

cel/prompt_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cel
16+
17+
import (
18+
_ "embed"
19+
"testing"
20+
21+
"github.com/google/cel-go/common/env"
22+
"github.com/google/cel-go/test"
23+
)
24+
25+
//go:embed testdata/basic.prompt.md
26+
var wantBasicPrompt string
27+
28+
//go:embed testdata/macros.prompt.md
29+
var wantMacrosPrompt string
30+
31+
//go:embed testdata/standard_env.prompt.md
32+
var wantStandardEnvPrompt string
33+
34+
func TestPromptTemplate(t *testing.T) {
35+
tests := []struct {
36+
name string
37+
envOpts []EnvOption
38+
out string
39+
}{
40+
{
41+
name: "basic",
42+
out: wantBasicPrompt,
43+
},
44+
{
45+
name: "macros",
46+
envOpts: []EnvOption{Macros(StandardMacros...)},
47+
out: wantMacrosPrompt,
48+
},
49+
{
50+
name: "standard_env",
51+
envOpts: []EnvOption{StdLib(StdLibSubset(env.NewLibrarySubset().SetDisableMacros(true)))},
52+
out: wantStandardEnvPrompt,
53+
},
54+
}
55+
56+
for _, tst := range tests {
57+
tc := tst
58+
t.Run(tc.name, func(t *testing.T) {
59+
env, err := NewCustomEnv(tc.envOpts...)
60+
if err != nil {
61+
t.Fatalf("cel.NewCustomEnv() failed: %v", err)
62+
}
63+
prompt, err := AuthoringPrompt(env)
64+
if err != nil {
65+
t.Fatalf("cel.AuthoringPrompt() failed: %v", err)
66+
}
67+
out := prompt.Render("<USER_PROMPT>")
68+
if !test.Compare(out, tc.out) {
69+
t.Errorf("got %s, wanted %s", out, tc.out)
70+
}
71+
})
72+
}
73+
}

cel/templates/BUILD.bazel

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
licenses(["notice"]) # Apache 2.0
2+
3+
filegroup(
4+
name = "templates",
5+
srcs = glob(["*.tmpl"]),
6+
visibility = ["//visibility:public"],
7+
)

0 commit comments

Comments
 (0)