package parsers import ( "testing" "github.com/ollama/ollama/api" ) func lagunaTestTools() []api.Tool { props := api.NewToolPropertiesMap() props.Set("location", api.ToolProperty{Type: api.PropertyType{"string"}}) props.Set("days", api.ToolProperty{Type: api.PropertyType{"integer"}}) return []api.Tool{{ Function: api.ToolFunction{ Name: "get_weather", Parameters: api.ToolFunctionParameters{ Properties: props, }, }, }} } func TestLagunaParserToolCall(t *testing.T) { parser := ParserForName("laguna") if parser == nil { t.Fatal("expected laguna parser") } if !parser.HasToolSupport() || !parser.HasThinkingSupport() { t.Fatal("laguna parser should advertise tools and thinking") } parser.Init(lagunaTestTools(), nil, nil) content, thinking, calls, err := parser.Add("get_weather\nlocation\nParis\ndays\n3\n", true) if err != nil { t.Fatal(err) } if content != "" || thinking != "" { t.Fatalf("content=%q thinking=%q, want empty", content, thinking) } if len(calls) != 1 { t.Fatalf("calls=%d, want 1", len(calls)) } if calls[0].Function.Name != "get_weather" { t.Fatalf("name=%q, want get_weather", calls[0].Function.Name) } if got, _ := calls[0].Function.Arguments.Get("location"); got != "Paris" { t.Fatalf("location=%v, want Paris", got) } if got, _ := calls[0].Function.Arguments.Get("days"); got != 3 { t.Fatalf("days=%v, want 3", got) } } func TestLagunaParserJSONToolCall(t *testing.T) { parser := ParserForName("laguna") parser.Init(lagunaTestTools(), nil, nil) _, _, calls, err := parser.Add("\n{\"name\":\"get_weather\",\"arguments\":{\"location\":\"Paris\",\"days\":3}}\n", true) if err != nil { t.Fatal(err) } if len(calls) != 1 { t.Fatalf("calls=%d, want 1", len(calls)) } if calls[0].Function.Name != "get_weather" { t.Fatalf("name=%q, want get_weather", calls[0].Function.Name) } if got, _ := calls[0].Function.Arguments.Get("location"); got != "Paris" { t.Fatalf("location=%v, want Paris", got) } if got, _ := calls[0].Function.Arguments.Get("days"); got != float64(3) { t.Fatalf("days=%v, want 3", got) } } func TestLagunaParserStandaloneJSONToolCall(t *testing.T) { parser := ParserForName("laguna") parser.Init(lagunaTestTools(), nil, nil) content, thinking, calls, err := parser.Add("{\"name\":\"get_weather\",\"arguments\":{\"location\":\"Paris\",\"days\":3}}", true) if err != nil { t.Fatal(err) } if content != "" || thinking != "" { t.Fatalf("content=%q thinking=%q", content, thinking) } if len(calls) != 1 { t.Fatalf("calls=%d, want 1", len(calls)) } if calls[0].Function.Name != "get_weather" { t.Fatalf("name=%q, want get_weather", calls[0].Function.Name) } } func TestLagunaParserStandaloneJSONToolCallAfterLeadingContent(t *testing.T) { parser := ParserForName("laguna") parser.Init(lagunaTestTools(), nil, nil) content, thinking, calls, err := parser.Add("Let me call the weather tool.\n{\"name\":\"get_weather\",\"arguments\":{\"location\":\"Paris\"}}", true) if err != nil { t.Fatal(err) } if content != "Let me call the weather tool." || thinking != "" { t.Fatalf("content=%q thinking=%q", content, thinking) } if len(calls) != 1 { t.Fatalf("calls=%d, want 1", len(calls)) } if calls[0].Function.Name != "get_weather" { t.Fatalf("name=%q, want get_weather", calls[0].Function.Name) } } func TestLagunaParserStreamingStandaloneJSONToolCall(t *testing.T) { parser := ParserForName("laguna") parser.Init(lagunaTestTools(), nil, nil) content, thinking, calls, err := parser.Add("{\"name\":\"get_weather\",\"arguments\":{\"location\":\"San Francisco,", false) if err != nil { t.Fatal(err) } if content != "" || thinking != "" || len(calls) != 0 { t.Fatalf("first chunk content=%q thinking=%q calls=%d", content, thinking, len(calls)) } content, thinking, calls, err = parser.Add(" CA\"}}", true) if err != nil { t.Fatal(err) } if content != "" || thinking != "" || len(calls) != 1 { t.Fatalf("second chunk content=%q thinking=%q calls=%d", content, thinking, len(calls)) } if calls[0].Function.Name != "get_weather" { t.Fatalf("name=%q, want get_weather", calls[0].Function.Name) } if got, _ := calls[0].Function.Arguments.Get("location"); got != "San Francisco, CA" { t.Fatalf("location=%v, want San Francisco, CA", got) } } func TestLagunaParserNameLineJSONToolCall(t *testing.T) { parser := ParserForName("laguna") parser.Init(lagunaTestTools(), nil, nil) _, _, calls, err := parser.Add("get_weather\n{\"location\":\"San Francisco\"}", true) if err != nil { t.Fatal(err) } if len(calls) != 1 { t.Fatalf("calls=%d, want 1", len(calls)) } if calls[0].Function.Name != "get_weather" { t.Fatalf("name=%q, want get_weather", calls[0].Function.Name) } if got, _ := calls[0].Function.Arguments.Get("location"); got != "San Francisco" { t.Fatalf("location=%v, want San Francisco", got) } } func TestLagunaParserNormalizesCommonToolAliases(t *testing.T) { props := api.NewToolPropertiesMap() props.Set("path", api.ToolProperty{Type: api.PropertyType{"string"}}) tools := []api.Tool{{ Function: api.ToolFunction{ Name: "read", Parameters: api.ToolFunctionParameters{ Properties: props, }, }, }} parser := ParserForName("laguna") parser.Init(tools, nil, nil) _, _, calls, err := parser.Add("\n{\"name\":\"read_file\",\"arguments\":{\"path\":\"./go.mod\"}}\n", true) if err != nil { t.Fatal(err) } if len(calls) != 1 { t.Fatalf("calls=%d, want 1", len(calls)) } if calls[0].Function.Name != "read" { t.Fatalf("name=%q, want read", calls[0].Function.Name) } if got, _ := calls[0].Function.Arguments.Get("path"); got != "./go.mod" { t.Fatalf("path=%v, want ./go.mod", got) } } func TestLagunaParserIgnoresDuplicatedNestedToolCall(t *testing.T) { props := api.NewToolPropertiesMap() props.Set("name", api.ToolProperty{Type: api.PropertyType{"string"}}) tools := []api.Tool{{ Function: api.ToolFunction{ Name: "skill", Parameters: api.ToolFunctionParameters{ Properties: props, }, }, }} parser := ParserForName("laguna") parser.Init(tools, nil, nil) _, _, calls, err := parser.Add("skill\n{\"name\":\"git-diff-review\"}\nskill\n{\"name\":\"git-diff-review\"}", true) if err != nil { t.Fatal(err) } if len(calls) != 1 { t.Fatalf("calls=%d, want 1", len(calls)) } if calls[0].Function.Name != "skill" { t.Fatalf("name=%q, want skill", calls[0].Function.Name) } if got, _ := calls[0].Function.Arguments.Get("name"); got != "git-diff-review" { t.Fatalf("name arg=%v, want git-diff-review", got) } } func TestLagunaParserThinkingThenTool(t *testing.T) { parser := ParserForName("laguna") parser.Init(lagunaTestTools(), nil, &api.ThinkValue{Value: true}) content, thinking, calls, err := parser.Add("Need current weather.\nget_weather\nlocation\nSF\n", true) if err != nil { t.Fatal(err) } if content != "" { t.Fatalf("content=%q, want empty", content) } if thinking != "Need current weather." { t.Fatalf("thinking=%q, want reasoning", thinking) } if len(calls) != 1 || calls[0].Function.Name != "get_weather" { t.Fatalf("unexpected calls: %#v", calls) } } func TestLagunaParserUserTaggedToolAlias(t *testing.T) { parser := ParserForName("laguna") parser.Init(lagunaTestTools(), nil, nil) content, thinking, calls, err := parser.Add("get_weather\nlocation\nSan Francisco, CA\n", true) if err != nil { t.Fatal(err) } if content != "" || thinking != "" { t.Fatalf("content=%q thinking=%q, want empty", content, thinking) } if len(calls) != 1 { t.Fatalf("calls=%d, want 1", len(calls)) } if calls[0].Function.Name != "get_weather" { t.Fatalf("name=%q, want get_weather", calls[0].Function.Name) } if got, _ := calls[0].Function.Arguments.Get("location"); got != "San Francisco, CA" { t.Fatalf("location=%v, want San Francisco, CA", got) } } func TestLagunaParserUserTaggedToolAliasWithLeadingContent(t *testing.T) { parser := ParserForName("laguna") props := api.NewToolPropertiesMap() props.Set("path", api.ToolProperty{Type: api.PropertyType{"string"}}) tools := []api.Tool{{ Function: api.ToolFunction{ Name: "read", Parameters: api.ToolFunctionParameters{ Properties: props, }, }, }} parser.Init(tools, nil, nil) content, thinking, calls, err := parser.Add("I'll read the file for you.\nread\npath\n/Users/test/code/myproject/go.mod\n", true) if err != nil { t.Fatal(err) } if content != "I'll read the file for you." || thinking != "" { t.Fatalf("content=%q thinking=%q", content, thinking) } if len(calls) != 1 { t.Fatalf("calls=%d, want 1", len(calls)) } if calls[0].Function.Name != "read" { t.Fatalf("name=%q, want read", calls[0].Function.Name) } if got, _ := calls[0].Function.Arguments.Get("path"); got != "/Users/test/code/myproject/go.mod" { t.Fatalf("path=%v, want /Users/test/code/myproject/go.mod", got) } } func TestLagunaParserUserTaggedJSONToolCallWithLeadingContent(t *testing.T) { parser := ParserForName("laguna") props := api.NewToolPropertiesMap() props.Set("command", api.ToolProperty{Type: api.PropertyType{"string"}}) tools := []api.Tool{{ Function: api.ToolFunction{ Name: "bash", Parameters: api.ToolFunctionParameters{ Properties: props, }, }, }} parser.Init(tools, nil, nil) content, thinking, calls, err := parser.Add("I'll run git diff for you.\n{\"name\":\"bash\",\"arguments\":{\"command\":\"git diff main\"}}\n", true) if err != nil { t.Fatal(err) } if content != "I'll run git diff for you." || thinking != "" { t.Fatalf("content=%q thinking=%q", content, thinking) } if len(calls) != 1 { t.Fatalf("calls=%d, want 1", len(calls)) } if calls[0].Function.Name != "bash" { t.Fatalf("name=%q, want bash", calls[0].Function.Name) } if got, _ := calls[0].Function.Arguments.Get("command"); got != "git diff main" { t.Fatalf("command=%v, want git diff main", got) } } func TestLagunaParserStreamingUserTaggedToolAliasAfterContent(t *testing.T) { parser := ParserForName("laguna") props := api.NewToolPropertiesMap() props.Set("path", api.ToolProperty{Type: api.PropertyType{"string"}}) tools := []api.Tool{{ Function: api.ToolFunction{ Name: "read", Parameters: api.ToolFunctionParameters{ Properties: props, }, }, }} parser.Init(tools, nil, nil) content, thinking, calls, err := parser.Add("I'll read the file for you.read\npath\n/Users/test/code/myproject/go.mod\n", true) if err != nil { t.Fatal(err) } if content != "" || thinking != "" { t.Fatalf("second chunk content=%q thinking=%q", content, thinking) } if len(calls) != 1 { t.Fatalf("calls=%d, want 1", len(calls)) } if calls[0].Function.Name != "read" { t.Fatalf("name=%q, want read", calls[0].Function.Name) } if got, _ := calls[0].Function.Arguments.Get("path"); got != "/Users/test/code/myproject/go.mod" { t.Fatalf("path=%v, want /Users/test/code/myproject/go.mod", got) } } func TestLagunaParserUserTaggedNonToolContent(t *testing.T) { parser := ParserForName("laguna") parser.Init(lagunaTestTools(), nil, nil) content, thinking, calls, err := parser.Add("hello", true) if err != nil { t.Fatal(err) } if content != "hello" || thinking != "" || len(calls) != 0 { t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls)) } } func TestLagunaParserThinkingDefaultsOn(t *testing.T) { parser := ParserForName("laguna") parser.Init(nil, nil, nil) content, thinking, calls, err := parser.Add("Need to reason.\nDirect answer.", true) if err != nil { t.Fatal(err) } if content != "Direct answer." || thinking != "Need to reason." || len(calls) != 0 { t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls)) } } func TestLagunaParserThinkingDefaultsOnWhenToolsPresent(t *testing.T) { parser := ParserForName("laguna") parser.Init(lagunaTestTools(), nil, nil) content, thinking, calls, err := parser.Add("Need to reason.\nget_weather\nlocation\nParis\n", true) if err != nil { t.Fatal(err) } if thinking != "Need to reason." || len(calls) != 1 { t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls)) } if content != "" { t.Fatalf("content=%q, want thinking block suppressed from content when default thinking is enabled", content) } } func TestLagunaParserThinkingExplicitlyDisabled(t *testing.T) { parser := ParserForName("laguna") parser.Init(nil, nil, &api.ThinkValue{Value: false}) content, thinking, calls, err := parser.Add("Hidden?\nDirect answer.", true) if err != nil { t.Fatal(err) } if content != "Direct answer." || thinking != "" || len(calls) != 0 { t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls)) } } func TestLagunaParserThinkingExplicitlyDisabledDropsLeadingCloseTag(t *testing.T) { parser := ParserForName("laguna") parser.Init(nil, nil, &api.ThinkValue{Value: false}) content, thinking, calls, err := parser.Add("\nTokyo\n", true) if err != nil { t.Fatal(err) } if content != "Tokyo\n" || thinking != "" || len(calls) != 0 { t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls)) } } func TestLagunaParserThinkingEnabledDropsLeadingCloseTag(t *testing.T) { parser := ParserForName("laguna") parser.Init(nil, nil, &api.ThinkValue{Value: true}) content, thinking, calls, err := parser.Add("\nTokyo\n", true) if err != nil { t.Fatal(err) } if content != "Tokyo\n" || thinking != "" || len(calls) != 0 { t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls)) } } func TestLagunaParserThinkingDefaultOnDropsLeadingCloseTag(t *testing.T) { parser := ParserForName("laguna") parser.Init(nil, nil, nil) content, thinking, calls, err := parser.Add("\nTokyo\n", true) if err != nil { t.Fatal(err) } if content != "Tokyo\n" || thinking != "" || len(calls) != 0 { t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls)) } } func TestLagunaParserThinkingEnabledUntaggedAnswerIsContent(t *testing.T) { parser := ParserForName("laguna") parser.Init(nil, nil, &api.ThinkValue{Value: true}) content, thinking, calls, err := parser.Add("Direct answer.", true) if err != nil { t.Fatal(err) } if content != "Direct answer." || thinking != "" || len(calls) != 0 { t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls)) } } func TestLagunaParserSplitToolTag(t *testing.T) { parser := ParserForName("laguna") parser.Init(lagunaTestTools(), nil, &api.ThinkValue{Value: true}) content, thinking, calls, err := parser.Add("Need lookupget_weather\nlocation\nSF\n", true) if err != nil { t.Fatal(err) } if content != "" || thinking != "" || len(calls) != 1 { t.Fatalf("second chunk content=%q thinking=%q calls=%d", content, thinking, len(calls)) } }