package renderers import ( "testing" "github.com/ollama/ollama/api" "github.com/stretchr/testify/assert" ) func TestFunctionGemmaRenderer(t *testing.T) { tests := []struct { name string messages []api.Message tools []api.Tool expected string }{ { name: "basic_user_message", messages: []api.Message{ {Role: "user", Content: "Hello!"}, }, expected: "user\nHello!\nmodel\n", }, { name: "with_system_message", messages: []api.Message{ {Role: "system", Content: "You are helpful"}, {Role: "user", Content: "Hello!"}, }, expected: "developer\nYou are helpful\nuser\nHello!\nmodel\n", }, { name: "with_developer_role", messages: []api.Message{ {Role: "developer", Content: "You are a coding assistant"}, {Role: "user", Content: "Hello!"}, }, expected: "developer\nYou are a coding assistant\nuser\nHello!\nmodel\n", }, { name: "custom_system_message_with_tools", messages: []api.Message{ {Role: "system", Content: "You are a weather expert."}, {Role: "user", Content: "Weather?"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Description: "Get weather", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}, Description: "City"}, }, }, }, }, }, // Custom system message is preserved, tools are appended expected: "developer\nYou are a weather expert.\nYou can do function calling with the following functions:declaration:get_weather{description:Get weather,parameters:{properties:{city:{description:City,type:STRING}},type:OBJECT}}\nuser\nWeather?\nmodel\n", }, { name: "developer_role_with_tools", messages: []api.Message{ {Role: "developer", Content: "Be concise."}, {Role: "user", Content: "Weather?"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Description: "Get weather", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}, Description: "City"}, }, }, }, }, }, // Developer role message is preserved, tools are appended expected: "developer\nBe concise.\nYou can do function calling with the following functions:declaration:get_weather{description:Get weather,parameters:{properties:{city:{description:City,type:STRING}},type:OBJECT}}\nuser\nWeather?\nmodel\n", }, { name: "multi_turn", messages: []api.Message{ {Role: "user", Content: "Hi"}, {Role: "assistant", Content: "Hello!"}, {Role: "user", Content: "More"}, }, expected: "user\nHi\nmodel\nHello!\nuser\nMore\nmodel\n", }, { name: "with_tools", messages: []api.Message{ {Role: "user", Content: "Weather?"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Description: "Get weather", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}, Description: "City"}, }, }, }, }, }, expected: "developer\nYou can do function calling with the following functions:declaration:get_weather{description:Get weather,parameters:{properties:{city:{description:City,type:STRING}},type:OBJECT}}\nuser\nWeather?\nmodel\n", }, { name: "tool_call", messages: []api.Message{ {Role: "user", Content: "Weather?"}, { Role: "assistant", ToolCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: api.ToolCallFunctionArguments{"city": "Paris"}, }, }, }, }, {Role: "tool", Content: "Sunny"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Description: "Get weather", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}, Description: "City"}, }, }, }, }, }, expected: "developer\nYou can do function calling with the following functions:declaration:get_weather{description:Get weather,parameters:{properties:{city:{description:City,type:STRING}},type:OBJECT}}\nuser\nWeather?\nmodel\ncall:get_weather{city:Paris}response:get_weather{Sunny}", }, { name: "assistant_content_with_tool_call", messages: []api.Message{ {Role: "user", Content: "Weather?"}, { Role: "assistant", Content: "Let me check.", ToolCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: api.ToolCallFunctionArguments{"city": "Paris"}, }, }, }, }, {Role: "tool", Content: "Sunny"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Description: "Get weather", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}, Description: "City"}, }, }, }, }, }, expected: "developer\nYou can do function calling with the following functions:declaration:get_weather{description:Get weather,parameters:{properties:{city:{description:City,type:STRING}},type:OBJECT}}\nuser\nWeather?\nmodel\nLet me check.call:get_weather{city:Paris}response:get_weather{Sunny}", }, { name: "numeric_arguments", messages: []api.Message{ {Role: "user", Content: "Add"}, { Role: "assistant", ToolCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "add", Arguments: api.ToolCallFunctionArguments{"a": float64(1), "b": float64(2)}, }, }, }, }, {Role: "tool", Content: "3"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "add", Description: "Add numbers", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "a": {Type: api.PropertyType{"number"}}, "b": {Type: api.PropertyType{"number"}}, }, }, }, }, }, expected: "developer\nYou can do function calling with the following functions:declaration:add{description:Add numbers,parameters:{properties:{a:{description:,type:NUMBER},b:{description:,type:NUMBER}},type:OBJECT}}\nuser\nAdd\nmodel\ncall:add{a:1,b:2}response:add{3}", }, { name: "empty_messages", messages: []api.Message{}, expected: "model\n", }, { name: "tool_with_required_params", messages: []api.Message{ {Role: "user", Content: "Weather?"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Description: "Gets the weather for a given city", Parameters: api.ToolFunctionParameters{ Type: "object", Required: []string{"city"}, Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}, Description: "City Name"}, "country": {Type: api.PropertyType{"string"}, Description: "Country Name"}, }, }, }, }, }, // Required params are escaped: required:[city] expected: "developer\nYou can do function calling with the following functions:declaration:get_weather{description:Gets the weather for a given city,parameters:{properties:{city:{description:City Name,type:STRING},country:{description:Country Name,type:STRING}},required:[city],type:OBJECT}}\nuser\nWeather?\nmodel\n", }, { name: "multiple_tools", messages: []api.Message{ {Role: "user", Content: "Weather and time?"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Description: "Get weather", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}, Description: "City"}, }, }, }, }, { Type: "function", Function: api.ToolFunction{ Name: "get_time", Description: "Get current time", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "timezone": {Type: api.PropertyType{"string"}, Description: "Timezone"}, }, }, }, }, }, // Multiple tool declarations are consecutive expected: "developer\nYou can do function calling with the following functions:declaration:get_weather{description:Get weather,parameters:{properties:{city:{description:City,type:STRING}},type:OBJECT}}declaration:get_time{description:Get current time,parameters:{properties:{timezone:{description:Timezone,type:STRING}},type:OBJECT}}\nuser\nWeather and time?\nmodel\n", }, { name: "parallel_tool_calls", messages: []api.Message{ {Role: "user", Content: "Weather and time?"}, { Role: "assistant", ToolCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: api.ToolCallFunctionArguments{"city": "Paris"}, }, }, { Function: api.ToolCallFunction{ Name: "get_time", Arguments: api.ToolCallFunctionArguments{"timezone": "UTC"}, }, }, }, }, {Role: "tool", Content: "Sunny"}, {Role: "tool", Content: "12:00"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Description: "Get weather", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}, Description: "City"}, }, }, }, }, { Type: "function", Function: api.ToolFunction{ Name: "get_time", Description: "Get current time", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "timezone": {Type: api.PropertyType{"string"}, Description: "Timezone"}, }, }, }, }, }, // Multiple tool calls and responses are consecutive expected: "developer\nYou can do function calling with the following functions:declaration:get_weather{description:Get weather,parameters:{properties:{city:{description:City,type:STRING}},type:OBJECT}}declaration:get_time{description:Get current time,parameters:{properties:{timezone:{description:Timezone,type:STRING}},type:OBJECT}}\nuser\nWeather and time?\nmodel\ncall:get_weather{city:Paris}call:get_time{timezone:UTC}response:get_weather{Sunny}response:get_time{12:00}", }, { name: "user_after_tool_response", messages: []api.Message{ {Role: "user", Content: "Weather?"}, { Role: "assistant", ToolCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: api.ToolCallFunctionArguments{"city": "Paris"}, }, }, }, }, {Role: "tool", Content: "Sunny"}, {Role: "user", Content: "Thanks! What about London?"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Description: "Get weather", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}, Description: "City"}, }, }, }, }, }, // User message after tool response gets concatenated (user reverted to this behavior) expected: "developer\nYou can do function calling with the following functions:declaration:get_weather{description:Get weather,parameters:{properties:{city:{description:City,type:STRING}},type:OBJECT}}\nuser\nWeather?\nmodel\ncall:get_weather{city:Paris}response:get_weather{Sunny}Thanks! What about London?\nmodel\n", }, // Edge cases { name: "tool_empty_properties", messages: []api.Message{ {Role: "user", Content: "Test"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "test_fn", Description: "", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{}, }, }, }, }, // Empty properties are omitted expected: "developer\nYou can do function calling with the following functions:declaration:test_fn{description:,parameters:{type:OBJECT}}\nuser\nTest\nmodel\n", }, { name: "unicode_content", messages: []api.Message{ {Role: "user", Content: "こんにちは 🎉"}, }, expected: "user\nこんにちは 🎉\nmodel\n", }, { name: "newlines_in_content", messages: []api.Message{ {Role: "user", Content: "Line 1\nLine 2\nLine 3"}, }, expected: "user\nLine 1\nLine 2\nLine 3\nmodel\n", }, { name: "special_chars_in_content", messages: []api.Message{ {Role: "user", Content: "Test & \"quotes\" chars"}, }, expected: "user\nTest & \"quotes\" chars\nmodel\n", }, { name: "boolean_argument", messages: []api.Message{ {Role: "user", Content: "Set flag"}, { Role: "assistant", ToolCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "set_flag", Arguments: api.ToolCallFunctionArguments{"enabled": true}, }, }, }, }, {Role: "tool", Content: "done"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "set_flag", Description: "Set a flag", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "enabled": {Type: api.PropertyType{"boolean"}, Description: "Flag value"}, }, }, }, }, }, expected: "developer\nYou can do function calling with the following functions:declaration:set_flag{description:Set a flag,parameters:{properties:{enabled:{description:Flag value,type:BOOLEAN}},type:OBJECT}}\nuser\nSet flag\nmodel\ncall:set_flag{enabled:true}response:set_flag{done}", }, { name: "multiple_required_params", messages: []api.Message{ {Role: "user", Content: "Test"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "test", Description: "Test", Parameters: api.ToolFunctionParameters{ Type: "object", Required: []string{"a", "b", "c"}, Properties: map[string]api.ToolProperty{ "a": {Type: api.PropertyType{"string"}, Description: "A"}, "b": {Type: api.PropertyType{"string"}, Description: "B"}, "c": {Type: api.PropertyType{"string"}, Description: "C"}, }, }, }, }, }, expected: "developer\nYou can do function calling with the following functions:declaration:test{description:Test,parameters:{properties:{a:{description:A,type:STRING},b:{description:B,type:STRING},c:{description:C,type:STRING}},required:[a,b,c],type:OBJECT}}\nuser\nTest\nmodel\n", }, { name: "array_type_param", messages: []api.Message{ {Role: "user", Content: "Test"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "test", Description: "Test", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "items": {Type: api.PropertyType{"array"}, Description: "List of items"}, }, }, }, }, }, expected: "developer\nYou can do function calling with the following functions:declaration:test{description:Test,parameters:{properties:{items:{description:List of items,type:ARRAY}},type:OBJECT}}\nuser\nTest\nmodel\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { renderer := &FunctionGemmaRenderer{} result, err := renderer.Render(tt.messages, tt.tools, nil) assert.NoError(t, err) assert.Equal(t, tt.expected, result) }) } }