mirror of
https://github.com/mudler/LocalAI.git
synced 2026-03-31 21:25:59 -04:00
* feat: add distributed mode (experimental) Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix data races, mutexes, transactions Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactorings Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix events and tool stream in agent chat Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * use ginkgo Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(cron): compute correctly time boundaries avoiding re-triggering Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * enhancements, refactorings Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * do not flood of healthy checks Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * do not list obvious backends as text backends Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * tests fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Drop redundant healthcheck Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * enhancements, refactorings Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
778 lines
23 KiB
Go
778 lines
23 KiB
Go
package peg_test
|
|
|
|
import (
|
|
"github.com/mudler/LocalAI/pkg/functions/peg"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
func extractTags(ast *peg.AstArena, result *peg.ParseResult) map[string]string {
|
|
tags := make(map[string]string)
|
|
ast.VisitResult(result, func(node *peg.AstNode) {
|
|
if node.Tag != "" {
|
|
tags[node.Tag] = node.Text
|
|
}
|
|
})
|
|
return tags
|
|
}
|
|
|
|
var _ = Describe("PEG Parser", func() {
|
|
Context("LiteralParser", func() {
|
|
It("succeeds on exact match", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Literal("hello")
|
|
})
|
|
ctx := peg.NewParseContext("hello world", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.Start).To(Equal(0))
|
|
Expect(r.End).To(Equal(5))
|
|
})
|
|
|
|
It("fails on mismatch", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Literal("hello")
|
|
})
|
|
ctx := peg.NewParseContext("world", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Fail))
|
|
})
|
|
|
|
It("returns NeedMoreInput in partial mode", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Literal("hello")
|
|
})
|
|
ctx := peg.NewParseContext("hel", true)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.NeedMoreInput))
|
|
})
|
|
|
|
It("fails on partial input when not in partial mode", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Literal("hello")
|
|
})
|
|
ctx := peg.NewParseContext("hel", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Fail))
|
|
})
|
|
})
|
|
|
|
Context("SequenceParser", func() {
|
|
It("matches full sequence", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Literal("hello"), b.Literal(" "), b.Literal("world"))
|
|
})
|
|
ctx := peg.NewParseContext("hello world", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(11))
|
|
})
|
|
|
|
It("fails midway", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Literal("hello"), b.Literal("X"))
|
|
})
|
|
ctx := peg.NewParseContext("hello world", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Fail))
|
|
})
|
|
|
|
It("returns NeedMoreInput at boundary in partial mode", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Literal("hello"), b.Literal(" world"))
|
|
})
|
|
ctx := peg.NewParseContext("hello wo", true)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.NeedMoreInput))
|
|
})
|
|
})
|
|
|
|
Context("ChoiceParser", func() {
|
|
It("matches first alternative", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Choice(b.Literal("hello"), b.Literal("world"))
|
|
})
|
|
ctx := peg.NewParseContext("hello", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(5))
|
|
})
|
|
|
|
It("matches second alternative", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Choice(b.Literal("hello"), b.Literal("world"))
|
|
})
|
|
ctx := peg.NewParseContext("world", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(5))
|
|
})
|
|
|
|
It("fails when all alternatives fail", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Choice(b.Literal("hello"), b.Literal("world"))
|
|
})
|
|
ctx := peg.NewParseContext("foo", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Fail))
|
|
})
|
|
})
|
|
|
|
Context("RepetitionParser", func() {
|
|
It("handles zero or more matches", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.ZeroOrMore(b.Literal("ab"))
|
|
})
|
|
|
|
ctx := peg.NewParseContext("ababab", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(6))
|
|
|
|
ctx2 := peg.NewParseContext("xyz", false)
|
|
r2 := arena.Parse(ctx2)
|
|
Expect(r2.Type).To(Equal(peg.Success))
|
|
Expect(r2.End).To(Equal(0))
|
|
})
|
|
|
|
It("handles one or more matches", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.OneOrMore(b.Literal("ab"))
|
|
})
|
|
|
|
ctx := peg.NewParseContext("ababab", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(6))
|
|
|
|
ctx2 := peg.NewParseContext("xyz", false)
|
|
r2 := arena.Parse(ctx2)
|
|
Expect(r2.Type).To(Equal(peg.Fail))
|
|
})
|
|
|
|
It("handles optional matches", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Optional(b.Literal("hello")), b.Literal("world"))
|
|
})
|
|
|
|
ctx := peg.NewParseContext("helloworld", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(10))
|
|
|
|
ctx2 := peg.NewParseContext("world", false)
|
|
r2 := arena.Parse(ctx2)
|
|
Expect(r2.Type).To(Equal(peg.Success))
|
|
Expect(r2.End).To(Equal(5))
|
|
})
|
|
|
|
It("respects bounded repetition", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Repeat(b.Literal("a"), 2, 4)
|
|
})
|
|
|
|
ctx := peg.NewParseContext("aaaaa", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(4))
|
|
|
|
ctx2 := peg.NewParseContext("a", false)
|
|
r2 := arena.Parse(ctx2)
|
|
Expect(r2.Type).To(Equal(peg.Fail))
|
|
})
|
|
})
|
|
|
|
Context("Lookahead", func() {
|
|
It("succeeds with positive lookahead", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Peek(b.Literal("hello")), b.Literal("hello"))
|
|
})
|
|
ctx := peg.NewParseContext("hello", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(5))
|
|
})
|
|
|
|
It("fails with positive lookahead mismatch", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Peek(b.Literal("world")), b.Literal("hello"))
|
|
})
|
|
ctx := peg.NewParseContext("hello", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Fail))
|
|
})
|
|
|
|
It("succeeds with negative lookahead", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Negate(b.Literal("world")), b.Literal("hello"))
|
|
})
|
|
ctx := peg.NewParseContext("hello", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(5))
|
|
})
|
|
|
|
It("fails with negative lookahead match", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Negate(b.Literal("hello")), b.Literal("hello"))
|
|
})
|
|
ctx := peg.NewParseContext("hello", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Fail))
|
|
})
|
|
})
|
|
|
|
Context("UntilParser", func() {
|
|
It("consumes until single delimiter", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Until("<end>"), b.Literal("<end>"))
|
|
})
|
|
ctx := peg.NewParseContext("content<end>", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(12))
|
|
})
|
|
|
|
It("consumes until first of multiple delimiters", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.UntilOneOf("<a>", "<b>")
|
|
})
|
|
ctx := peg.NewParseContext("content<b>more", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(7))
|
|
})
|
|
|
|
It("consumes rest of input", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Rest()
|
|
})
|
|
ctx := peg.NewParseContext("everything", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(10))
|
|
})
|
|
|
|
It("returns NeedMoreInput in partial mode", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Until("<end>")
|
|
})
|
|
ctx := peg.NewParseContext("content", true)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.NeedMoreInput))
|
|
})
|
|
})
|
|
|
|
Context("JSONParser", func() {
|
|
It("parses objects", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.JSON()
|
|
})
|
|
ctx := peg.NewParseContext(`{"key": "value", "num": 42}`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(27))
|
|
})
|
|
|
|
It("parses arrays", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.JSON()
|
|
})
|
|
ctx := peg.NewParseContext(`[1, "two", true, null]`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(22))
|
|
})
|
|
|
|
It("parses strings with escapes", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.JSON()
|
|
})
|
|
ctx := peg.NewParseContext(`"hello \"world\""`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(17))
|
|
})
|
|
|
|
It("parses numbers", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.JSON()
|
|
})
|
|
ctx := peg.NewParseContext(`-123.45e10`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(10))
|
|
})
|
|
|
|
It("parses booleans", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.JSON()
|
|
})
|
|
ctx := peg.NewParseContext(`true`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(4))
|
|
})
|
|
|
|
It("parses null", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.JSON()
|
|
})
|
|
ctx := peg.NewParseContext(`null`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(4))
|
|
})
|
|
|
|
It("parses nested structures", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.JSON()
|
|
})
|
|
input := `{"a": [1, {"b": true}], "c": null}`
|
|
ctx := peg.NewParseContext(input, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(len(input)))
|
|
})
|
|
})
|
|
|
|
Context("Tag extraction", func() {
|
|
It("extracts basic tags", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(
|
|
b.Tag("greeting", b.Until(" ")),
|
|
b.Literal(" "),
|
|
b.Tag("name", b.Rest()),
|
|
)
|
|
})
|
|
ctx := peg.NewParseContext("Hello World", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
|
|
tags := extractTags(&ctx.Ast, &r)
|
|
Expect(tags["greeting"]).To(Equal("Hello"))
|
|
Expect(tags["name"]).To(Equal("World"))
|
|
})
|
|
|
|
It("extracts structured tags", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(
|
|
b.Tag("header", b.Until("\n")),
|
|
b.Literal("\n"),
|
|
b.Tag("body", b.Rest()),
|
|
)
|
|
})
|
|
ctx := peg.NewParseContext("Title\nBody content here", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
|
|
tags := extractTags(&ctx.Ast, &r)
|
|
Expect(tags["header"]).To(Equal("Title"))
|
|
Expect(tags["body"]).To(Equal("Body content here"))
|
|
})
|
|
|
|
It("overwrites duplicate tags", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(
|
|
b.Tag("item", b.Until(",")),
|
|
b.Literal(","),
|
|
b.Tag("item", b.Rest()),
|
|
)
|
|
})
|
|
ctx := peg.NewParseContext("first,second", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
|
|
tags := extractTags(&ctx.Ast, &r)
|
|
Expect(tags["item"]).To(Equal("second"))
|
|
})
|
|
|
|
It("returns empty map when no tags", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Rest()
|
|
})
|
|
ctx := peg.NewParseContext("Hello", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
|
|
tags := extractTags(&ctx.Ast, &r)
|
|
Expect(tags).To(HaveLen(0))
|
|
})
|
|
})
|
|
|
|
Context("Rule and Ref", func() {
|
|
It("handles named rules", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
word := b.Rule("word", b.Chars("[a-z]", 1, -1))
|
|
return b.Seq(word, b.Literal(" "), word)
|
|
})
|
|
ctx := peg.NewParseContext("hello world", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(11))
|
|
})
|
|
|
|
It("handles forward references", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
ref := b.Ref("greeting")
|
|
b.Rule("greeting", b.Literal("hello"))
|
|
return ref
|
|
})
|
|
ctx := peg.NewParseContext("hello", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(5))
|
|
})
|
|
})
|
|
|
|
Context("AtomicParser", func() {
|
|
It("suppresses partial AST nodes", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Atomic(b.Tag("test", b.Literal("hello world")))
|
|
})
|
|
ctx := peg.NewParseContext("hello", true)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.NeedMoreInput))
|
|
Expect(r.Nodes).To(HaveLen(0))
|
|
})
|
|
})
|
|
|
|
Context("Start and End parsers", func() {
|
|
It("matches at start of input", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Start(), b.Literal("hello"))
|
|
})
|
|
ctx := peg.NewParseContext("hello", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
})
|
|
|
|
It("matches at end of input", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Literal("hello"), b.End())
|
|
})
|
|
ctx := peg.NewParseContext("hello", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
|
|
ctx2 := peg.NewParseContext("hello world", false)
|
|
r2 := arena.Parse(ctx2)
|
|
Expect(r2.Type).To(Equal(peg.Fail))
|
|
})
|
|
})
|
|
|
|
Context("Partial parsing", func() {
|
|
It("extracts tags during partial parse", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(
|
|
b.Tag("prefix", b.Until(":")),
|
|
b.Literal(":"),
|
|
b.Tag("value", b.Rest()),
|
|
)
|
|
})
|
|
ctx := peg.NewParseContext("key:val", true)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).NotTo(Equal(peg.Fail))
|
|
|
|
tags := extractTags(&ctx.Ast, &r)
|
|
Expect(tags["prefix"]).To(Equal("key"))
|
|
})
|
|
})
|
|
|
|
Context("ParseAnywhere", func() {
|
|
It("finds pattern in middle of input", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(
|
|
b.Choice(b.Literal("{"), b.Literal(":")),
|
|
b.Space(),
|
|
b.Literal("\""),
|
|
b.Atomic(b.Literal("fun_name")),
|
|
)
|
|
})
|
|
|
|
input := `This is a very long jinja template string... <tool_call>{ "fun_name" : { "arg" : 1 }</tool_call>`
|
|
found := false
|
|
for i := range len(input) {
|
|
ctx := peg.NewParseContext(input, false)
|
|
r := arena.ParseFrom(ctx, i)
|
|
if r.Type == peg.Success {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
Expect(found).To(BeTrue())
|
|
})
|
|
|
|
It("fails when pattern is not found", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(
|
|
b.Choice(b.Literal("{"), b.Literal(":")),
|
|
b.Space(),
|
|
b.Literal("\""),
|
|
b.Atomic(b.Literal("fun_name")),
|
|
)
|
|
})
|
|
|
|
input := `This is a very long jinja template string... <tool_call><fun=fun_name><arg name=arg>1</arg></tool_call>`
|
|
found := false
|
|
for i := range len(input) {
|
|
ctx := peg.NewParseContext(input, false)
|
|
r := arena.ParseFrom(ctx, i)
|
|
if r.Type == peg.Success {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
Expect(found).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
Context("CharsParser", func() {
|
|
It("matches lowercase letters", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Chars("[a-z]", 1, -1)
|
|
})
|
|
ctx := peg.NewParseContext("hello123", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(5))
|
|
})
|
|
|
|
It("matches negated character class", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Chars("[^0-9]", 1, -1)
|
|
})
|
|
ctx := peg.NewParseContext("hello123", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(5))
|
|
})
|
|
})
|
|
|
|
Context("JSONStringParser", func() {
|
|
It("parses basic strings", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Literal("\""), b.JSONString(), b.Literal("\""))
|
|
})
|
|
ctx := peg.NewParseContext(`"hello world"`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(13))
|
|
})
|
|
|
|
It("parses strings with escapes", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Literal("\""), b.JSONString(), b.Literal("\""))
|
|
})
|
|
ctx := peg.NewParseContext(`"hello \"world\""`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(17))
|
|
})
|
|
|
|
It("parses strings with unicode escapes", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Literal("\""), b.JSONString(), b.Literal("\""))
|
|
})
|
|
ctx := peg.NewParseContext(`"hello \u0041"`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(14))
|
|
})
|
|
})
|
|
|
|
Context("SpaceParser", func() {
|
|
It("matches whitespace", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Literal("a"), b.Space(), b.Literal("b"))
|
|
})
|
|
ctx := peg.NewParseContext("a b", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(5))
|
|
})
|
|
|
|
It("matches zero whitespace", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Literal("a"), b.Space(), b.Literal("b"))
|
|
})
|
|
ctx := peg.NewParseContext("ab", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(2))
|
|
})
|
|
})
|
|
|
|
Context("PythonDictStringParser", func() {
|
|
It("parses basic single-quoted strings", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Literal("'"), b.PythonDictString(), b.Literal("'"))
|
|
})
|
|
ctx := peg.NewParseContext("'hello world'", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
Expect(r.End).To(Equal(13))
|
|
})
|
|
|
|
It("handles escaped single quotes", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Literal("'"), b.PythonDictString(), b.Literal("'"))
|
|
})
|
|
ctx := peg.NewParseContext(`'it\'s fine'`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
})
|
|
|
|
It("handles double quotes inside single-quoted strings", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Literal("'"), b.PythonDictString(), b.Literal("'"))
|
|
})
|
|
ctx := peg.NewParseContext(`'He said "hi"'`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
})
|
|
})
|
|
|
|
Context("peg.ParseCharClassChar", func() {
|
|
It("parses \\x hex escape", func() {
|
|
r, n := peg.ParseCharClassChar(`\x41`, 0)
|
|
Expect(r).To(Equal('A'))
|
|
Expect(n).To(Equal(4))
|
|
})
|
|
|
|
It("parses \\u unicode escape", func() {
|
|
r, n := peg.ParseCharClassChar(`\u0041`, 0)
|
|
Expect(r).To(Equal('A'))
|
|
Expect(n).To(Equal(6))
|
|
})
|
|
|
|
It("parses \\U unicode escape", func() {
|
|
r, n := peg.ParseCharClassChar(`\U00000041`, 0)
|
|
Expect(r).To(Equal('A'))
|
|
Expect(n).To(Equal(10))
|
|
})
|
|
|
|
It("falls back on invalid hex", func() {
|
|
r, n := peg.ParseCharClassChar(`\xZZ`, 0)
|
|
Expect(r).To(Equal('x'))
|
|
Expect(n).To(Equal(2))
|
|
})
|
|
})
|
|
|
|
Context("ParseAnywhere method", func() {
|
|
It("finds pattern in middle of input", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Seq(b.Literal("needle"), b.Tag("after", b.Until(".")))
|
|
})
|
|
ctx := peg.NewParseContext("some hay needle found.", false)
|
|
r := arena.ParseAnywhere(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
tags := extractTags(&ctx.Ast, &r)
|
|
Expect(tags["after"]).To(Equal(" found"))
|
|
})
|
|
|
|
It("finds function tag with name", func() {
|
|
haystack := "\n<tool_call>\n<function=foofoo>\n<parameter=first>\nXXXX\n</parameter>\n<parameter=second>\nYYYY\n</parameter>\n</function>\n</tool_call>\n"
|
|
needle := "foofoo"
|
|
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Tag("fun_marker", b.Choice(
|
|
b.Seq(
|
|
b.Tag("fun_pre", b.Seq(b.Literal("<"), b.UntilOneOf(">", needle))),
|
|
b.Literal(needle),
|
|
b.Tag("fun_post", b.Seq(
|
|
b.Seq(b.Negate(b.Seq(b.Space(), b.Literal("<"))), b.Until(">"), b.Literal(">")),
|
|
)),
|
|
b.Space(),
|
|
),
|
|
b.Seq(
|
|
b.Tag("fun_pre", b.Seq(b.Literal("["), b.UntilOneOf("]", needle))),
|
|
b.Literal(needle),
|
|
b.Tag("fun_post", b.Seq(
|
|
b.Negate(b.Seq(b.Space(), b.Seq(b.Literal("["), b.Until("]"), b.Literal("]")))),
|
|
b.Space(),
|
|
)),
|
|
),
|
|
))
|
|
})
|
|
|
|
ctx := peg.NewParseContext(haystack, false)
|
|
r := arena.ParseAnywhere(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
tags := extractTags(&ctx.Ast, &r)
|
|
Expect(tags["fun_pre"]).To(Equal("<function="))
|
|
Expect(tags["fun_post"]).To(Equal(">"))
|
|
})
|
|
})
|
|
|
|
Context("LazyRule", func() {
|
|
It("handles recursive JSON-like structures", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
b.LazyRule("value", func() peg.ParserID {
|
|
str := b.Seq(b.Literal("\""), b.JSONString(), b.Literal("\""))
|
|
arr := b.Seq(
|
|
b.Literal("["), b.Space(),
|
|
b.Ref("value"),
|
|
b.ZeroOrMore(b.Seq(b.Space(), b.Literal(","), b.Space(), b.Ref("value"))),
|
|
b.Space(), b.Literal("]"),
|
|
)
|
|
return b.Choice(str, arr)
|
|
})
|
|
return b.Ref("value")
|
|
})
|
|
ctx := peg.NewParseContext(`["hello",["world","nested"]]`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
})
|
|
|
|
It("parses python dicts", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.PythonDict()
|
|
})
|
|
ctx := peg.NewParseContext(`{'key': 'value', 'num': 42}`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
})
|
|
|
|
It("parses nested python values", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.PythonValue()
|
|
})
|
|
ctx := peg.NewParseContext(`{'outer': {'inner': [1, 2, 'three']}}`, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
})
|
|
|
|
It("parses python booleans and None", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.PythonValue()
|
|
})
|
|
for _, input := range []string{"True", "False", "None"} {
|
|
ctx := peg.NewParseContext(input, false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
}
|
|
})
|
|
})
|
|
|
|
Context("Marker", func() {
|
|
It("matches angle brackets", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Marker()
|
|
})
|
|
ctx := peg.NewParseContext("<tool_call>", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
})
|
|
|
|
It("matches square brackets", func() {
|
|
arena := peg.BuildPegParser(func(b *peg.Builder) peg.ParserID {
|
|
return b.Marker()
|
|
})
|
|
ctx := peg.NewParseContext("[TOOL]", false)
|
|
r := arena.Parse(ctx)
|
|
Expect(r.Type).To(Equal(peg.Success))
|
|
})
|
|
})
|
|
})
|