mirror of https://github.com/ollama/ollama.git
working renderer with tests
This commit is contained in:
parent
ec46dc0660
commit
ef84ad9440
|
@ -16,6 +16,8 @@ type Parser interface {
|
||||||
HasThinkingSupport() bool
|
HasThinkingSupport() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// used like builtinParser := parsers.ParserForName(m.Config.Parser)
|
||||||
|
|
||||||
func ParserForName(name string) Parser {
|
func ParserForName(name string) Parser {
|
||||||
switch name {
|
switch name {
|
||||||
case "qwen3-coder":
|
case "qwen3-coder":
|
||||||
|
|
|
@ -55,7 +55,12 @@ func renderAdditionalKeys(obj any, handledKeys map[string]bool) string {
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Qwen3CoderRenderer(messages []api.Message, tools []api.Tool, _ *api.ThinkValue) (string, error) {
|
type Qwen3CoderRenderer struct {
|
||||||
|
isThinking bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Qwen3CoderRenderer) Render(messages []api.Message, tools []api.Tool, _ *api.ThinkValue) (string, error) {
|
||||||
|
// func Qwen3CoderRenderer(messages []api.Message, tools []api.Tool, _ *api.ThinkValue) (string, error) {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
// filter out system messages and choose the first (if any) to win
|
// filter out system messages and choose the first (if any) to win
|
||||||
|
|
|
@ -288,7 +288,8 @@ call tool<|im_end|>
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
rendered, err := Qwen3CoderRenderer(tt.msgs, tt.tools, nil)
|
// rendered, err := Qwen3CoderRenderer(tt.msgs, tt.tools, nil)
|
||||||
|
rendered, err := (&Qwen3CoderRenderer{false}).Render(tt.msgs, tt.tools, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,12 @@ func marshalWithSpaces(v any) ([]byte, error) {
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderContent(content api.Message, doVisionCount bool) string {
|
type Qwen3VLRenderer struct {
|
||||||
|
isThinking bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// func renderContent(content api.Message, doVisionCount bool) string {
|
||||||
|
func (r *Qwen3VLRenderer) renderContent(content api.Message, doVisionCount bool) string {
|
||||||
// This assumes all images are at the front of the message - same assumption as ollama/ollama/runner.go
|
// This assumes all images are at the front of the message - same assumption as ollama/ollama/runner.go
|
||||||
var subSb strings.Builder
|
var subSb strings.Builder
|
||||||
for _ = range content.Images {
|
for _ = range content.Images {
|
||||||
|
@ -64,9 +69,10 @@ func renderContent(content api.Message, doVisionCount bool) string {
|
||||||
return subSb.String()
|
return subSb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Qwen3VLRenderer(messages []api.Message, tools []api.Tool, _ *api.ThinkValue) (string, error) {
|
// func Qwen3VLRenderer(messages []api.Message, tools []api.Tool, _ *api.ThinkValue) (string, error) {
|
||||||
|
func (r *Qwen3VLRenderer) Render(messages []api.Message, tools []api.Tool, _ *api.ThinkValue) (string, error) {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
isThinking := false
|
// r.isThinking = false
|
||||||
|
|
||||||
if len(tools) > 0 {
|
if len(tools) > 0 {
|
||||||
sb.WriteString(imStartTag + "system\n")
|
sb.WriteString(imStartTag + "system\n")
|
||||||
|
@ -100,7 +106,7 @@ func Qwen3VLRenderer(messages []api.Message, tools []api.Tool, _ *api.ThinkValue
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, message := range messages {
|
for i, message := range messages {
|
||||||
content := renderContent(message, true)
|
content := r.renderContent(message, true)
|
||||||
|
|
||||||
if message.Role == "user" || message.Role == "system" && i != 0 {
|
if message.Role == "user" || message.Role == "system" && i != 0 {
|
||||||
sb.WriteString("<|im_start|>" + message.Role + "\n" + content + "<|im_end|>\n")
|
sb.WriteString("<|im_start|>" + message.Role + "\n" + content + "<|im_end|>\n")
|
||||||
|
@ -108,7 +114,7 @@ func Qwen3VLRenderer(messages []api.Message, tools []api.Tool, _ *api.ThinkValue
|
||||||
contentReasoning := ""
|
contentReasoning := ""
|
||||||
|
|
||||||
// here we need to reconstruct
|
// here we need to reconstruct
|
||||||
if isThinking { // we only do this if its a thinking model (i.e contentReasoning != "" if its a thinking model)
|
if r.isThinking { // we only do this if its a thinking model (i.e contentReasoning != "" if its a thinking model)
|
||||||
if message.Thinking != "" {
|
if message.Thinking != "" {
|
||||||
contentReasoning = message.Thinking
|
contentReasoning = message.Thinking
|
||||||
} else if strings.Contains(content, "</think>") {
|
} else if strings.Contains(content, "</think>") {
|
||||||
|
@ -128,7 +134,7 @@ func Qwen3VLRenderer(messages []api.Message, tools []api.Tool, _ *api.ThinkValue
|
||||||
// reconstruct the content
|
// reconstruct the content
|
||||||
|
|
||||||
// isThinking && i > lastQueryIndex
|
// isThinking && i > lastQueryIndex
|
||||||
if isThinking && i > lastQueryIndex { // if it is a thinking model
|
if r.isThinking && i > lastQueryIndex { // if it is a thinking model
|
||||||
if i == len(messages)-1 || contentReasoning != "" {
|
if i == len(messages)-1 || contentReasoning != "" {
|
||||||
sb.WriteString("<|im_start|>" + message.Role + "\n<think>\n" + strings.Trim(contentReasoning, "\n") + "\n</think>\n\n" + strings.TrimLeft(content, "\n"))
|
sb.WriteString("<|im_start|>" + message.Role + "\n<think>\n" + strings.Trim(contentReasoning, "\n") + "\n</think>\n\n" + strings.TrimLeft(content, "\n"))
|
||||||
} else {
|
} else {
|
||||||
|
@ -165,7 +171,7 @@ func Qwen3VLRenderer(messages []api.Message, tools []api.Tool, _ *api.ThinkValue
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.WriteString("<|im_start|>assistant\n")
|
sb.WriteString("<|im_start|>assistant\n")
|
||||||
if isThinking {
|
if r.isThinking {
|
||||||
sb.WriteString("<think>\n") // Thinking models end with <|im_start|>assistant\n<think>\n
|
sb.WriteString("<think>\n") // Thinking models end with <|im_start|>assistant\n<think>\n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -297,7 +297,9 @@ Thanks! What are the results?<|im_end|>
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
rendered, err := Qwen3VLRenderer(tt.msgs, tt.tools, nil)
|
// rendered, err := Qwen3VLRenderer(tt.msgs, tt.tools, nil)
|
||||||
|
// renderer := RendererForName("qwen3-vl")
|
||||||
|
rendered, err := (&Qwen3VLRenderer{false}).Render(tt.msgs, tt.tools, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -307,43 +309,3 @@ Thanks! What are the results?<|im_end|>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// what is this function for?
|
|
||||||
|
|
||||||
// func TestFormatToolCallArgumentVL(t *testing.T) {
|
|
||||||
// tests := []struct {
|
|
||||||
// name string
|
|
||||||
// arg any
|
|
||||||
// expected string
|
|
||||||
// }{
|
|
||||||
// {
|
|
||||||
// name: "string",
|
|
||||||
// arg: "foo",
|
|
||||||
// // notice no quotes around the string
|
|
||||||
// expected: "foo",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "map",
|
|
||||||
// arg: map[string]any{"foo": "bar"},
|
|
||||||
// expected: "{\"foo\":\"bar\"}",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "number",
|
|
||||||
// arg: 1,
|
|
||||||
// expected: "1",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "boolean",
|
|
||||||
// arg: true,
|
|
||||||
// expected: "true",
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// for _, tt := range tests {
|
|
||||||
// t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// got := formatToolCallArgument(tt.arg)
|
|
||||||
// if got != tt.expected {
|
|
||||||
// t.Errorf("formatToolCallArgument(%v) = %v, want %v", tt.arg, got, tt.expected)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
|
@ -0,0 +1,380 @@
|
||||||
|
package renderers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var IMAGE1_BASE64 = base64.StdEncoding.EncodeToString([]byte("image1"))
|
||||||
|
var IMAGE2_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAADMElEQVR4nOzVwQnAIBQFQYXff81RUkQCOyDj1YOPnbXWPmeTRef+/3O/OyBjzh3CD95BfqICMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMO0TAAD//2Anhf4QtqobAAAAAElFTkSuQmCC"
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// - [ ] test videos?
|
||||||
|
// - [ ] set descriptions to omitempty?
|
||||||
|
// - [] images add the auto tag
|
||||||
|
|
||||||
|
func TestQwen3VLThinkingRenderer(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
msgs []api.Message
|
||||||
|
images []api.ImageData
|
||||||
|
tools []api.Tool
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic",
|
||||||
|
msgs: []api.Message{
|
||||||
|
{Role: "system", Content: "You are a helpful assistant."},
|
||||||
|
{Role: "user", Content: "Hello, how are you?"},
|
||||||
|
},
|
||||||
|
expected: `<|im_start|>system
|
||||||
|
You are a helpful assistant.<|im_end|>
|
||||||
|
<|im_start|>user
|
||||||
|
Hello, how are you?<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
<think>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "With thinking, end assistant.",
|
||||||
|
msgs: []api.Message{
|
||||||
|
// {Role: "system", Content: "You are a helpful assistant."},
|
||||||
|
{Role: "user", Content: "Tell me a story in two sentences."},
|
||||||
|
{Role: "assistant", Content: "abc<think>To make this story interesting, I will speak in poetry.</think>"}, // does the thinking even work?
|
||||||
|
},
|
||||||
|
expected: `<|im_start|>user
|
||||||
|
Tell me a story in two sentences.<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
<think>
|
||||||
|
To make this story interesting, I will speak in poetry.
|
||||||
|
</think>
|
||||||
|
|
||||||
|
<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
<think>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple thinking",
|
||||||
|
msgs: []api.Message{
|
||||||
|
{Role: "user", Content: "Tell me a story in two sentences."},
|
||||||
|
{Role: "assistant", Content: "abc<think>To make this story interesting, I will speak in poetry.</think><think>And I will speak in poetry after the first sentence.</think>"},
|
||||||
|
},
|
||||||
|
expected: `<|im_start|>user
|
||||||
|
Tell me a story in two sentences.<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
<think>
|
||||||
|
To make this story interesting, I will speak in poetry.
|
||||||
|
</think>
|
||||||
|
|
||||||
|
<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
<think>
|
||||||
|
`, // the second thinking tag is not captured
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple thinking, multiple messages.",
|
||||||
|
msgs: []api.Message{
|
||||||
|
{Role: "user", Content: "Tell me a story in two sentences."},
|
||||||
|
{Role: "assistant", Content: "abc<think>To make this story interesting, I will speak in poetry.</think><think>And I will speak in poetry after the first sentence.</think>"},
|
||||||
|
{Role: "user", Content: "What is the weather like in San Francisco? <think>I will check the weather in San Francisco for you.</think>"},
|
||||||
|
{Role: "assistant", Content: "I'll check the weather in San Francisco for you.<think>Speak poetry after the first sentence.</think><think>Speak poetry after the second sentence.</think>"},
|
||||||
|
},
|
||||||
|
expected: `<|im_start|>user
|
||||||
|
Tell me a story in two sentences.<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
<|im_end|>
|
||||||
|
<|im_start|>user
|
||||||
|
What is the weather like in San Francisco? <think>I will check the weather in San Francisco for you.</think><|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
<think>
|
||||||
|
Speak poetry after the first sentence.
|
||||||
|
</think>
|
||||||
|
|
||||||
|
<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
<think>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Image",
|
||||||
|
msgs: []api.Message{ // i think this is because it does not go through the renderer?
|
||||||
|
{Role: "user", Content: "Describe this image.", Images: []api.ImageData{api.ImageData(IMAGE2_BASE64)}}, // does this work?
|
||||||
|
}, // this is actually a local test, remote model may need to be different
|
||||||
|
expected: `<|im_start|>user
|
||||||
|
[img-0]Describe this image.<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
<think>
|
||||||
|
`,
|
||||||
|
}, // there's no way to do videos?
|
||||||
|
{
|
||||||
|
name: "Multiple images",
|
||||||
|
msgs: []api.Message{
|
||||||
|
{Role: "user", Content: "Describe these images.", Images: []api.ImageData{api.ImageData(IMAGE1_BASE64), api.ImageData(IMAGE2_BASE64)}},
|
||||||
|
},
|
||||||
|
expected: `<|im_start|>user
|
||||||
|
[img-0][img-1]Describe these images.<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
<think>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with tools and response",
|
||||||
|
msgs: []api.Message{
|
||||||
|
{Role: "system", Content: "You are a helpful assistant with access to tools."},
|
||||||
|
{Role: "user", Content: "What's the weather like in New York?"},
|
||||||
|
{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: "I'll check the weather in New York for you.",
|
||||||
|
ToolCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "get-current-weather",
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"location": "New York",
|
||||||
|
"unit": "fahrenheit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{Role: "tool", Content: "80", ToolName: "get-current-weather"},
|
||||||
|
{Role: "user", Content: "That sounds nice! What about San Francisco?"},
|
||||||
|
},
|
||||||
|
tools: []api.Tool{
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "get-current-weather",
|
||||||
|
Description: "Get the current weather for a location",
|
||||||
|
Parameters: api.ToolFunctionParameters{
|
||||||
|
Type: "object",
|
||||||
|
Required: []string{"location"},
|
||||||
|
Properties: map[string]api.ToolProperty{
|
||||||
|
"location": {
|
||||||
|
Type: api.PropertyType{"string"},
|
||||||
|
Description: "The city and state, e.g. San Francisco, CA",
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
Type: api.PropertyType{"string"},
|
||||||
|
Enum: []any{"celsius", "fahrenheit"},
|
||||||
|
Description: "The temperature unit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `<|im_start|>system
|
||||||
|
You are a helpful assistant with access to tools.
|
||||||
|
|
||||||
|
# Tools
|
||||||
|
|
||||||
|
You may call one or more functions to assist with the user query.
|
||||||
|
|
||||||
|
You are provided with function signatures within <tools></tools> XML tags:
|
||||||
|
<tools>
|
||||||
|
{"type": "function", "function": {"name": "get-current-weather", "description": "Get the current weather for a location", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "The city and state, e.g. San Francisco, CA"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "The temperature unit"}}, "required": ["location"]}}}
|
||||||
|
</tools>
|
||||||
|
|
||||||
|
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
|
||||||
|
<tool_call>
|
||||||
|
{"name": <function-name>, "arguments": <args-json-object>}
|
||||||
|
</tool_call><|im_end|>
|
||||||
|
<|im_start|>user
|
||||||
|
What's the weather like in New York?<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
I'll check the weather in New York for you.
|
||||||
|
<tool_call>
|
||||||
|
{"name": "get-current-weather", "arguments": {"location": "New York", "unit": "fahrenheit"}}
|
||||||
|
</tool_call><|im_end|>
|
||||||
|
<|im_start|>user
|
||||||
|
<tool_response>
|
||||||
|
80
|
||||||
|
</tool_response><|im_end|>
|
||||||
|
<|im_start|>user
|
||||||
|
That sounds nice! What about San Francisco?<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
<think>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "With tools and response, multiple tool calls",
|
||||||
|
msgs: []api.Message{
|
||||||
|
{
|
||||||
|
Role: "system",
|
||||||
|
Content: "You are a helpful assistant with access to tools.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Role: "user",
|
||||||
|
Content: "Call two tools for me: add and multiply.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: "Sure, I'll call both tools for you.",
|
||||||
|
ToolCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "add",
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"a": 2,
|
||||||
|
"b": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "multiply",
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"x": 4,
|
||||||
|
"y": 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Role: "tool",
|
||||||
|
Content: "5",
|
||||||
|
ToolName: "add",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Role: "tool",
|
||||||
|
Content: "20",
|
||||||
|
ToolName: "multiply",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Role: "user",
|
||||||
|
Content: "Thanks! What are the results?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: []api.Tool{
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "add",
|
||||||
|
Description: "Add two numbers",
|
||||||
|
Parameters: api.ToolFunctionParameters{
|
||||||
|
Type: "object",
|
||||||
|
Required: []string{"a", "b"},
|
||||||
|
Properties: map[string]api.ToolProperty{
|
||||||
|
"a": {Type: api.PropertyType{"integer"}, Description: "First number"},
|
||||||
|
"b": {Type: api.PropertyType{"integer"}, Description: "Second number"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: api.ToolFunction{
|
||||||
|
Name: "multiply",
|
||||||
|
Description: "Multiply two numbers",
|
||||||
|
Parameters: api.ToolFunctionParameters{
|
||||||
|
Type: "object",
|
||||||
|
Required: []string{"x", "y"},
|
||||||
|
Properties: map[string]api.ToolProperty{
|
||||||
|
"x": {Type: api.PropertyType{"integer"}, Description: "First factor"},
|
||||||
|
"y": {Type: api.PropertyType{"integer"}, Description: "Second factor"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `<|im_start|>system
|
||||||
|
You are a helpful assistant with access to tools.
|
||||||
|
|
||||||
|
# Tools
|
||||||
|
|
||||||
|
You may call one or more functions to assist with the user query.
|
||||||
|
|
||||||
|
You are provided with function signatures within <tools></tools> XML tags:
|
||||||
|
<tools>
|
||||||
|
{"type": "function", "function": {"name": "add", "description": "Add two numbers", "parameters": {"type": "object", "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}}, "required": ["a", "b"]}}}
|
||||||
|
{"type": "function", "function": {"name": "multiply", "description": "Multiply two numbers", "parameters": {"type": "object", "properties": {"x": {"type": "integer"}, "y": {"type": "integer"}}, "required": ["x", "y"]}}}
|
||||||
|
</tools>
|
||||||
|
|
||||||
|
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
|
||||||
|
<tool_call>
|
||||||
|
{"name": <function-name>, "arguments": <args-json-object>}
|
||||||
|
</tool_call><|im_end|>
|
||||||
|
<|im_start|>user
|
||||||
|
Call two tools for me: add and multiply.<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
Sure, I'll call both tools for you.
|
||||||
|
<tool_call>
|
||||||
|
{"name": "add", "arguments": {"a": 2, "b": 3}}
|
||||||
|
</tool_call>
|
||||||
|
<tool_call>
|
||||||
|
{"name": "multiply", "arguments": {"x": 4, "y": 5}}
|
||||||
|
</tool_call><|im_end|>
|
||||||
|
<|im_start|>user
|
||||||
|
<tool_response>
|
||||||
|
5
|
||||||
|
</tool_response>
|
||||||
|
<tool_response>
|
||||||
|
20
|
||||||
|
</tool_response><|im_end|>
|
||||||
|
<|im_start|>user
|
||||||
|
Thanks! What are the results?<|im_end|>
|
||||||
|
<|im_start|>assistant
|
||||||
|
<think>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// rendered, err := Qwen3VLRenderer(tt.msgs, tt.tools, nil)
|
||||||
|
rendered, err := (&Qwen3VLRenderer{true}).Render(tt.msgs, tt.tools, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(rendered, tt.expected); diff != "" {
|
||||||
|
t.Errorf("mismatch (-got +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// what is this function for?
|
||||||
|
|
||||||
|
func TestFormatToolCallArgumentVL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
arg any
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "string",
|
||||||
|
arg: "foo",
|
||||||
|
// notice no quotes around the string
|
||||||
|
expected: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map",
|
||||||
|
arg: map[string]any{"foo": "bar"},
|
||||||
|
expected: "{\"foo\":\"bar\"}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "number",
|
||||||
|
arg: 1,
|
||||||
|
expected: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "boolean",
|
||||||
|
arg: true,
|
||||||
|
expected: "true",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := formatToolCallArgument(tt.arg)
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("formatToolCallArgument(%v) = %v, want %v", tt.arg, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
// package renderers
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// "encoding/json"
|
||||||
|
// "strings"
|
||||||
|
|
||||||
|
// "github.com/ollama/ollama/api"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// var imageCount int
|
||||||
|
// var videoCount int
|
||||||
|
|
||||||
|
// func marshalWithSpaces(v any) ([]byte, error) {
|
||||||
|
// b, err := json.Marshal(v)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// out := make([]byte, 0, len(b)+len(b)/8)
|
||||||
|
// inStr, esc := false, false
|
||||||
|
// for _, c := range b {
|
||||||
|
// if inStr {
|
||||||
|
// out = append(out, c)
|
||||||
|
// if esc {
|
||||||
|
// esc = false
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
// if c == '\\' {
|
||||||
|
// esc = true
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
// if c == '"' {
|
||||||
|
// inStr = false
|
||||||
|
// }
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
// switch c {
|
||||||
|
// case '"':
|
||||||
|
// inStr = true
|
||||||
|
// out = append(out, c)
|
||||||
|
// case ':':
|
||||||
|
// out = append(out, ':', ' ')
|
||||||
|
// case ',':
|
||||||
|
// out = append(out, ',', ' ')
|
||||||
|
// default:
|
||||||
|
// out = append(out, c)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return out, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func renderContent(content api.Message, doVisionCount bool) string {
|
||||||
|
// // This assumes all images are at the front of the message - same assumption as ollama/ollama/runner.go
|
||||||
|
// var subSb strings.Builder
|
||||||
|
// for _ = range content.Images {
|
||||||
|
// if doVisionCount {
|
||||||
|
// imageCount++
|
||||||
|
// }
|
||||||
|
// subSb.WriteString("<|vision_start|><|image_pad|><|vision_end|>")
|
||||||
|
// }
|
||||||
|
// // TODO: support videos
|
||||||
|
|
||||||
|
// subSb.WriteString(content.Content)
|
||||||
|
// return subSb.String()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func Qwen3VLRenderer(messages []api.Message, tools []api.Tool, _ *api.ThinkValue) (string, error) {
|
||||||
|
// var sb strings.Builder
|
||||||
|
|
||||||
|
// if len(tools) > 0 {
|
||||||
|
// sb.WriteString(imStartTag + "system\n")
|
||||||
|
// if len(messages) > 0 && messages[0].Role == "system" {
|
||||||
|
// sb.WriteString(messages[0].Content + "\n\n")
|
||||||
|
// }
|
||||||
|
// sb.WriteString("# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>")
|
||||||
|
// for _, tool := range tools {
|
||||||
|
// sb.WriteString("\n")
|
||||||
|
// if b, err := marshalWithSpaces(tool); err == nil {
|
||||||
|
// sb.Write(b)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// sb.WriteString("\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n")
|
||||||
|
// } else if len(messages) > 0 && messages[0].Role == "system" {
|
||||||
|
// sb.WriteString("<|im_start|>system\n" + messages[0].Content + "<|im_end|>\n")
|
||||||
|
// }
|
||||||
|
// multiStepTool := true
|
||||||
|
// lastQueryIndex := len(messages) - 1
|
||||||
|
|
||||||
|
// for i := len(messages) - 1; i >= 0; i-- {
|
||||||
|
// message := messages[i]
|
||||||
|
// if multiStepTool && message.Role == "user" {
|
||||||
|
// // Check if content starts with <tool_response> and ends with </tool_response>
|
||||||
|
// content := message.Content
|
||||||
|
// if !(strings.HasPrefix(content, "<tool_response>") && strings.HasSuffix(content, "</tool_response>")) {
|
||||||
|
// multiStepTool = false
|
||||||
|
// lastQueryIndex = i
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for i, message := range messages {
|
||||||
|
// content := renderContent(message, true)
|
||||||
|
|
||||||
|
// if message.Role == "user" || message.Role == "system" && i != 0 {
|
||||||
|
// sb.WriteString("<|im_start|>" + message.Role + "\n" + content + "<|im_end|>\n")
|
||||||
|
// } else if message.Role == "assistant" {
|
||||||
|
// contentReasoning := ""
|
||||||
|
// if message.Thinking != "" {
|
||||||
|
// contentReasoning = message.Thinking
|
||||||
|
// } else if strings.Contains(content, "</think>") {
|
||||||
|
// contentReasoning = strings.Split(content, "</think>")[0]
|
||||||
|
// contentReasoning = strings.TrimRight(contentReasoning, "\n")
|
||||||
|
|
||||||
|
// contentReasoningSplit := strings.Split(contentReasoning, "<think>")
|
||||||
|
// contentReasoning = contentReasoningSplit[len(contentReasoningSplit)-1]
|
||||||
|
|
||||||
|
// contentReasoning = strings.TrimLeft(contentReasoning, "\n")
|
||||||
|
|
||||||
|
// contentSplit := strings.Split(content, "</think>")
|
||||||
|
// content = contentSplit[len(contentSplit)-1]
|
||||||
|
// content = strings.TrimLeft(content, "\n")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if i > lastQueryIndex {
|
||||||
|
// if i == len(messages)-1 || contentReasoning != "" {
|
||||||
|
// sb.WriteString("<|im_start|>" + message.Role + "\n<think>\n" + strings.Trim(contentReasoning, "\n") + "\n</think>\n\n" + strings.TrimLeft(content, "\n"))
|
||||||
|
// } else {
|
||||||
|
// sb.WriteString("<|im_start|>" + message.Role + "\n" + content)
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// sb.WriteString("<|im_start|>" + message.Role + "\n" + content)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if len(message.ToolCalls) > 0 {
|
||||||
|
// for j, toolCall := range message.ToolCalls {
|
||||||
|
// if j > 0 || content != "" {
|
||||||
|
// sb.WriteString("\n")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// sb.WriteString("<tool_call>\n{\"name\": \"" + toolCall.Function.Name + "\", \"arguments\": ")
|
||||||
|
// if b, err := marshalWithSpaces(toolCall.Function.Arguments); err == nil {
|
||||||
|
// sb.Write(b)
|
||||||
|
// }
|
||||||
|
// sb.WriteString("}\n</tool_call>")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// sb.WriteString("<|im_end|>\n")
|
||||||
|
// } else if message.Role == "tool" {
|
||||||
|
// if i == 0 || messages[i-1].Role != "tool" {
|
||||||
|
// sb.WriteString("<|im_start|>user")
|
||||||
|
// }
|
||||||
|
// sb.WriteString("\n<tool_response>\n" + message.Content + "\n</tool_response>")
|
||||||
|
// if i == len(messages)-1 || messages[i+1].Role != "tool" {
|
||||||
|
// sb.WriteString("<|im_end|>\n")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
// sb.WriteString("<|im_start|>assistant<think>\n")
|
||||||
|
// return sb.String(), nil
|
||||||
|
|
||||||
|
// }
|
|
@ -1,27 +1,30 @@
|
||||||
package renderers
|
package renderers
|
||||||
|
|
||||||
import (
|
import "github.com/ollama/ollama/api"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ollama/ollama/api"
|
// type rendererFunc func([]api.Message, []api.Tool, *api.ThinkValue) (string, error)
|
||||||
)
|
|
||||||
|
|
||||||
type rendererFunc func([]api.Message, []api.Tool, *api.ThinkValue) (string, error)
|
// func RenderWithRenderer(name string, msgs []api.Message, tools []api.Tool, think *api.ThinkValue) (string, error) {
|
||||||
|
// renderer := rendererForName(name)
|
||||||
|
// if renderer == nil {
|
||||||
|
// return "", fmt.Errorf("unknown renderer %q", name)
|
||||||
|
// }
|
||||||
|
// return renderer(msgs, tools, think)
|
||||||
|
// }
|
||||||
|
|
||||||
func RenderWithRenderer(name string, msgs []api.Message, tools []api.Tool, think *api.ThinkValue) (string, error) {
|
type Renderer interface {
|
||||||
renderer := rendererForName(name)
|
Render(messages []api.Message, tools []api.Tool, think *api.ThinkValue) (string, error)
|
||||||
if renderer == nil {
|
|
||||||
return "", fmt.Errorf("unknown renderer %q", name)
|
|
||||||
}
|
|
||||||
return renderer(msgs, tools, think)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func rendererForName(name string) rendererFunc {
|
// func rendererForName(name string) rendererFunc {
|
||||||
|
func RendererForName(name string) Renderer {
|
||||||
switch name {
|
switch name {
|
||||||
case "qwen3-coder":
|
case "qwen3-coder":
|
||||||
return Qwen3CoderRenderer
|
renderer := &Qwen3CoderRenderer{false} // this is not implemented yet
|
||||||
|
return renderer
|
||||||
case "qwen3-vl":
|
case "qwen3-vl":
|
||||||
return Qwen3VLRenderer
|
renderer := &Qwen3VLRenderer{false}
|
||||||
|
return renderer
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,9 @@ func chatPrompt(ctx context.Context, m *Model, tokenize tokenizeFunc, opts *api.
|
||||||
|
|
||||||
func renderPrompt(m *Model, msgs []api.Message, tools []api.Tool, think *api.ThinkValue) (string, error) {
|
func renderPrompt(m *Model, msgs []api.Message, tools []api.Tool, think *api.ThinkValue) (string, error) {
|
||||||
if m.Config.Renderer != "" {
|
if m.Config.Renderer != "" {
|
||||||
rendered, err := renderers.RenderWithRenderer(m.Config.Renderer, msgs, tools, think)
|
// rendered, err := renderers.RenderWithRenderer(m.Config.Renderer, msgs, tools, think)
|
||||||
|
renderer := renderers.RendererForName(m.Config.Renderer)
|
||||||
|
rendered, err := renderer.Render(msgs, tools, think)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue