Skip to content
Open
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
2 changes: 1 addition & 1 deletion backend/pkg/cast/chain_ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const (
FallbackResponseContent = "the call was not handled, please try again"
SummarizationToolName = "execute_task_and_return_summary"
SummarizationToolArgs = `{"question": "delegate and execute the task, then return the summary of the result"}`
ToolCallIDTemplate = "call_{r:24:x}"
ToolCallIDTemplate = templates.DefaultToolCallIDTemplate

// Fake reasoning signatures for different providers when summarizing content
// that originally contained reasoning signatures
Expand Down
2 changes: 1 addition & 1 deletion backend/pkg/providers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type dummyMessage struct {
}

func toolCallIDTemplateOrDefault(template string) string {
if template != "" {
if template != "" && templates.ValidatePatternSyntax(template) == nil {
return template
}

Expand Down
1 change: 1 addition & 0 deletions backend/pkg/providers/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ func TestHelpers_ToolCallIDTemplateOrDefault_NeverYieldsAnEmptyTemplate(t *testi
}{
{name: "an empty detection falls back to the default", detected: "", want: "call_{r:24:x}"},
{name: "a detected pattern is kept", detected: "toolu_{r:24:b}", want: "toolu_{r:24:b}"},
{name: "a pattern the regexp cannot compile falls back to the default", detected: "call_{r:1001:x}", want: "call_{r:24:x}"},
}

for _, tt := range tests {
Expand Down
7 changes: 6 additions & 1 deletion backend/pkg/providers/provider/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,12 @@ func fallbackHeuristicDetection(samples []templates.PatternSample) string {
}
}

return pattern.String()
template := pattern.String()
if templates.ValidatePatternSyntax(template) != nil {
return ""
}

return template
}

// determineCommonCharset finds charset that covers all character sets across positions
Expand Down
4 changes: 4 additions & 0 deletions backend/pkg/providers/provider/agents_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package provider

import (
"strings"
"testing"

"pentagi/pkg/templates"
Expand Down Expand Up @@ -43,6 +44,9 @@ func TestAgents_FallbackHeuristicDetection_BuildsThePatternTheSamplesShare(t *te
{"upper case only", samples("KEY_ABCDEFGH", "KEY_ZYXWVUTS", "KEY_QPONMLKJ"), "KEY_{r:8:u}"},
{"no samples", samples(), ""},
{"a single sample is all literal", samples("test_123abc"), "test_123abc"},
{"a random run longer than the regexp compiles yields nothing", samples(
"call_"+strings.Repeat("a", 1001), "call_"+strings.Repeat("b", 1001), "call_"+strings.Repeat("c", 1001),
), ""},
}

for _, tc := range tests {
Expand Down
44 changes: 34 additions & 10 deletions backend/pkg/templates/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,13 @@ const (

var patternRegex = regexp.MustCompile(`\{r:(\d+):(d|digit|l|lower|u|upper|a|alpha|x|alnum|h|hex|H|HEX|b|base62)\}|\{f\}`)

// DefaultToolCallIDTemplate is the pattern used when a provider's own could not
// be detected or does not compile.
const DefaultToolCallIDTemplate = "call_{r:24:x}"

// maxRandomPartLength is the largest repeat count Go's regexp compiles.
const maxRandomPartLength = 1000

type patternPart struct {
literal string
isRandom bool
Expand Down Expand Up @@ -817,8 +824,14 @@ func getCharset(name string) string {
}
}

// ValidatePatternSyntax reports whether pattern can be compiled and generated from.
func ValidatePatternSyntax(pattern string) error {
_, err := parsePattern(pattern)
return err
}

// parsePattern parses a pattern string into parts
func parsePattern(pattern string) []patternPart {
func parsePattern(pattern string) ([]patternPart, error) {
var parts []patternPart
lastIndex := 0

Expand All @@ -842,7 +855,12 @@ func parsePattern(pattern string) []patternPart {
})
} else {
// Parse random part
length, _ := strconv.Atoi(pattern[match[2]:match[3]])
lengthText := pattern[match[2]:match[3]]
length, err := strconv.Atoi(lengthText)
if err != nil || length > maxRandomPartLength {
return nil, fmt.Errorf("pattern %q: random part length %s exceeds %d",
pattern, lengthText, maxRandomPartLength)
}
charsetName := pattern[match[4]:match[5]]
parts = append(parts, patternPart{
isRandom: true,
Expand All @@ -863,7 +881,7 @@ func parsePattern(pattern string) []patternPart {
})
}

return parts
return parts, nil
}

// generateRandomString generates a random string of specified length using the given charset
Expand All @@ -889,13 +907,17 @@ func generateRandomString(length int, charset string) string {
}

// GenerateFromPattern generates a random string matching the given pattern template.
// This function never returns an error - it uses fallback values for invalid patterns.
// This function never returns an error - it uses fallback values for invalid patterns,
// and a pattern that does not compile yields an ID of DefaultToolCallIDTemplate.
//
// Pattern format: literal text with {r:LENGTH:CHARSET} for random parts and {f} for function name
// Example: "toolu_{r:24:base62}" → "toolu_xK9pQw2mN5vR8tY7uI6oP3zA"
// Example: "{f}:{r:1:d}" with functionName="get_number" → "get_number:0"
func GenerateFromPattern(pattern string, functionName string) string {
parts := parsePattern(pattern)
parts, err := parsePattern(pattern)
if err != nil {
parts, _ = parsePattern(DefaultToolCallIDTemplate)
}
var result strings.Builder

for _, part := range parts {
Expand Down Expand Up @@ -944,12 +966,11 @@ func (e *PatternValidationError) Error() string {
// Pattern format: literal text with {r:LENGTH:CHARSET} for random parts and {f} for function name
// Example: ValidatePattern("call_{r:24:alnum}", []PatternSample{{Value: "call_abc123..."}})
func ValidatePattern(pattern string, samples []PatternSample) error {
if len(samples) == 0 {
return nil
parts, err := parsePattern(pattern)
if err != nil {
return err
}

parts := parsePattern(pattern)

// Validate each sample
for _, sample := range samples {
value := sample.Value
Expand Down Expand Up @@ -981,7 +1002,10 @@ func ValidatePattern(pattern string, samples []PatternSample) error {
}

regexPattern := "^" + strings.Join(regexParts, "") + "$"
re := regexp.MustCompile(regexPattern)
re, err := regexp.Compile(regexPattern)
if err != nil {
return fmt.Errorf("pattern %q: %w", pattern, err)
}

// Check length
if len(value) != expectedLen {
Expand Down
41 changes: 41 additions & 0 deletions backend/pkg/templates/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,18 @@ func TestTemplates_ValidatePattern_NamesWhereASampleDiverges(t *testing.T) {
samples: []PatternSample{{Value: "wrong_name:0", FunctionName: "get_number"}},
wantErr: []string{"pattern mismatch"},
},
{
name: "a repeat the regexp cannot compile",
pattern: "call_{r:1001:x}",
samples: []PatternSample{{Value: "call_x"}},
wantErr: []string{"exceeds 1000"},
},
{
name: "no sample excuses a repeat the regexp cannot compile",
pattern: "call_{r:1001:x}",
samples: []PatternSample{},
wantErr: []string{"exceeds 1000"},
},
}

for _, tt := range tests {
Expand All @@ -190,3 +202,32 @@ func TestTemplates_ValidatePattern_NamesWhereASampleDiverges(t *testing.T) {
})
}
}

func TestTemplates_ValidatePatternSyntax_StopsAtTheLargestRepeatTheRegexpCompiles(t *testing.T) {
tests := []struct {
name string
pattern string
wantErr string
}{
{"the largest repeat the regexp compiles", "call_{r:1000:x}", ""},
{"one above it", "call_{r:1001:x}", "exceeds 1000"},
{"a count that overflows int", "call_{r:99999999999999999999:x}", "exceeds 1000"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidatePatternSyntax(tt.pattern)
if tt.wantErr == "" {
assert.NoError(t, err)
return
}
assert.ErrorContains(t, err, tt.wantErr)
})
}
}

func TestTemplates_GenerateFromPattern_FallsBackToTheDefaultTemplateWhenThePatternCannotCompile(t *testing.T) {
got := GenerateFromPattern("call_{r:1001:x}", "")

assert.Regexp(t, `^call_[a-zA-Z0-9]{24}$`, got)
}