ollama source for Momentry Core verification
This commit is contained in:
436
model/renderers/nemotron3nano.go
Normal file
436
model/renderers/nemotron3nano.go
Normal file
@@ -0,0 +1,436 @@
|
||||
package renderers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ollama/ollama/api"
|
||||
)
|
||||
|
||||
type Nemotron3NanoRenderer struct{}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) Render(messages []api.Message, tools []api.Tool, thinkValue *api.ThinkValue) (string, error) {
|
||||
var sb strings.Builder
|
||||
imageOffset := 0
|
||||
|
||||
enableThinking := r.resolveThinking(messages, thinkValue)
|
||||
|
||||
// Extract system message if present
|
||||
var systemMessage string
|
||||
var loopMessages []api.Message
|
||||
if len(messages) > 0 && messages[0].Role == "system" {
|
||||
systemMessage = r.sanitizeSystemMessage(messages[0].Content)
|
||||
loopMessages = messages[1:]
|
||||
} else {
|
||||
loopMessages = messages
|
||||
}
|
||||
|
||||
// Find last user message index for thinking truncation
|
||||
lastUserIdx := -1
|
||||
for i, msg := range loopMessages {
|
||||
if msg.Role == "user" {
|
||||
lastUserIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n\n\n")
|
||||
sb.WriteString("<|im_start|>system\n")
|
||||
if systemMessage != "" {
|
||||
sb.WriteString(systemMessage)
|
||||
}
|
||||
|
||||
if len(tools) > 0 {
|
||||
if systemMessage != "" {
|
||||
sb.WriteString("\n\n")
|
||||
}
|
||||
sb.WriteString(r.renderTools(tools))
|
||||
}
|
||||
sb.WriteString("<|im_end|>\n\n")
|
||||
|
||||
for i, message := range loopMessages {
|
||||
switch message.Role {
|
||||
case "assistant":
|
||||
content := r.buildContent(message)
|
||||
shouldTruncate := i < lastUserIdx
|
||||
|
||||
if len(message.ToolCalls) > 0 {
|
||||
sb.WriteString("<|im_start|>assistant\n")
|
||||
sb.WriteString(r.formatToolCallContent(content, shouldTruncate))
|
||||
r.writeToolCalls(&sb, message.ToolCalls)
|
||||
sb.WriteString("<|im_end|>\n")
|
||||
} else {
|
||||
formatted := r.formatAssistantContent(content, shouldTruncate)
|
||||
sb.WriteString("<|im_start|>assistant\n")
|
||||
sb.WriteString(formatted)
|
||||
sb.WriteString("<|im_end|>\n")
|
||||
}
|
||||
|
||||
case "user", "system":
|
||||
sb.WriteString("<|im_start|>" + message.Role + "\n")
|
||||
sb.WriteString(r.renderMessageContent(message, imageOffset))
|
||||
imageOffset += len(message.Images)
|
||||
sb.WriteString("<|im_end|>\n")
|
||||
|
||||
case "tool":
|
||||
// Check if previous message was also a tool message
|
||||
prevWasTool := i > 0 && loopMessages[i-1].Role == "tool"
|
||||
nextIsTool := i+1 < len(loopMessages) && loopMessages[i+1].Role == "tool"
|
||||
content := r.renderMessageContent(message, imageOffset)
|
||||
imageOffset += len(message.Images)
|
||||
|
||||
if !prevWasTool {
|
||||
sb.WriteString("<|im_start|>user\n")
|
||||
}
|
||||
sb.WriteString("<tool_response>\n")
|
||||
sb.WriteString(content)
|
||||
sb.WriteString("\n</tool_response>\n")
|
||||
|
||||
if !nextIsTool {
|
||||
sb.WriteString("<|im_end|>\n")
|
||||
}
|
||||
|
||||
default:
|
||||
sb.WriteString("<|im_start|>" + message.Role + "\n" + message.Content + "<|im_end|>\n")
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Add generation prompt
|
||||
if enableThinking {
|
||||
sb.WriteString("<|im_start|>assistant\n<think>\n")
|
||||
} else {
|
||||
sb.WriteString("<|im_start|>assistant\n<think></think>")
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) renderTools(tools []api.Tool) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("# Tools\n\nYou have access to the following functions:\n\n<tools>")
|
||||
|
||||
for _, tool := range tools {
|
||||
fn := tool.Function
|
||||
sb.WriteString("\n<function>\n<name>" + fn.Name + "</name>")
|
||||
|
||||
if fn.Description != "" {
|
||||
sb.WriteString("\n<description>" + strings.TrimSpace(fn.Description) + "</description>")
|
||||
}
|
||||
|
||||
sb.WriteString("\n<parameters>")
|
||||
if fn.Parameters.Properties != nil {
|
||||
for paramName, paramFields := range fn.Parameters.Properties.All() {
|
||||
sb.WriteString("\n<parameter>")
|
||||
sb.WriteString("\n<name>" + paramName + "</name>")
|
||||
|
||||
if len(paramFields.Type) > 0 {
|
||||
sb.WriteString("\n<type>" + r.formatPropertyType(paramFields.Type) + "</type>")
|
||||
}
|
||||
|
||||
if paramFields.Description != "" {
|
||||
sb.WriteString("\n<description>" + strings.TrimSpace(paramFields.Description) + "</description>")
|
||||
}
|
||||
|
||||
if len(paramFields.Enum) > 0 {
|
||||
sb.WriteString("\n<enum>" + r.pythonJSON(paramFields.Enum) + "</enum>")
|
||||
}
|
||||
|
||||
r.renderToolPropertyExtraKeys(&sb, paramFields)
|
||||
sb.WriteString("\n</parameter>")
|
||||
}
|
||||
}
|
||||
|
||||
r.renderToolParameterExtraKeys(&sb, fn.Parameters)
|
||||
if len(fn.Parameters.Required) > 0 {
|
||||
sb.WriteString("\n<required>" + r.pythonJSON(fn.Parameters.Required) + "</required>")
|
||||
}
|
||||
|
||||
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>")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) buildContent(message api.Message) string {
|
||||
content := nemotron3NanoRenderContent(message.Content)
|
||||
if message.Thinking != "" {
|
||||
return "<think>\n" + message.Thinking + "\n</think>\n" + content
|
||||
}
|
||||
if !strings.Contains(content, "<think>") && !strings.Contains(content, "</think>") {
|
||||
return "<think></think>" + content
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) formatAssistantContent(content string, truncate bool) string {
|
||||
if !truncate {
|
||||
return strings.TrimSpace(content)
|
||||
}
|
||||
|
||||
c := content
|
||||
if strings.Contains(c, "<think>") && strings.Contains(c, "</think>") {
|
||||
parts := strings.Split(c, "</think>")
|
||||
c = "<think></think>" + parts[len(parts)-1]
|
||||
}
|
||||
return strings.TrimSpace(c)
|
||||
}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) formatToolCallContent(content string, truncate bool) string {
|
||||
if strings.TrimSpace(content) == "" {
|
||||
return "<think></think>"
|
||||
}
|
||||
|
||||
if !truncate {
|
||||
return strings.TrimSpace(content) + "\n"
|
||||
}
|
||||
|
||||
c := content
|
||||
if strings.Contains(c, "</think>") {
|
||||
parts := strings.Split(c, "</think>")
|
||||
c = parts[len(parts)-1]
|
||||
} else if strings.Contains(c, "<think>") {
|
||||
parts := strings.Split(c, "<think>")
|
||||
c = parts[0]
|
||||
}
|
||||
c = "<think></think>" + strings.TrimSpace(c)
|
||||
|
||||
return strings.TrimSpace(c) + "\n"
|
||||
}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) writeToolCalls(sb *strings.Builder, toolCalls []api.ToolCall) {
|
||||
for _, tc := range toolCalls {
|
||||
sb.WriteString("<tool_call>\n<function=" + tc.Function.Name + ">\n")
|
||||
for name, value := range tc.Function.Arguments.All() {
|
||||
sb.WriteString("<parameter=" + name + ">\n" + r.formatArgValue(value) + "\n</parameter>\n")
|
||||
}
|
||||
sb.WriteString("</function>\n</tool_call>\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) formatArgValue(value any) string {
|
||||
switch v := value.(type) {
|
||||
case map[string]any, []any:
|
||||
return r.pythonJSON(v)
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) renderMessageContent(message api.Message, imageOffset int) string {
|
||||
content := nemotron3NanoRenderContent(message.Content)
|
||||
if len(message.Images) == 0 {
|
||||
return content
|
||||
}
|
||||
|
||||
content, _ = renderContentWithImageTags(content, len(message.Images), imageOffset)
|
||||
return content
|
||||
}
|
||||
|
||||
func nemotron3NanoRenderContent(content any) string {
|
||||
switch v := content.(type) {
|
||||
case string:
|
||||
return v
|
||||
case []any:
|
||||
var sb strings.Builder
|
||||
for _, item := range v {
|
||||
obj, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
bts, _ := json.Marshal(item)
|
||||
sb.Write(bts)
|
||||
continue
|
||||
}
|
||||
|
||||
switch obj["type"] {
|
||||
case "image":
|
||||
sb.WriteString("<image>")
|
||||
case "text":
|
||||
if text, ok := obj["text"].(string); ok {
|
||||
sb.WriteString(text)
|
||||
}
|
||||
default:
|
||||
bts, _ := json.Marshal(item)
|
||||
sb.Write(bts)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
default:
|
||||
bts, _ := json.Marshal(v)
|
||||
return string(bts)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) resolveThinking(messages []api.Message, thinkValue *api.ThinkValue) bool {
|
||||
enableThinking := thinkValue == nil || thinkValue.Bool()
|
||||
for _, message := range messages {
|
||||
if message.Role != "user" && message.Role != "system" {
|
||||
continue
|
||||
}
|
||||
content := message.Content
|
||||
if strings.Contains(strings.ReplaceAll(content, "</think>", ""), "/think") {
|
||||
enableThinking = true
|
||||
} else if strings.Contains(content, "/no_think") {
|
||||
enableThinking = false
|
||||
}
|
||||
}
|
||||
return enableThinking
|
||||
}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) sanitizeSystemMessage(content string) string {
|
||||
system := nemotron3NanoRenderContent(content)
|
||||
system = strings.ReplaceAll(system, "</think>", "<_end_think>")
|
||||
system = strings.ReplaceAll(system, "/think", "")
|
||||
system = strings.ReplaceAll(system, "/no_think", "")
|
||||
system = strings.ReplaceAll(system, "<_end_think>", "</think>")
|
||||
return system
|
||||
}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) formatPropertyType(propertyType api.PropertyType) string {
|
||||
if len(propertyType) == 1 {
|
||||
return propertyType[0]
|
||||
}
|
||||
quoted := make([]string, 0, len(propertyType))
|
||||
for _, v := range propertyType {
|
||||
quoted = append(quoted, "'"+v+"'")
|
||||
}
|
||||
return "[" + strings.Join(quoted, ", ") + "]"
|
||||
}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) renderToolPropertyExtraKeys(sb *strings.Builder, prop api.ToolProperty) {
|
||||
if len(prop.AnyOf) > 0 {
|
||||
sb.WriteString("\n<anyOf>" + r.pythonJSON(prop.AnyOf) + "</anyOf>")
|
||||
}
|
||||
if prop.Items != nil {
|
||||
sb.WriteString("\n<items>" + r.pythonJSON(prop.Items) + "</items>")
|
||||
}
|
||||
if prop.Properties != nil {
|
||||
sb.WriteString("\n<properties>" + r.pythonJSON(prop.Properties) + "</properties>")
|
||||
}
|
||||
if len(prop.Required) > 0 {
|
||||
sb.WriteString("\n<required>" + r.pythonJSON(prop.Required) + "</required>")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) renderToolParameterExtraKeys(sb *strings.Builder, params api.ToolFunctionParameters) {
|
||||
if params.Defs != nil {
|
||||
sb.WriteString("\n<$defs>" + r.pythonJSON(params.Defs) + "</$defs>")
|
||||
}
|
||||
if params.Items != nil {
|
||||
sb.WriteString("\n<items>" + r.pythonJSON(params.Items) + "</items>")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Nemotron3NanoRenderer) pythonJSON(v any) string {
|
||||
switch value := v.(type) {
|
||||
case nil:
|
||||
return "null"
|
||||
case string:
|
||||
return strconv.Quote(value)
|
||||
case bool:
|
||||
if value {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case int, int8, int16, int32, int64:
|
||||
return fmt.Sprintf("%d", reflect.ValueOf(value).Int())
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return fmt.Sprintf("%d", reflect.ValueOf(value).Uint())
|
||||
case float32, float64:
|
||||
b, _ := json.Marshal(value)
|
||||
return string(b)
|
||||
case api.PropertyType:
|
||||
return r.pythonJSON([]string(value))
|
||||
case []string:
|
||||
parts := make([]string, 0, len(value))
|
||||
for _, item := range value {
|
||||
parts = append(parts, r.pythonJSON(item))
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case []any:
|
||||
parts := make([]string, 0, len(value))
|
||||
for _, item := range value {
|
||||
parts = append(parts, r.pythonJSON(item))
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case []api.ToolProperty:
|
||||
parts := make([]string, 0, len(value))
|
||||
for _, item := range value {
|
||||
parts = append(parts, r.pythonJSON(item))
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case map[string]any:
|
||||
keys := make([]string, 0, len(value))
|
||||
for key := range value {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
parts = append(parts, strconv.Quote(key)+": "+r.pythonJSON(value[key]))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
case *api.ToolPropertiesMap:
|
||||
if value == nil {
|
||||
return "null"
|
||||
}
|
||||
parts := make([]string, 0, value.Len())
|
||||
for key, prop := range value.All() {
|
||||
parts = append(parts, strconv.Quote(key)+": "+r.pythonJSON(prop))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
case api.ToolProperty:
|
||||
parts := make([]string, 0, 6)
|
||||
if len(value.AnyOf) > 0 {
|
||||
parts = append(parts, `"anyOf": `+r.pythonJSON(value.AnyOf))
|
||||
}
|
||||
if len(value.Type) > 0 {
|
||||
if len(value.Type) == 1 {
|
||||
parts = append(parts, `"type": `+r.pythonJSON(value.Type[0]))
|
||||
} else {
|
||||
parts = append(parts, `"type": `+r.pythonJSON([]string(value.Type)))
|
||||
}
|
||||
}
|
||||
if value.Items != nil {
|
||||
parts = append(parts, `"items": `+r.pythonJSON(value.Items))
|
||||
}
|
||||
if value.Description != "" {
|
||||
parts = append(parts, `"description": `+r.pythonJSON(value.Description))
|
||||
}
|
||||
if len(value.Enum) > 0 {
|
||||
parts = append(parts, `"enum": `+r.pythonJSON(value.Enum))
|
||||
}
|
||||
if value.Properties != nil {
|
||||
parts = append(parts, `"properties": `+r.pythonJSON(value.Properties))
|
||||
}
|
||||
if len(value.Required) > 0 {
|
||||
parts = append(parts, `"required": `+r.pythonJSON(value.Required))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
default:
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return "null"
|
||||
}
|
||||
var generic any
|
||||
if err := json.Unmarshal(b, &generic); err != nil {
|
||||
return string(b)
|
||||
}
|
||||
return r.pythonJSON(generic)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user