mirror of
https://github.com/jmorganca/ollama
synced 2025-10-07 09:12:48 +02:00
374 lines
7.8 KiB
Go
374 lines
7.8 KiB
Go
package tools
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/ollama/ollama/api"
|
|
)
|
|
|
|
type toolsState int
|
|
|
|
const (
|
|
toolsState_LookingForTag toolsState = iota
|
|
toolsState_ToolCalling
|
|
toolsState_Done
|
|
)
|
|
|
|
type Parser struct {
|
|
tag string
|
|
tools []api.Tool
|
|
|
|
state toolsState
|
|
buffer []byte
|
|
n int
|
|
}
|
|
|
|
func (p *Parser) GetBuffer() []byte {
|
|
return p.buffer
|
|
}
|
|
|
|
// NewParser creates a new tool call parser from a model's chat
|
|
// template and a list of provided tools.
|
|
func NewParser(tmpl *template.Template, tools []api.Tool) *Parser {
|
|
return NewParserWithTag(tools, parseTag(tmpl))
|
|
}
|
|
|
|
func NewParserWithTag(tools []api.Tool, tag string) *Parser {
|
|
return &Parser{
|
|
tag: tag,
|
|
tools: tools,
|
|
}
|
|
}
|
|
|
|
// Add processes a string input to parse tool calls and content that
|
|
// should be sent back to the user.
|
|
func (p *Parser) Add(s string) (calls []api.ToolCall, content string) {
|
|
if p.state == toolsState_Done {
|
|
return nil, s
|
|
}
|
|
|
|
p.buffer = append(p.buffer, s...)
|
|
|
|
if p.state == toolsState_LookingForTag {
|
|
i, found := p.findTag()
|
|
if i == -1 {
|
|
content = string(p.buffer)
|
|
p.buffer = []byte{}
|
|
} else {
|
|
content = string(p.buffer[:i])
|
|
p.buffer = p.buffer[i:]
|
|
}
|
|
|
|
// for models where { or [ are used as tool calling
|
|
// tags, we only support parsing tools if the first non-
|
|
// whitespace character is { or [
|
|
if p.tag == "{" || p.tag == "[" {
|
|
if strings.TrimSpace(content) != "" {
|
|
p.state = toolsState_Done
|
|
return nil, content + string(p.buffer)
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return nil, content
|
|
}
|
|
|
|
p.state = toolsState_ToolCalling
|
|
}
|
|
|
|
for {
|
|
call := p.parseToolCall()
|
|
if call == nil {
|
|
break
|
|
}
|
|
|
|
calls = append(calls, *call)
|
|
}
|
|
|
|
if p.done() {
|
|
p.state = toolsState_Done
|
|
content = string(p.buffer)
|
|
p.buffer = []byte{}
|
|
}
|
|
|
|
return calls, content
|
|
}
|
|
|
|
// findTag searches the buffer to find and handle a tool calling tag
|
|
// returning true if the tag was found and false otherwise, and
|
|
// a string content signaling any content that should be sent back to the user
|
|
func (p *Parser) findTag() (int, bool) {
|
|
// First check for complete substring anywhere in s
|
|
if i := bytes.Index(p.buffer, []byte(p.tag)); i > -1 {
|
|
return i, true
|
|
}
|
|
|
|
// Then check for partial suffix overlap
|
|
max := min(len(p.buffer), len(p.tag))
|
|
for i := max; i > 0; i-- {
|
|
if bytes.HasSuffix(p.buffer, []byte(p.tag[:i])) {
|
|
return len(p.buffer) - i, false
|
|
}
|
|
}
|
|
return -1, false
|
|
}
|
|
|
|
// parseToolCall finds the next complete tool call in the buffer
|
|
// incrementing n and advancing the buffer.
|
|
func (p *Parser) parseToolCall() *api.ToolCall {
|
|
tool, end := findTool(p.tools, p.buffer)
|
|
if tool == nil {
|
|
return nil
|
|
}
|
|
|
|
var args map[string]any
|
|
if found, i := findArguments(p.buffer); found == nil {
|
|
return nil
|
|
} else {
|
|
args = found
|
|
if i > end {
|
|
end = i
|
|
}
|
|
}
|
|
|
|
tc := &api.ToolCall{
|
|
Function: api.ToolCallFunction{
|
|
Name: tool.Function.Name,
|
|
Arguments: args,
|
|
Index: p.n,
|
|
},
|
|
}
|
|
|
|
p.n++
|
|
p.buffer = p.buffer[end:]
|
|
return tc
|
|
}
|
|
|
|
// findTool finds the first tool name in the list that matches the
|
|
// beginning of the buffer, returning nil if no tool is found
|
|
// or if the buffer ends with a partial tool name since we need
|
|
// to wait for more data to disambiguate.
|
|
// The second return value is the end position of the tool name
|
|
// if one is found, otherwise 0.
|
|
func findTool(tools []api.Tool, buf []byte) (*api.Tool, int) {
|
|
if len(buf) == 0 {
|
|
return nil, 0
|
|
}
|
|
|
|
// check if buffer ends with a partial tool name
|
|
// this prevents matching "get" when seeing "get_weather"
|
|
var longest string
|
|
for _, t := range tools {
|
|
if len(t.Function.Name) > len(longest) {
|
|
longest = t.Function.Name
|
|
}
|
|
}
|
|
|
|
// Only check up to longest characters from the end
|
|
for i := 1; i <= min(len(buf), len(longest)); i++ {
|
|
tail := buf[len(buf)-i:]
|
|
for _, t := range tools {
|
|
name := []byte(t.Function.Name)
|
|
if len(tail) < len(name) && bytes.HasPrefix(name, tail) {
|
|
return nil, 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// find first occurrence of the longest tool name
|
|
var found *api.Tool
|
|
start := -1
|
|
end := -1
|
|
|
|
for i := range tools {
|
|
name := []byte(tools[i].Function.Name)
|
|
pos := bytes.Index(buf, name)
|
|
if pos == -1 {
|
|
continue
|
|
}
|
|
|
|
// Skip if we have a better match already
|
|
if start != -1 {
|
|
if pos > start {
|
|
continue
|
|
}
|
|
if pos == start && len(name) <= len(found.Function.Name) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
found = &tools[i]
|
|
start = pos
|
|
end = pos + len(name)
|
|
}
|
|
|
|
if found != nil {
|
|
return found, end
|
|
}
|
|
|
|
return nil, 0
|
|
}
|
|
|
|
// findArguments returns the first object that appears to be
|
|
// arguments for the provided tool in the provided buffer,
|
|
// returning nil if no arguments are found and the end position
|
|
// TODO (jmorganca): this does not support parsing omitted arguments
|
|
// objects for functions that have all-optional parameters
|
|
// e.g. `{"name": "get_conditions", "arguments": {}}` will work but
|
|
// `{"name": "get_conditions"}` will not currently work
|
|
func findArguments(buffer []byte) (map[string]any, int) {
|
|
if len(buffer) == 0 {
|
|
return nil, 0
|
|
}
|
|
|
|
start := -1
|
|
var braces int
|
|
var inString, escaped bool
|
|
|
|
for i := range buffer {
|
|
c := buffer[i]
|
|
|
|
if escaped {
|
|
escaped = false
|
|
continue
|
|
}
|
|
|
|
if c == '\\' {
|
|
escaped = true
|
|
continue
|
|
}
|
|
|
|
if c == '"' {
|
|
inString = !inString
|
|
continue
|
|
}
|
|
|
|
if inString {
|
|
continue
|
|
}
|
|
|
|
if c == '{' {
|
|
if braces == 0 {
|
|
start = i
|
|
}
|
|
braces++
|
|
} else if c == '}' {
|
|
braces--
|
|
if braces == 0 && start != -1 {
|
|
object := buffer[start : i+1]
|
|
|
|
var data map[string]any
|
|
if err := json.Unmarshal(object, &data); err != nil {
|
|
// not a valid object, keep looking
|
|
start = -1
|
|
continue
|
|
}
|
|
|
|
var findObject func(obj map[string]any) (map[string]any, bool)
|
|
findObject = func(obj map[string]any) (map[string]any, bool) {
|
|
if _, hasName := obj["name"]; hasName {
|
|
if args, ok := obj["arguments"].(map[string]any); ok {
|
|
return args, true
|
|
}
|
|
if argsStr, ok := obj["arguments"].(string); ok {
|
|
var argsData map[string]interface{}
|
|
if err := json.Unmarshal([]byte(argsStr), &argsData); err == nil {
|
|
return argsData, ok
|
|
}
|
|
}
|
|
if args, ok := obj["parameters"].(map[string]any); ok {
|
|
return args, true
|
|
}
|
|
if argsStr, ok := obj["parameters"].(string); ok {
|
|
var argsData map[string]interface{}
|
|
if err := json.Unmarshal([]byte(argsStr), &argsData); err == nil {
|
|
return argsData, ok
|
|
}
|
|
}
|
|
return nil, true
|
|
}
|
|
|
|
for _, v := range obj {
|
|
switch child := v.(type) {
|
|
case map[string]any:
|
|
if result, found := findObject(child); found {
|
|
return result, true
|
|
}
|
|
case []any:
|
|
for _, item := range child {
|
|
if childObj, ok := item.(map[string]any); ok {
|
|
if result, found := findObject(childObj); found {
|
|
return result, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
if args, found := findObject(data); found {
|
|
return args, i
|
|
}
|
|
|
|
return data, i
|
|
}
|
|
|
|
if braces < 0 {
|
|
braces = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, 0
|
|
}
|
|
|
|
// done checks if the parser is done parsing by looking
|
|
// for closing tag. currently only } and ] are supported
|
|
// for closing tags as {} or [] pairs may not always
|
|
// represent tool calls and we need to send the content back
|
|
func (p *Parser) done() bool {
|
|
var open, close rune
|
|
switch p.tag {
|
|
case "{":
|
|
open, close = '{', '}'
|
|
case "[":
|
|
open, close = '[', ']'
|
|
default:
|
|
return false
|
|
}
|
|
|
|
var count int
|
|
for _, c := range p.buffer {
|
|
if c == byte(open) {
|
|
count++
|
|
} else if c == byte(close) {
|
|
count--
|
|
if count == 0 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Content returns any remaining content that
|
|
// should be sent to the user. This should be the empty string
|
|
// string unless the tag is { or [ and a tool call was not found
|
|
func (p *Parser) Content() string {
|
|
if p.n > 0 {
|
|
return ""
|
|
}
|
|
|
|
if p.tag == "{" || p.tag == "[" {
|
|
return string(p.buffer)
|
|
}
|
|
|
|
return ""
|
|
}
|