mirror of
https://github.com/ollama/ollama.git
synced 2026-01-02 12:38:15 -05:00
Compare commits
4 Commits
implement-
...
jmorganca/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32c6d43e1b | ||
|
|
f8ba24577b | ||
|
|
861a521b19 | ||
|
|
793248c280 |
44
model/parsers/intellect3.go
Normal file
44
model/parsers/intellect3.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"github.com/ollama/ollama/api"
|
||||
"github.com/ollama/ollama/thinking"
|
||||
)
|
||||
|
||||
// Intellect3Parser combines thinking support using
|
||||
// the built-in thinking parser, with tool call support
|
||||
// via qwen3-coder's parser.
|
||||
type Intellect3Parser struct {
|
||||
thinkingParser thinking.Parser
|
||||
toolParser Qwen3CoderParser
|
||||
}
|
||||
|
||||
func (p *Intellect3Parser) HasToolSupport() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Intellect3Parser) HasThinkingSupport() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Intellect3Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||
p.thinkingParser = thinking.Parser{
|
||||
OpeningTag: "<think>",
|
||||
ClosingTag: "</think>",
|
||||
}
|
||||
p.toolParser = Qwen3CoderParser{}
|
||||
return p.toolParser.Init(tools, lastMessage, thinkValue)
|
||||
}
|
||||
|
||||
func (p *Intellect3Parser) Add(s string, done bool) (content string, thinking string, calls []api.ToolCall, err error) {
|
||||
// First extract thinking content
|
||||
thinkingContent, remainingContent := p.thinkingParser.AddContent(s)
|
||||
|
||||
// Then process the remaining content for tool calls
|
||||
toolContent, _, toolCalls, err := p.toolParser.Add(remainingContent, done)
|
||||
if err != nil {
|
||||
return "", thinkingContent, nil, err
|
||||
}
|
||||
|
||||
return toolContent, thinkingContent, toolCalls, nil
|
||||
}
|
||||
542
model/parsers/intellect3_test.go
Normal file
542
model/parsers/intellect3_test.go
Normal file
@@ -0,0 +1,542 @@
|
||||
package parsers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/ollama/ollama/api"
|
||||
)
|
||||
|
||||
func TestIntellect3ParserThinkingOnly(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
chunks []string
|
||||
wantText string
|
||||
wantThink string
|
||||
}{
|
||||
{
|
||||
desc: "simple thinking content",
|
||||
chunks: []string{"<think>I need to analyze this</think>Here is my response"},
|
||||
wantText: "Here is my response",
|
||||
wantThink: "I need to analyze this",
|
||||
},
|
||||
{
|
||||
desc: "thinking with whitespace",
|
||||
chunks: []string{"<think>\n Some thoughts \n</think>\n\nContent"},
|
||||
wantText: "Content",
|
||||
wantThink: "Some thoughts \n", // Thinking parser preserves internal whitespace
|
||||
},
|
||||
{
|
||||
desc: "thinking only",
|
||||
chunks: []string{"<think>Just thinking</think>"},
|
||||
wantText: "",
|
||||
wantThink: "Just thinking",
|
||||
},
|
||||
{
|
||||
desc: "no thinking tags",
|
||||
chunks: []string{"Just regular content"},
|
||||
wantText: "Just regular content",
|
||||
wantThink: "",
|
||||
},
|
||||
{
|
||||
desc: "streaming thinking content",
|
||||
chunks: []string{"<think>Fir", "st part", " second part</think>Content"},
|
||||
wantText: "Content",
|
||||
wantThink: "First part second part",
|
||||
},
|
||||
{
|
||||
desc: "partial opening tag",
|
||||
chunks: []string{"<thi", "nk>Thinking</think>Content"},
|
||||
wantText: "Content",
|
||||
wantThink: "Thinking",
|
||||
},
|
||||
{
|
||||
desc: "partial closing tag",
|
||||
chunks: []string{"<think>Thinking</thi", "nk>Content"},
|
||||
wantText: "Content",
|
||||
wantThink: "Thinking",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
parser := Intellect3Parser{}
|
||||
parser.Init(nil, nil, nil)
|
||||
|
||||
var gotText, gotThink string
|
||||
for i, chunk := range tc.chunks {
|
||||
isLast := i == len(tc.chunks)-1
|
||||
text, think, calls, err := parser.Add(chunk, isLast)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
gotText += text
|
||||
gotThink += think
|
||||
if len(calls) > 0 {
|
||||
t.Fatalf("expected no tool calls, got %v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
if gotText != tc.wantText {
|
||||
t.Errorf("content: got %q, want %q", gotText, tc.wantText)
|
||||
}
|
||||
if gotThink != tc.wantThink {
|
||||
t.Errorf("thinking: got %q, want %q", gotThink, tc.wantThink)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntellect3ParserToolCallsOnly(t *testing.T) {
|
||||
tools := []api.Tool{
|
||||
tool("get_weather", map[string]api.ToolProperty{
|
||||
"location": {Type: api.PropertyType{"string"}},
|
||||
"unit": {Type: api.PropertyType{"string"}},
|
||||
}),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chunks []string
|
||||
wantText string
|
||||
wantCalls []api.ToolCall
|
||||
}{
|
||||
{
|
||||
desc: "simple tool call",
|
||||
chunks: []string{
|
||||
"Let me check the weather<tool_call><function=get_weather>\n<parameter=location>\nSan Francisco\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function></tool_call>",
|
||||
},
|
||||
wantText: "Let me check the weather",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "San Francisco",
|
||||
"unit": "celsius",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "tool call streaming",
|
||||
chunks: []string{
|
||||
"Checking<tool_call><function=get_wea",
|
||||
"ther>\n<parameter=location>\nNew York\n</param", //nolint:all
|
||||
"eter>\n<parameter=unit>\nfahrenheit\n</parameter>\n</function></tool_call>Done",
|
||||
},
|
||||
wantText: "CheckingDone",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "New York",
|
||||
"unit": "fahrenheit",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple tool calls",
|
||||
chunks: []string{
|
||||
"<tool_call><function=get_weather>\n<parameter=location>\nBoston\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function></tool_call>",
|
||||
"<tool_call><function=get_weather>\n<parameter=location>\nSeattle\n</parameter>\n<parameter=unit>\nfahrenheit\n</parameter>\n</function></tool_call>",
|
||||
},
|
||||
wantText: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Boston",
|
||||
"unit": "celsius",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Seattle",
|
||||
"unit": "fahrenheit",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "no tool calls",
|
||||
chunks: []string{"Just regular content"},
|
||||
wantText: "Just regular content",
|
||||
wantCalls: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
parser := Intellect3Parser{}
|
||||
parser.Init(tools, nil, nil)
|
||||
|
||||
var gotText string
|
||||
var gotCalls []api.ToolCall
|
||||
for i, chunk := range tc.chunks {
|
||||
isLast := i == len(tc.chunks)-1
|
||||
text, think, calls, err := parser.Add(chunk, isLast)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
gotText += text
|
||||
gotCalls = append(gotCalls, calls...)
|
||||
if think != "" {
|
||||
t.Fatalf("expected no thinking, got %q", think)
|
||||
}
|
||||
}
|
||||
|
||||
if gotText != tc.wantText {
|
||||
t.Errorf("content: got %q, want %q", gotText, tc.wantText)
|
||||
}
|
||||
if !reflect.DeepEqual(gotCalls, tc.wantCalls) {
|
||||
t.Errorf("tool calls: got %#v, want %#v", gotCalls, tc.wantCalls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntellect3ParserCombined(t *testing.T) {
|
||||
tools := []api.Tool{
|
||||
tool("get_weather", map[string]api.ToolProperty{
|
||||
"location": {Type: api.PropertyType{"string"}},
|
||||
"unit": {Type: api.PropertyType{"string"}},
|
||||
}),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chunks []string
|
||||
wantText string
|
||||
wantThink string
|
||||
wantCalls []api.ToolCall
|
||||
}{
|
||||
{
|
||||
desc: "thinking then tool call",
|
||||
chunks: []string{
|
||||
"<think>Need to get weather data</think>Let me check<tool_call><function=get_weather>\n<parameter=location>\nParis\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function></tool_call>",
|
||||
},
|
||||
wantText: "Let me check",
|
||||
wantThink: "Need to get weather data",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Paris",
|
||||
"unit": "celsius",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "thinking, tool call, and final content",
|
||||
chunks: []string{
|
||||
"<think>User wants weather info</think>Checking weather<tool_call><function=get_weather>\n<parameter=location>\nTokyo\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function></tool_call>Done!",
|
||||
},
|
||||
wantText: "Checking weatherDone!",
|
||||
wantThink: "User wants weather info",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Tokyo",
|
||||
"unit": "celsius",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "streaming combined content",
|
||||
chunks: []string{
|
||||
"<think>Analyzing",
|
||||
" the request</think>",
|
||||
"Let me help<tool_call>",
|
||||
"<function=get_weather>\n<parameter=location>\nLondon",
|
||||
"\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function>",
|
||||
"</tool_call>There you go!",
|
||||
},
|
||||
wantText: "Let me helpThere you go!",
|
||||
wantThink: "Analyzing the request",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "London",
|
||||
"unit": "celsius",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple tool calls with thinking",
|
||||
chunks: []string{
|
||||
"<think>Need multiple locations</think>",
|
||||
"<tool_call><function=get_weather>\n<parameter=location>\nBoston\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function></tool_call>",
|
||||
"and<tool_call><function=get_weather>\n<parameter=location>\nBerlin\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function></tool_call>",
|
||||
},
|
||||
wantText: "and",
|
||||
wantThink: "Need multiple locations",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Boston",
|
||||
"unit": "celsius",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Berlin",
|
||||
"unit": "celsius",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
parser := Intellect3Parser{}
|
||||
parser.Init(tools, nil, nil)
|
||||
|
||||
var gotText, gotThink string
|
||||
var gotCalls []api.ToolCall
|
||||
for i, chunk := range tc.chunks {
|
||||
isLast := i == len(tc.chunks)-1
|
||||
text, think, calls, err := parser.Add(chunk, isLast)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
gotText += text
|
||||
gotThink += think
|
||||
gotCalls = append(gotCalls, calls...)
|
||||
}
|
||||
|
||||
if gotText != tc.wantText {
|
||||
t.Errorf("content: got %q, want %q", gotText, tc.wantText)
|
||||
}
|
||||
if gotThink != tc.wantThink {
|
||||
t.Errorf("thinking: got %q, want %q", gotThink, tc.wantThink)
|
||||
}
|
||||
if !reflect.DeepEqual(gotCalls, tc.wantCalls) {
|
||||
t.Errorf("tool calls: got %#v, want %#v", gotCalls, tc.wantCalls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntellect3ParserEdgeCases(t *testing.T) {
|
||||
tools := []api.Tool{
|
||||
tool("test_func", map[string]api.ToolProperty{
|
||||
"param": {Type: api.PropertyType{"string"}},
|
||||
}),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chunks []string
|
||||
wantText string
|
||||
wantThink string
|
||||
wantCalls int
|
||||
}{
|
||||
{
|
||||
desc: "empty input",
|
||||
chunks: []string{""},
|
||||
wantText: "",
|
||||
wantThink: "",
|
||||
wantCalls: 0,
|
||||
},
|
||||
{
|
||||
desc: "only whitespace",
|
||||
chunks: []string{" \n \t "},
|
||||
wantText: "",
|
||||
wantThink: "",
|
||||
wantCalls: 0,
|
||||
},
|
||||
{
|
||||
desc: "unclosed thinking tag",
|
||||
chunks: []string{"<think>Never closes"},
|
||||
wantText: "",
|
||||
wantThink: "Never closes",
|
||||
wantCalls: 0,
|
||||
},
|
||||
{
|
||||
desc: "unclosed tool call tag",
|
||||
chunks: []string{"<tool_call><function=test_func>\n<parameter=param>\nvalue\n</parameter>\n</function>"},
|
||||
wantText: "", // Qwen3CoderParser waits for closing tag, doesn't emit partial tool calls
|
||||
wantThink: "",
|
||||
wantCalls: 0, // Won't be parsed until </tool_call> is seen
|
||||
},
|
||||
{
|
||||
desc: "unicode in thinking",
|
||||
chunks: []string{"<think>思考中 🤔</think>答案是 42"},
|
||||
wantText: "答案是 42",
|
||||
wantThink: "思考中 🤔",
|
||||
wantCalls: 0,
|
||||
},
|
||||
{
|
||||
desc: "fake thinking tag",
|
||||
chunks: []string{"<thinking>This is not the right tag</thinking>Content"},
|
||||
wantText: "<thinking>This is not the right tag</thinking>Content",
|
||||
wantThink: "",
|
||||
wantCalls: 0,
|
||||
},
|
||||
{
|
||||
desc: "fake tool call tag",
|
||||
chunks: []string{"<tool>Not a tool call</tool>"},
|
||||
wantText: "<tool>Not a tool call</tool>",
|
||||
wantThink: "",
|
||||
wantCalls: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
parser := Intellect3Parser{}
|
||||
parser.Init(tools, nil, nil)
|
||||
|
||||
var gotText, gotThink string
|
||||
var gotCalls []api.ToolCall
|
||||
for i, chunk := range tc.chunks {
|
||||
isLast := i == len(tc.chunks)-1
|
||||
text, think, calls, err := parser.Add(chunk, isLast)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
gotText += text
|
||||
gotThink += think
|
||||
gotCalls = append(gotCalls, calls...)
|
||||
}
|
||||
|
||||
if gotText != tc.wantText {
|
||||
t.Errorf("content: got %q, want %q", gotText, tc.wantText)
|
||||
}
|
||||
if gotThink != tc.wantThink {
|
||||
t.Errorf("thinking: got %q, want %q", gotThink, tc.wantThink)
|
||||
}
|
||||
if len(gotCalls) != tc.wantCalls {
|
||||
t.Errorf("tool calls count: got %d, want %d", len(gotCalls), tc.wantCalls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntellect3ParserCapabilities(t *testing.T) {
|
||||
parser := Intellect3Parser{}
|
||||
|
||||
if !parser.HasToolSupport() {
|
||||
t.Error("Intellect3Parser should have tool support")
|
||||
}
|
||||
|
||||
if !parser.HasThinkingSupport() {
|
||||
t.Error("Intellect3Parser should have thinking support")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntellect3ParserInit(t *testing.T) {
|
||||
parser := Intellect3Parser{}
|
||||
|
||||
tools := []api.Tool{
|
||||
tool("test", map[string]api.ToolProperty{
|
||||
"param": {Type: api.PropertyType{"string"}},
|
||||
}),
|
||||
}
|
||||
|
||||
returnedTools := parser.Init(tools, nil, nil)
|
||||
|
||||
// Should return tools unchanged (delegated to Qwen3CoderParser)
|
||||
if !reflect.DeepEqual(returnedTools, tools) {
|
||||
t.Errorf("Init should return tools unchanged")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntellect3ParserWhitespaceHandling(t *testing.T) {
|
||||
tools := []api.Tool{
|
||||
tool("test", map[string]api.ToolProperty{
|
||||
"param": {Type: api.PropertyType{"string"}},
|
||||
}),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
chunks []string
|
||||
wantText string
|
||||
wantThink string
|
||||
}{
|
||||
{
|
||||
desc: "whitespace between thinking and content",
|
||||
chunks: []string{"<think>Thinking</think>\n\n\nContent"},
|
||||
wantText: "Content",
|
||||
wantThink: "Thinking",
|
||||
},
|
||||
{
|
||||
desc: "whitespace inside thinking tags",
|
||||
chunks: []string{"<think> \n Thinking \n </think>Content"},
|
||||
wantText: "Content",
|
||||
wantThink: "Thinking \n ", // Thinking parser preserves internal whitespace
|
||||
},
|
||||
{
|
||||
desc: "leading whitespace before thinking",
|
||||
chunks: []string{" <think>Thinking</think>Content"},
|
||||
wantText: "Content",
|
||||
wantThink: "Thinking",
|
||||
},
|
||||
{
|
||||
desc: "whitespace before tool call",
|
||||
chunks: []string{"Text <tool_call><function=test>\n<parameter=param>\nvalue\n</parameter>\n</function></tool_call>"},
|
||||
wantText: "Text",
|
||||
wantThink: "",
|
||||
},
|
||||
{
|
||||
desc: "whitespace after tool call",
|
||||
chunks: []string{"<tool_call><function=test>\n<parameter=param>\nvalue\n</parameter>\n</function></tool_call> Text"},
|
||||
wantText: "Text",
|
||||
wantThink: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
parser := Intellect3Parser{}
|
||||
parser.Init(tools, nil, nil)
|
||||
|
||||
var gotText, gotThink string
|
||||
for i, chunk := range tc.chunks {
|
||||
isLast := i == len(tc.chunks)-1
|
||||
text, think, _, err := parser.Add(chunk, isLast)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
gotText += text
|
||||
gotThink += think
|
||||
}
|
||||
|
||||
if gotText != tc.wantText {
|
||||
t.Errorf("content: got %q, want %q", gotText, tc.wantText)
|
||||
}
|
||||
if gotThink != tc.wantThink {
|
||||
t.Errorf("thinking: got %q, want %q", gotThink, tc.wantThink)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,8 @@ func ParserForName(name string) Parser {
|
||||
return harmony.NewHarmonyMessageHandler()
|
||||
case "cogito":
|
||||
return &CogitoParser{}
|
||||
case "intellect-3":
|
||||
return &Intellect3Parser{}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
158
model/renderers/intellect3.go
Normal file
158
model/renderers/intellect3.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package renderers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ollama/ollama/api"
|
||||
)
|
||||
|
||||
const intellect3DefaultSystemMessage = "You are INTELLECT-3, a helpful assistant developed by Prime Intellect, that can interact with a computer to solve tasks."
|
||||
|
||||
type Intellect3Renderer struct{}
|
||||
|
||||
func (r *Intellect3Renderer) Render(messages []api.Message, tools []api.Tool, think *api.ThinkValue) (string, error) {
|
||||
var sb strings.Builder
|
||||
|
||||
// filter out system messages and choose the first (if any) to win
|
||||
var systemMessage string
|
||||
var filteredMessages []api.Message
|
||||
for _, message := range messages {
|
||||
if message.Role != "system" {
|
||||
filteredMessages = append(filteredMessages, message)
|
||||
continue
|
||||
}
|
||||
|
||||
if systemMessage == "" {
|
||||
systemMessage = message.Content
|
||||
}
|
||||
}
|
||||
|
||||
if systemMessage != "" || len(tools) > 0 {
|
||||
sb.WriteString(imStartTag + "system\n")
|
||||
|
||||
// Use default system message when tools present but no user system message
|
||||
if systemMessage == "" && len(tools) > 0 {
|
||||
systemMessage = intellect3DefaultSystemMessage
|
||||
}
|
||||
|
||||
sb.WriteString(systemMessage)
|
||||
|
||||
if len(tools) > 0 {
|
||||
sb.WriteString("\n\n# Tools\n\nYou have access to the following functions:\n\n")
|
||||
sb.WriteString("<tools>")
|
||||
for _, tool := range tools {
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString("<function>\n")
|
||||
sb.WriteString("<name>" + tool.Function.Name + "</name>")
|
||||
if tool.Function.Description != "" {
|
||||
sb.WriteString("\n<description>" + tool.Function.Description + "</description>")
|
||||
}
|
||||
sb.WriteString("\n<parameters>")
|
||||
|
||||
for name, prop := range tool.Function.Parameters.Properties {
|
||||
sb.WriteString("\n<parameter>")
|
||||
sb.WriteString("\n<name>" + name + "</name>")
|
||||
|
||||
if len(prop.Type) > 0 {
|
||||
sb.WriteString("\n<type>" + formatToolDefinitionType(prop.Type) + "</type>")
|
||||
}
|
||||
|
||||
if prop.Description != "" {
|
||||
sb.WriteString("\n<description>" + prop.Description + "</description>")
|
||||
}
|
||||
|
||||
// Render any additional keys not already handled
|
||||
handledKeys := map[string]bool{
|
||||
"type": true,
|
||||
"description": true,
|
||||
}
|
||||
sb.WriteString(renderAdditionalKeys(prop, handledKeys))
|
||||
|
||||
sb.WriteString("\n</parameter>")
|
||||
}
|
||||
|
||||
// Render extra keys for parameters (everything except 'type' and 'properties')
|
||||
paramHandledKeys := map[string]bool{
|
||||
"type": true,
|
||||
"properties": true,
|
||||
}
|
||||
sb.WriteString(renderAdditionalKeys(tool.Function.Parameters, paramHandledKeys))
|
||||
|
||||
sb.WriteString("\n</parameters>")
|
||||
sb.WriteString("\n</function>")
|
||||
}
|
||||
sb.WriteString("\n</tools>")
|
||||
sb.WriteString("\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>")
|
||||
}
|
||||
|
||||
sb.WriteString(imEndTag + "\n")
|
||||
}
|
||||
|
||||
for i, message := range filteredMessages {
|
||||
lastMessage := i == len(filteredMessages)-1
|
||||
prefill := lastMessage && message.Role == "assistant"
|
||||
switch message.Role {
|
||||
case "assistant":
|
||||
if len(message.ToolCalls) > 0 {
|
||||
sb.WriteString(imStartTag + "assistant\n")
|
||||
|
||||
// Add thinking tags if present
|
||||
if message.Thinking != "" {
|
||||
sb.WriteString("<think>" + strings.TrimSpace(message.Thinking) + "</think>\n")
|
||||
}
|
||||
|
||||
if message.Content != "" {
|
||||
sb.WriteString(strings.TrimSpace(message.Content) + "\n")
|
||||
}
|
||||
|
||||
for _, toolCall := range message.ToolCalls {
|
||||
sb.WriteString("\n<tool_call>\n<function=" + toolCall.Function.Name + ">")
|
||||
for name, value := range toolCall.Function.Arguments {
|
||||
valueStr := formatToolCallArgument(value)
|
||||
sb.WriteString("\n<parameter=" + name + ">\n" + valueStr + "\n</parameter>")
|
||||
}
|
||||
sb.WriteString("\n</function>\n</tool_call>")
|
||||
}
|
||||
sb.WriteString("<|im_end|>\n")
|
||||
} else {
|
||||
sb.WriteString(imStartTag + "assistant\n")
|
||||
|
||||
// Add thinking tags if present
|
||||
if message.Thinking != "" {
|
||||
sb.WriteString("<think>" + strings.TrimSpace(message.Thinking) + "</think>\n")
|
||||
}
|
||||
|
||||
// Add content if present
|
||||
if message.Content != "" {
|
||||
sb.WriteString(message.Content)
|
||||
}
|
||||
|
||||
if !prefill {
|
||||
sb.WriteString(imEndTag + "\n")
|
||||
}
|
||||
}
|
||||
case "tool":
|
||||
if i == 0 || filteredMessages[i-1].Role != "tool" {
|
||||
sb.WriteString(imStartTag + "user\n")
|
||||
}
|
||||
|
||||
sb.WriteString("<tool_response>\n")
|
||||
sb.WriteString(message.Content)
|
||||
sb.WriteString("\n</tool_response>\n")
|
||||
|
||||
if i == len(filteredMessages)-1 || filteredMessages[i+1].Role != "tool" {
|
||||
sb.WriteString(imEndTag + "\n")
|
||||
}
|
||||
default:
|
||||
sb.WriteString(imStartTag + message.Role + "\n")
|
||||
sb.WriteString(message.Content)
|
||||
sb.WriteString(imEndTag + "\n")
|
||||
}
|
||||
|
||||
if lastMessage && !prefill {
|
||||
sb.WriteString(imStartTag + "assistant\n<think>")
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
218
model/renderers/intellect3_test.go
Normal file
218
model/renderers/intellect3_test.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package renderers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/ollama/ollama/api"
|
||||
)
|
||||
|
||||
func TestIntellect3Renderer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
msgs []api.Message
|
||||
tools []api.Tool
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "basic user message",
|
||||
msgs: []api.Message{
|
||||
{Role: "user", Content: "Hello!"},
|
||||
},
|
||||
expected: "<|im_start|>user\n" +
|
||||
"Hello!<|im_end|>\n" +
|
||||
"<|im_start|>assistant\n" +
|
||||
"<think>",
|
||||
},
|
||||
{
|
||||
name: "with system message",
|
||||
msgs: []api.Message{
|
||||
{Role: "system", Content: "You are helpful."},
|
||||
{Role: "user", Content: "Hi"},
|
||||
},
|
||||
expected: "<|im_start|>system\n" +
|
||||
"You are helpful.<|im_end|>\n" +
|
||||
"<|im_start|>user\n" +
|
||||
"Hi<|im_end|>\n" +
|
||||
"<|im_start|>assistant\n" +
|
||||
"<think>",
|
||||
},
|
||||
{
|
||||
name: "multi-turn conversation",
|
||||
msgs: []api.Message{
|
||||
{Role: "user", Content: "Hello"},
|
||||
{Role: "assistant", Content: "Hi!"},
|
||||
{Role: "user", Content: "Bye"},
|
||||
},
|
||||
expected: "<|im_start|>user\n" +
|
||||
"Hello<|im_end|>\n" +
|
||||
"<|im_start|>assistant\n" +
|
||||
"Hi!<|im_end|>\n" +
|
||||
"<|im_start|>user\n" +
|
||||
"Bye<|im_end|>\n" +
|
||||
"<|im_start|>assistant\n" +
|
||||
"<think>",
|
||||
},
|
||||
{
|
||||
name: "with tools no system message",
|
||||
msgs: []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{
|
||||
"location": {Type: api.PropertyType{"string"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "<|im_start|>system\n" +
|
||||
"You are INTELLECT-3, a helpful assistant developed by Prime Intellect, that can interact with a computer to solve tasks.\n\n" +
|
||||
"# Tools\n\n" +
|
||||
"You have access to the following functions:\n\n" +
|
||||
"<tools>\n" +
|
||||
"<function>\n" +
|
||||
"<name>get_weather</name>\n" +
|
||||
"<description>Get weather</description>\n" +
|
||||
"<parameters>\n" +
|
||||
"<parameter>\n" +
|
||||
"<name>location</name>\n" +
|
||||
"<type>string</type>\n" +
|
||||
"</parameter>\n" +
|
||||
"</parameters>\n" +
|
||||
"</function>\n" +
|
||||
"</tools>\n\n" +
|
||||
"If you choose to call a function ONLY reply in the following format with NO suffix:\n\n" +
|
||||
"<tool_call>\n" +
|
||||
"<function=example_function_name>\n" +
|
||||
"<parameter=example_parameter_1>\n" +
|
||||
"value_1\n" +
|
||||
"</parameter>\n" +
|
||||
"<parameter=example_parameter_2>\n" +
|
||||
"This is the value for the second parameter\n" +
|
||||
"that can span\n" +
|
||||
"multiple lines\n" +
|
||||
"</parameter>\n" +
|
||||
"</function>\n" +
|
||||
"</tool_call>\n\n" +
|
||||
"<IMPORTANT>\n" +
|
||||
"Reminder:\n" +
|
||||
"- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n" +
|
||||
"- Required parameters MUST be specified\n" +
|
||||
"- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n" +
|
||||
"- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n" +
|
||||
"</IMPORTANT><|im_end|>\n" +
|
||||
"<|im_start|>user\n" +
|
||||
"Weather?<|im_end|>\n" +
|
||||
"<|im_start|>assistant\n" +
|
||||
"<think>",
|
||||
},
|
||||
{
|
||||
name: "tool call and response",
|
||||
msgs: []api.Message{
|
||||
{Role: "user", Content: "Weather?"},
|
||||
{
|
||||
Role: "assistant",
|
||||
Content: "Checking.",
|
||||
ToolCalls: []api.ToolCall{
|
||||
{
|
||||
ID: "1",
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{"location": "SF"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{Role: "tool", Content: `{"temp": 68}`, ToolCallID: "1"},
|
||||
},
|
||||
tools: []api.Tool{
|
||||
{
|
||||
Type: "function",
|
||||
Function: api.ToolFunction{
|
||||
Name: "get_weather",
|
||||
Parameters: api.ToolFunctionParameters{
|
||||
Type: "object",
|
||||
Properties: map[string]api.ToolProperty{
|
||||
"location": {Type: api.PropertyType{"string"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "<|im_start|>system\n" +
|
||||
"You are INTELLECT-3, a helpful assistant developed by Prime Intellect, that can interact with a computer to solve tasks.\n\n" +
|
||||
"# Tools\n\n" +
|
||||
"You have access to the following functions:\n\n" +
|
||||
"<tools>\n" +
|
||||
"<function>\n" +
|
||||
"<name>get_weather</name>\n" +
|
||||
"<parameters>\n" +
|
||||
"<parameter>\n" +
|
||||
"<name>location</name>\n" +
|
||||
"<type>string</type>\n" +
|
||||
"</parameter>\n" +
|
||||
"</parameters>\n" +
|
||||
"</function>\n" +
|
||||
"</tools>\n\n" +
|
||||
"If you choose to call a function ONLY reply in the following format with NO suffix:\n\n" +
|
||||
"<tool_call>\n" +
|
||||
"<function=example_function_name>\n" +
|
||||
"<parameter=example_parameter_1>\n" +
|
||||
"value_1\n" +
|
||||
"</parameter>\n" +
|
||||
"<parameter=example_parameter_2>\n" +
|
||||
"This is the value for the second parameter\n" +
|
||||
"that can span\n" +
|
||||
"multiple lines\n" +
|
||||
"</parameter>\n" +
|
||||
"</function>\n" +
|
||||
"</tool_call>\n\n" +
|
||||
"<IMPORTANT>\n" +
|
||||
"Reminder:\n" +
|
||||
"- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n" +
|
||||
"- Required parameters MUST be specified\n" +
|
||||
"- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n" +
|
||||
"- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n" +
|
||||
"</IMPORTANT><|im_end|>\n" +
|
||||
"<|im_start|>user\n" +
|
||||
"Weather?<|im_end|>\n" +
|
||||
"<|im_start|>assistant\n" +
|
||||
"Checking.\n\n" +
|
||||
"<tool_call>\n" +
|
||||
"<function=get_weather>\n" +
|
||||
"<parameter=location>\n" +
|
||||
"SF\n" +
|
||||
"</parameter>\n" +
|
||||
"</function>\n" +
|
||||
"</tool_call><|im_end|>\n" +
|
||||
"<|im_start|>user\n" +
|
||||
"<tool_response>\n" +
|
||||
`{"temp": 68}` + "\n" +
|
||||
"</tool_response>\n" +
|
||||
"<|im_end|>\n" +
|
||||
"<|im_start|>assistant\n" +
|
||||
"<think>",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rendered, err := (&Intellect3Renderer{}).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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,9 @@ func rendererForName(name string) Renderer {
|
||||
case "cogito":
|
||||
renderer := &CogitoRenderer{isThinking: true}
|
||||
return renderer
|
||||
case "intellect-3":
|
||||
renderer := &Intellect3Renderer{}
|
||||
return renderer
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user