| // Copyright 2025 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package cel |
| |
| import ( |
| _ "embed" |
| "sort" |
| "strings" |
| "text/template" |
| |
| "github.com/google/cel-go/common" |
| "github.com/google/cel-go/common/operators" |
| "github.com/google/cel-go/common/overloads" |
| "github.com/google/cel-go/common/types" |
| ) |
| |
| //go:embed templates/authoring.tmpl |
| var authoringPrompt string |
| |
| // splitImpl splits a string into a list of strings. |
| // |
| // Normalizes extracted comments (trim common prefix whitespace and extra trailing newlines). |
| func splitImpl(str string) []string { |
| str = strings.TrimRight(str, " \n\t\r") |
| out := strings.Split(str, "\n") |
| if len(out) == 0 { |
| return nil |
| } |
| negative := strings.TrimLeft(out[0], " \t") |
| lenNegative := len(negative) |
| lenOut := len(out[0]) |
| if lenNegative == lenOut { |
| return out |
| } |
| prefix := out[0][:lenOut-lenNegative] |
| trimmed := make([]string, len(out)) |
| for i, line := range out { |
| if line == "" { |
| trimmed[i] = "" |
| continue |
| } |
| if !strings.HasPrefix(line, prefix) { |
| return out |
| } |
| trimmed[i] = strings.TrimPrefix(line, prefix) |
| } |
| |
| return trimmed |
| } |
| |
| // AuthoringPrompt creates a prompt template from a CEL environment for the purpose of AI-assisted authoring. |
| func AuthoringPrompt(env *Env) (*Prompt, error) { |
| funcMap := template.FuncMap{ |
| "split": splitImpl, |
| "newlineToSpace": func(str string) string { return strings.ReplaceAll(str, "\n", " ") }, |
| } |
| tmpl := template.New("cel").Funcs(funcMap) |
| tmpl, err := tmpl.Parse(authoringPrompt) |
| if err != nil { |
| return nil, err |
| } |
| return &Prompt{ |
| Persona: defaultPersona, |
| FormatRules: defaultFormatRules, |
| GeneralUsage: defaultGeneralUsage, |
| tmpl: tmpl, |
| env: env, |
| }, nil |
| } |
| |
| // AuthoringPromptWithFieldPaths creates a prompt template from a CEL environment for the purpose of AI-assisted authoring. |
| // Includes documentation for all of the reachable field paths in the environment. |
| func AuthoringPromptWithFieldPaths(env *Env) (*Prompt, error) { |
| p, err := AuthoringPrompt(env) |
| if err != nil { |
| return nil, err |
| } |
| p.fieldPaths = true |
| return p, nil |
| } |
| |
| // Prompt represents the core components of an LLM prompt based on a CEL environment. |
| // |
| // All fields of the prompt may be overwritten / modified with support for rendering the |
| // prompt to a human-readable string. |
| type Prompt struct { |
| // Persona indicates something about the kind of user making the request |
| Persona string |
| |
| // FormatRules indicate how the LLM should generate its output |
| FormatRules string |
| |
| // GeneralUsage specifies additional context on how CEL should be used. |
| GeneralUsage string |
| |
| // tmpl is the text template base-configuration for rendering text. |
| tmpl *template.Template |
| |
| // fieldPaths is a flag to enable including reachable field paths in the prompt. |
| fieldPaths bool |
| |
| // env reference used to collect variables, functions, and macros available to the prompt. |
| env *Env |
| } |
| |
| type promptVariable struct { |
| *common.Doc |
| FieldPaths []*common.Doc |
| } |
| |
| type promptInst struct { |
| *Prompt |
| |
| Variables []*promptVariable |
| Macros []*common.Doc |
| Functions []*common.Doc |
| UserPrompt string |
| } |
| |
| // Render renders the user prompt with the associated context from the prompt template |
| // for use with LLM generators. |
| func (p *Prompt) Render(userPrompt string) string { |
| var buffer strings.Builder |
| vars := make([]*promptVariable, len(p.env.Variables())) |
| for i, v := range p.env.Variables() { |
| vars[i] = &promptVariable{Doc: v.Documentation()} |
| if p.fieldPaths && v.Type().Kind() == types.StructKind { |
| var fieldPaths []*common.Doc |
| |
| paths := fieldPathsForType(p.env.CELTypeProvider(), v.Name(), v.Type()) |
| if len(paths) < 2 { |
| paths = nil |
| } else { |
| // First path is the variable which is already documented. |
| paths = paths[1:] |
| } |
| for _, path := range paths { |
| fieldPaths = append(fieldPaths, path.Documentation()) |
| } |
| |
| sort.SliceStable(fieldPaths, func(i, j int) bool { |
| return fieldPaths[i].Name < fieldPaths[j].Name |
| }) |
| vars[i].FieldPaths = fieldPaths |
| } |
| } |
| sort.SliceStable(vars, func(i, j int) bool { |
| return vars[i].Name < vars[j].Name |
| }) |
| macs := make([]*common.Doc, len(p.env.Macros())) |
| for i, m := range p.env.Macros() { |
| macs[i] = m.(common.Documentor).Documentation() |
| } |
| funcs := make([]*common.Doc, 0, len(p.env.Functions())) |
| for _, f := range p.env.Functions() { |
| if _, hidden := hiddenFunctions[f.Name()]; hidden { |
| continue |
| } |
| funcs = append(funcs, f.Documentation()) |
| } |
| sort.SliceStable(funcs, func(i, j int) bool { |
| return funcs[i].Name < funcs[j].Name |
| }) |
| inst := &promptInst{ |
| Prompt: p, |
| Variables: vars, |
| Macros: macs, |
| Functions: funcs, |
| UserPrompt: userPrompt} |
| p.tmpl.Execute(&buffer, inst) |
| return buffer.String() |
| } |
| |
| const ( |
| defaultPersona = `You are a software engineer with expertise in networking and application security |
| authoring boolean Common Expression Language (CEL) expressions to ensure firewall, |
| networking, authentication, and data access is only permitted when all conditions |
| are satisfied.` |
| |
| defaultFormatRules = `Output your response as a CEL expression. |
| |
| Write the expression with the comment on the first line and the expression on the |
| subsequent lines. Format the expression using 80-character line limits commonly |
| found in C++ or Java code.` |
| |
| defaultGeneralUsage = `CEL supports Protocol Buffer and JSON types, as well as simple types and aggregate types. |
| |
| Simple types include bool, bytes, double, int, string, and uint: |
| |
| * double literals must always include a decimal point: 1.0, 3.5, -2.2 |
| * uint literals must be positive values suffixed with a 'u': 42u |
| * byte literals are strings prefixed with a 'b': b'1235' |
| * string literals can use either single quotes or double quotes: 'hello', "world" |
| * string literals can also be treated as raw strings that do not require any |
| escaping within the string by using the 'R' prefix: R"""quote: "hi" """ |
| |
| Aggregate types include list and map: |
| |
| * list literals consist of zero or more values between brackets: "['a', 'b', 'c']" |
| * map literal consist of colon-separated key-value pairs within braces: "{'key1': 1, 'key2': 2}" |
| * Only int, uint, string, and bool types are valid map keys. |
| * Maps containing HTTP headers must always use lower-cased string keys. |
| |
| Comments start with two-forward slashes followed by text and a newline.` |
| ) |
| |
| var ( |
| hiddenFunctions = map[string]bool{ |
| overloads.DeprecatedIn: true, |
| operators.OldIn: true, |
| operators.OldNotStrictlyFalse: true, |
| operators.NotStrictlyFalse: true, |
| } |
| ) |