mirror of
https://github.com/ollama/ollama.git
synced 2026-01-29 07:12:03 +03:00
* preserve tool definition and call JSON ordering This is another iteration of <https://github.com/ollama/ollama/pull/12518>, but this time we've simplified things by relaxing the competing requirements of being compatible AND order-preserving with templates (vs. renderers). We maintain backwards compatibility at the cost of not guaranteeing order for templates. We plan on moving more and more models to renderers, which have been updated to use these new data types, and additionally we could add an opt-in way of templates getting an order-preserved list (e.g., via sibling template vars) * orderedmap_test: remove testify
625 lines
14 KiB
Go
625 lines
14 KiB
Go
package template
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"maps"
|
|
"math"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"text/template"
|
|
"text/template/parse"
|
|
"time"
|
|
|
|
"github.com/agnivade/levenshtein"
|
|
|
|
"github.com/ollama/ollama/api"
|
|
)
|
|
|
|
//go:embed index.json
|
|
var indexBytes []byte
|
|
|
|
//go:embed *.gotmpl
|
|
//go:embed *.json
|
|
var templatesFS embed.FS
|
|
|
|
var templatesOnce = sync.OnceValues(func() ([]*named, error) {
|
|
var templates []*named
|
|
if err := json.Unmarshal(indexBytes, &templates); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, t := range templates {
|
|
bts, err := templatesFS.ReadFile(t.Name + ".gotmpl")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// normalize line endings
|
|
t.Bytes = bytes.ReplaceAll(bts, []byte("\r\n"), []byte("\n"))
|
|
|
|
params, err := templatesFS.ReadFile(t.Name + ".json")
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if err := json.Unmarshal(params, &t.Parameters); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return templates, nil
|
|
})
|
|
|
|
type named struct {
|
|
Name string `json:"name"`
|
|
Template string `json:"template"`
|
|
Bytes []byte
|
|
|
|
Parameters *struct {
|
|
Stop []string `json:"stop"`
|
|
}
|
|
}
|
|
|
|
func (t named) Reader() io.Reader {
|
|
return bytes.NewReader(t.Bytes)
|
|
}
|
|
|
|
func Named(s string) (*named, error) {
|
|
templates, err := templatesOnce()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var template *named
|
|
score := math.MaxInt
|
|
for _, t := range templates {
|
|
if s := levenshtein.ComputeDistance(s, t.Template); s < score {
|
|
score = s
|
|
template = t
|
|
}
|
|
}
|
|
|
|
if score < 100 {
|
|
return template, nil
|
|
}
|
|
|
|
return nil, errors.New("no matching template found")
|
|
}
|
|
|
|
var DefaultTemplate, _ = Parse("{{ .Prompt }}")
|
|
|
|
type Template struct {
|
|
*template.Template
|
|
raw string
|
|
}
|
|
|
|
// response is a template node that can be added to templates that don't already have one
|
|
var response = parse.ActionNode{
|
|
NodeType: parse.NodeAction,
|
|
Pipe: &parse.PipeNode{
|
|
NodeType: parse.NodePipe,
|
|
Cmds: []*parse.CommandNode{
|
|
{
|
|
NodeType: parse.NodeCommand,
|
|
Args: []parse.Node{
|
|
&parse.FieldNode{
|
|
NodeType: parse.NodeField,
|
|
Ident: []string{"Response"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
var funcs = template.FuncMap{
|
|
"json": func(v any) string {
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
},
|
|
"currentDate": func(args ...string) string {
|
|
// Currently ignoring the format argument, but accepting it for future use
|
|
// Default format is YYYY-MM-DD
|
|
return time.Now().Format("2006-01-02")
|
|
},
|
|
"yesterdayDate": func(args ...string) string {
|
|
return time.Now().AddDate(0, 0, -1).Format("2006-01-02")
|
|
},
|
|
"toTypeScriptType": func(v any) string {
|
|
if param, ok := v.(api.ToolProperty); ok {
|
|
return param.ToTypeScriptType()
|
|
}
|
|
// Handle pointer case
|
|
if param, ok := v.(*api.ToolProperty); ok && param != nil {
|
|
return param.ToTypeScriptType()
|
|
}
|
|
return "any"
|
|
},
|
|
}
|
|
|
|
func Parse(s string) (*Template, error) {
|
|
tmpl := template.New("").Option("missingkey=zero").Funcs(funcs)
|
|
|
|
tmpl, err := tmpl.Parse(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
t := Template{Template: tmpl, raw: s}
|
|
vars, err := t.Vars()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !slices.Contains(vars, "messages") && !slices.Contains(vars, "response") {
|
|
// touch up the template and append {{ .Response }}
|
|
tmpl.Tree.Root.Nodes = append(tmpl.Tree.Root.Nodes, &response)
|
|
}
|
|
|
|
return &t, nil
|
|
}
|
|
|
|
func (t *Template) String() string {
|
|
return t.raw
|
|
}
|
|
|
|
func (t *Template) Vars() ([]string, error) {
|
|
var vars []string
|
|
for _, tt := range t.Templates() {
|
|
for _, n := range tt.Root.Nodes {
|
|
v, err := Identifiers(n)
|
|
if err != nil {
|
|
return vars, err
|
|
}
|
|
vars = append(vars, v...)
|
|
}
|
|
}
|
|
|
|
set := make(map[string]struct{})
|
|
for _, n := range vars {
|
|
set[strings.ToLower(n)] = struct{}{}
|
|
}
|
|
|
|
return slices.Sorted(maps.Keys(set)), nil
|
|
}
|
|
|
|
func (t *Template) Contains(s string) bool {
|
|
return strings.Contains(t.raw, s)
|
|
}
|
|
|
|
type Values struct {
|
|
Messages []api.Message
|
|
api.Tools
|
|
Prompt string
|
|
Suffix string
|
|
Think bool
|
|
// ThinkLevel contains the thinking level if Think is true and a string value was provided
|
|
ThinkLevel string
|
|
// whether or not the user explicitly set the thinking flag (vs. it being
|
|
// implicitly false). Templates can't see whether `Think` is nil
|
|
IsThinkSet bool
|
|
|
|
// forceLegacy is a flag used to test compatibility with legacy templates
|
|
forceLegacy bool
|
|
}
|
|
|
|
func (t *Template) Subtree(fn func(parse.Node) bool) *template.Template {
|
|
var walk func(parse.Node) parse.Node
|
|
walk = func(n parse.Node) parse.Node {
|
|
if fn(n) {
|
|
return n
|
|
}
|
|
|
|
switch t := n.(type) {
|
|
case *parse.ListNode:
|
|
for _, c := range t.Nodes {
|
|
if n := walk(c); n != nil {
|
|
return n
|
|
}
|
|
}
|
|
case *parse.BranchNode:
|
|
for _, n := range []*parse.ListNode{t.List, t.ElseList} {
|
|
if n != nil {
|
|
if n := walk(n); n != nil {
|
|
return n
|
|
}
|
|
}
|
|
}
|
|
case *parse.IfNode:
|
|
return walk(&t.BranchNode)
|
|
case *parse.WithNode:
|
|
return walk(&t.BranchNode)
|
|
case *parse.RangeNode:
|
|
return walk(&t.BranchNode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if n := walk(t.Tree.Root); n != nil {
|
|
return (&template.Template{
|
|
Tree: &parse.Tree{
|
|
Root: &parse.ListNode{
|
|
Nodes: []parse.Node{n},
|
|
},
|
|
},
|
|
}).Funcs(funcs)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *Template) Execute(w io.Writer, v Values) error {
|
|
system, messages := collate(v.Messages)
|
|
vars, err := t.Vars()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if v.Prompt != "" && v.Suffix != "" {
|
|
return t.Template.Execute(w, map[string]any{
|
|
"Prompt": v.Prompt,
|
|
"Suffix": v.Suffix,
|
|
"Response": "",
|
|
"Think": v.Think,
|
|
"ThinkLevel": v.ThinkLevel,
|
|
"IsThinkSet": v.IsThinkSet,
|
|
})
|
|
} else if !v.forceLegacy && slices.Contains(vars, "messages") {
|
|
return t.Template.Execute(w, map[string]any{
|
|
"System": system,
|
|
"Messages": convertMessagesForTemplate(messages),
|
|
"Tools": convertToolsForTemplate(v.Tools),
|
|
"Response": "",
|
|
"Think": v.Think,
|
|
"ThinkLevel": v.ThinkLevel,
|
|
"IsThinkSet": v.IsThinkSet,
|
|
})
|
|
}
|
|
|
|
system = ""
|
|
var b bytes.Buffer
|
|
var prompt, response string
|
|
for _, m := range messages {
|
|
execute := func() error {
|
|
if err := t.Template.Execute(&b, map[string]any{
|
|
"System": system,
|
|
"Prompt": prompt,
|
|
"Response": response,
|
|
"Think": v.Think,
|
|
"ThinkLevel": v.ThinkLevel,
|
|
"IsThinkSet": v.IsThinkSet,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
system = ""
|
|
prompt = ""
|
|
response = ""
|
|
return nil
|
|
}
|
|
|
|
switch m.Role {
|
|
case "system":
|
|
if prompt != "" || response != "" {
|
|
if err := execute(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
system = m.Content
|
|
case "user":
|
|
if response != "" {
|
|
if err := execute(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
prompt = m.Content
|
|
case "assistant":
|
|
response = m.Content
|
|
}
|
|
}
|
|
|
|
var cut bool
|
|
nodes := deleteNode(t.Template.Root.Copy(), func(n parse.Node) bool {
|
|
if field, ok := n.(*parse.FieldNode); ok && slices.Contains(field.Ident, "Response") {
|
|
cut = true
|
|
return false
|
|
}
|
|
|
|
return cut
|
|
})
|
|
|
|
tree := parse.Tree{Root: nodes.(*parse.ListNode)}
|
|
if err := template.Must(template.New("").AddParseTree("", &tree)).Execute(&b, map[string]any{
|
|
"System": system,
|
|
"Prompt": prompt,
|
|
"Response": response,
|
|
"Think": v.Think,
|
|
"ThinkLevel": v.ThinkLevel,
|
|
"IsThinkSet": v.IsThinkSet,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = io.Copy(w, &b)
|
|
return err
|
|
}
|
|
|
|
// collate messages based on role. consecutive messages of the same role are merged
|
|
// into a single message (except for tool messages which preserve individual metadata).
|
|
// collate also collects and returns all system messages.
|
|
// collate mutates message content adding image tags ([img-%d]) as needed
|
|
// todo(parthsareen): revisit for contextual image support
|
|
func collate(msgs []api.Message) (string, []*api.Message) {
|
|
var system []string
|
|
var collated []*api.Message
|
|
for i := range msgs {
|
|
if msgs[i].Role == "system" {
|
|
system = append(system, msgs[i].Content)
|
|
}
|
|
|
|
// merges consecutive messages of the same role into a single message (except for tool messages)
|
|
if len(collated) > 0 && collated[len(collated)-1].Role == msgs[i].Role && msgs[i].Role != "tool" {
|
|
collated[len(collated)-1].Content += "\n\n" + msgs[i].Content
|
|
} else {
|
|
collated = append(collated, &msgs[i])
|
|
}
|
|
}
|
|
|
|
return strings.Join(system, "\n\n"), collated
|
|
}
|
|
|
|
// templateTools is a slice of templateTool that marshals to JSON.
|
|
type templateTools []templateTool
|
|
|
|
func (t templateTools) String() string {
|
|
bts, _ := json.Marshal(t)
|
|
return string(bts)
|
|
}
|
|
|
|
// templateTool is a template-compatible representation of api.Tool
|
|
// with Properties as a regular map for template ranging.
|
|
type templateTool struct {
|
|
Type string `json:"type"`
|
|
Items any `json:"items,omitempty"`
|
|
Function templateToolFunction `json:"function"`
|
|
}
|
|
|
|
type templateToolFunction struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Parameters templateToolFunctionParameters `json:"parameters"`
|
|
}
|
|
|
|
type templateToolFunctionParameters struct {
|
|
Type string `json:"type"`
|
|
Defs any `json:"$defs,omitempty"`
|
|
Items any `json:"items,omitempty"`
|
|
Required []string `json:"required,omitempty"`
|
|
Properties map[string]api.ToolProperty `json:"properties"`
|
|
}
|
|
|
|
// templateToolCall is a template-compatible representation of api.ToolCall
|
|
// with Arguments as a regular map for template ranging.
|
|
type templateToolCall struct {
|
|
ID string
|
|
Function templateToolCallFunction
|
|
}
|
|
|
|
type templateToolCallFunction struct {
|
|
Index int
|
|
Name string
|
|
Arguments map[string]any
|
|
}
|
|
|
|
// templateMessage is a template-compatible representation of api.Message
|
|
// with ToolCalls converted for template use.
|
|
type templateMessage struct {
|
|
Role string
|
|
Content string
|
|
Thinking string
|
|
Images []api.ImageData
|
|
ToolCalls []templateToolCall
|
|
ToolName string
|
|
ToolCallID string
|
|
}
|
|
|
|
// convertToolsForTemplate converts Tools to template-compatible format.
|
|
func convertToolsForTemplate(tools api.Tools) templateTools {
|
|
if tools == nil {
|
|
return nil
|
|
}
|
|
result := make(templateTools, len(tools))
|
|
for i, tool := range tools {
|
|
result[i] = templateTool{
|
|
Type: tool.Type,
|
|
Items: tool.Items,
|
|
Function: templateToolFunction{
|
|
Name: tool.Function.Name,
|
|
Description: tool.Function.Description,
|
|
Parameters: templateToolFunctionParameters{
|
|
Type: tool.Function.Parameters.Type,
|
|
Defs: tool.Function.Parameters.Defs,
|
|
Items: tool.Function.Parameters.Items,
|
|
Required: tool.Function.Parameters.Required,
|
|
Properties: tool.Function.Parameters.Properties.ToMap(),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// convertMessagesForTemplate converts Messages to template-compatible format.
|
|
func convertMessagesForTemplate(messages []*api.Message) []*templateMessage {
|
|
if messages == nil {
|
|
return nil
|
|
}
|
|
result := make([]*templateMessage, len(messages))
|
|
for i, msg := range messages {
|
|
var toolCalls []templateToolCall
|
|
for _, tc := range msg.ToolCalls {
|
|
toolCalls = append(toolCalls, templateToolCall{
|
|
ID: tc.ID,
|
|
Function: templateToolCallFunction{
|
|
Index: tc.Function.Index,
|
|
Name: tc.Function.Name,
|
|
Arguments: tc.Function.Arguments.ToMap(),
|
|
},
|
|
})
|
|
}
|
|
result[i] = &templateMessage{
|
|
Role: msg.Role,
|
|
Content: msg.Content,
|
|
Thinking: msg.Thinking,
|
|
Images: msg.Images,
|
|
ToolCalls: toolCalls,
|
|
ToolName: msg.ToolName,
|
|
ToolCallID: msg.ToolCallID,
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Identifiers walks the node tree returning any identifiers it finds along the way
|
|
func Identifiers(n parse.Node) ([]string, error) {
|
|
switch n := n.(type) {
|
|
case *parse.ListNode:
|
|
var names []string
|
|
for _, n := range n.Nodes {
|
|
i, err := Identifiers(n)
|
|
if err != nil {
|
|
return names, err
|
|
}
|
|
names = append(names, i...)
|
|
}
|
|
|
|
return names, nil
|
|
case *parse.TemplateNode:
|
|
if n.Pipe == nil {
|
|
return nil, errors.New("undefined template specified")
|
|
}
|
|
return Identifiers(n.Pipe)
|
|
case *parse.ActionNode:
|
|
if n.Pipe == nil {
|
|
return nil, errors.New("undefined action in template")
|
|
}
|
|
return Identifiers(n.Pipe)
|
|
case *parse.BranchNode:
|
|
if n.Pipe == nil {
|
|
return nil, errors.New("undefined branch")
|
|
}
|
|
names, err := Identifiers(n.Pipe)
|
|
if err != nil {
|
|
return names, err
|
|
}
|
|
for _, n := range []*parse.ListNode{n.List, n.ElseList} {
|
|
if n != nil {
|
|
i, err := Identifiers(n)
|
|
if err != nil {
|
|
return names, err
|
|
}
|
|
names = append(names, i...)
|
|
}
|
|
}
|
|
return names, nil
|
|
case *parse.IfNode:
|
|
return Identifiers(&n.BranchNode)
|
|
case *parse.RangeNode:
|
|
return Identifiers(&n.BranchNode)
|
|
case *parse.WithNode:
|
|
return Identifiers(&n.BranchNode)
|
|
case *parse.PipeNode:
|
|
var names []string
|
|
for _, c := range n.Cmds {
|
|
for _, a := range c.Args {
|
|
i, err := Identifiers(a)
|
|
if err != nil {
|
|
return names, err
|
|
}
|
|
names = append(names, i...)
|
|
}
|
|
}
|
|
return names, nil
|
|
case *parse.FieldNode:
|
|
return n.Ident, nil
|
|
case *parse.VariableNode:
|
|
return n.Ident, nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// deleteNode walks the node list and deletes nodes that match the predicate
|
|
// this is currently to remove the {{ .Response }} node from templates
|
|
func deleteNode(n parse.Node, fn func(parse.Node) bool) parse.Node {
|
|
var walk func(n parse.Node) parse.Node
|
|
walk = func(n parse.Node) parse.Node {
|
|
if fn(n) {
|
|
return nil
|
|
}
|
|
|
|
switch t := n.(type) {
|
|
case *parse.ListNode:
|
|
var nodes []parse.Node
|
|
for _, c := range t.Nodes {
|
|
if n := walk(c); n != nil {
|
|
nodes = append(nodes, n)
|
|
}
|
|
}
|
|
|
|
t.Nodes = nodes
|
|
return t
|
|
case *parse.IfNode:
|
|
t.BranchNode = *(walk(&t.BranchNode).(*parse.BranchNode))
|
|
case *parse.WithNode:
|
|
t.BranchNode = *(walk(&t.BranchNode).(*parse.BranchNode))
|
|
case *parse.RangeNode:
|
|
t.BranchNode = *(walk(&t.BranchNode).(*parse.BranchNode))
|
|
case *parse.BranchNode:
|
|
t.List = walk(t.List).(*parse.ListNode)
|
|
if t.ElseList != nil {
|
|
t.ElseList = walk(t.ElseList).(*parse.ListNode)
|
|
}
|
|
case *parse.ActionNode:
|
|
n := walk(t.Pipe)
|
|
if n == nil {
|
|
return nil
|
|
}
|
|
|
|
t.Pipe = n.(*parse.PipeNode)
|
|
case *parse.PipeNode:
|
|
var commands []*parse.CommandNode
|
|
for _, c := range t.Cmds {
|
|
var args []parse.Node
|
|
for _, a := range c.Args {
|
|
if n := walk(a); n != nil {
|
|
args = append(args, n)
|
|
}
|
|
}
|
|
|
|
if len(args) == 0 {
|
|
return nil
|
|
}
|
|
|
|
c.Args = args
|
|
commands = append(commands, c)
|
|
}
|
|
|
|
if len(commands) == 0 {
|
|
return nil
|
|
}
|
|
|
|
t.Cmds = commands
|
|
}
|
|
|
|
return n
|
|
}
|
|
|
|
return walk(n)
|
|
}
|