0e19476b56
instead of appending image tags, prepend them - this generally produces better results
221 lines
5.9 KiB
Go
221 lines
5.9 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"text/template"
|
|
"text/template/parse"
|
|
|
|
"github.com/jmorganca/ollama/api"
|
|
)
|
|
|
|
// isResponseNode checks if the node contains .Response
|
|
func isResponseNode(node *parse.ActionNode) bool {
|
|
for _, cmd := range node.Pipe.Cmds {
|
|
for _, arg := range cmd.Args {
|
|
if fieldNode, ok := arg.(*parse.FieldNode); ok && len(fieldNode.Ident) > 0 {
|
|
if fieldNode.Ident[0] == "Response" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// formatTemplateForResponse formats the template AST to:
|
|
// 1. remove all nodes after the first .Response (if generate=true)
|
|
// 2. add a .Response node to the end if it doesn't exist
|
|
// TODO(jmorganca): this should recursively cut the template before the first .Response
|
|
func formatTemplateForResponse(tmpl *template.Template, generate bool) {
|
|
var found bool
|
|
for i, node := range tmpl.Tree.Root.Nodes {
|
|
if actionNode, ok := node.(*parse.ActionNode); ok {
|
|
if isResponseNode(actionNode) {
|
|
found = true
|
|
if generate {
|
|
tmpl.Tree.Root.Nodes = tmpl.Tree.Root.Nodes[:i+1]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
// add the response node if it doesn't exist
|
|
responseFieldNode := &parse.FieldNode{NodeType: parse.NodeField, Ident: []string{"Response"}}
|
|
responsePipeNode := &parse.PipeNode{NodeType: parse.NodePipe, Cmds: []*parse.CommandNode{{NodeType: parse.NodeCommand, Args: []parse.Node{responseFieldNode}}}}
|
|
responseActionNode := &parse.ActionNode{NodeType: parse.NodeAction, Pipe: responsePipeNode}
|
|
tmpl.Tree.Root.Nodes = append(tmpl.Tree.Root.Nodes, responseActionNode)
|
|
}
|
|
}
|
|
|
|
// Prompt renders a prompt from a template. If generate is set to true,
|
|
// the response and parts of the template following it are not rendered
|
|
func Prompt(tmpl, system, prompt, response string, generate bool) (string, error) {
|
|
parsed, err := template.New("").Option("missingkey=zero").Parse(tmpl)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
formatTemplateForResponse(parsed, generate)
|
|
|
|
vars := map[string]any{
|
|
"System": system,
|
|
"Prompt": prompt,
|
|
"Response": response,
|
|
}
|
|
|
|
var sb strings.Builder
|
|
if err := parsed.Execute(&sb, vars); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return sb.String(), nil
|
|
}
|
|
|
|
func countTokens(tmpl string, system string, prompt string, response string, encode func(string) ([]int, error)) (int, error) {
|
|
rendered, err := Prompt(tmpl, system, prompt, response, false)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
tokens, err := encode(rendered)
|
|
if err != nil {
|
|
slog.Error("failed to encode prompt", "err", err)
|
|
return 0, err
|
|
}
|
|
|
|
return len(tokens), err
|
|
}
|
|
|
|
// ChatPrompt builds up a prompt from a series of messages, truncating based on context window size
|
|
func ChatPrompt(tmpl string, messages []api.Message, window int, encode func(string) ([]int, error)) (string, error) {
|
|
type prompt struct {
|
|
System string
|
|
Prompt string
|
|
Response string
|
|
|
|
images []int
|
|
tokens int
|
|
}
|
|
|
|
var p prompt
|
|
|
|
// iterate through messages to build up {system,user,response} prompts
|
|
var imgId int
|
|
var prompts []prompt
|
|
for _, msg := range messages {
|
|
switch strings.ToLower(msg.Role) {
|
|
case "system":
|
|
if p.System != "" || p.Prompt != "" || p.Response != "" {
|
|
prompts = append(prompts, p)
|
|
p = prompt{}
|
|
}
|
|
|
|
p.System = msg.Content
|
|
case "user":
|
|
if p.Prompt != "" || p.Response != "" {
|
|
prompts = append(prompts, p)
|
|
p = prompt{}
|
|
}
|
|
|
|
var sb strings.Builder
|
|
for range msg.Images {
|
|
fmt.Fprintf(&sb, "[img-%d] ", imgId)
|
|
p.images = append(p.images, imgId)
|
|
imgId += 1
|
|
}
|
|
|
|
sb.WriteString(msg.Content)
|
|
p.Prompt = sb.String()
|
|
case "assistant":
|
|
if p.Response != "" {
|
|
prompts = append(prompts, p)
|
|
p = prompt{}
|
|
}
|
|
|
|
p.Response = msg.Content
|
|
default:
|
|
return "", fmt.Errorf("invalid role: %s, role must be one of [system, user, assistant]", msg.Role)
|
|
}
|
|
}
|
|
|
|
// add final prompt
|
|
if p.System != "" || p.Prompt != "" || p.Response != "" {
|
|
prompts = append(prompts, p)
|
|
}
|
|
|
|
// calculate token lengths for each prompt, estimating 768 tokens per images
|
|
for i, p := range prompts {
|
|
tokens, err := countTokens(tmpl, p.System, p.Prompt, p.Response, encode)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
prompts[i].tokens = tokens + len(prompts[i].images)*768
|
|
}
|
|
|
|
// truncate images and prompts starting from the beginning of the list
|
|
// until either one prompt remains or the total tokens fits the context window
|
|
// TODO (jmorganca): this doesn't account for the context window room required for the response
|
|
for {
|
|
var required int
|
|
for _, p := range prompts {
|
|
required += p.tokens
|
|
}
|
|
|
|
required += 1 // for bos token
|
|
|
|
if required <= window {
|
|
slog.Debug("prompt now fits in context window", "required", required, "window", window)
|
|
break
|
|
}
|
|
|
|
prompt := &prompts[0]
|
|
|
|
if len(prompt.images) > 1 {
|
|
img := prompt.images[0]
|
|
slog.Debug("prompt longer than context window, removing image", "id", img, "required", required, "window", window)
|
|
prompt.images = prompt.images[1:]
|
|
prompt.Prompt = strings.Replace(prompt.Prompt, fmt.Sprintf(" [img-%d]", img), "", 1)
|
|
prompt.tokens -= 768
|
|
continue
|
|
}
|
|
|
|
if len(prompts) > 1 {
|
|
slog.Debug("required tokens longer than context window, removing first prompt", "prompt", prompts[0].tokens, "required", required, "window", window)
|
|
system := prompt.System
|
|
prompts = prompts[1:]
|
|
|
|
if system != "" && prompts[0].System == "" {
|
|
prompts[0].System = system
|
|
|
|
tokens, err := countTokens(tmpl, prompts[0].System, prompts[0].Prompt, prompts[0].Response, encode)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
prompts[0].tokens = tokens + len(prompts[0].images)*768
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
// stop truncating if there's only one prompt left
|
|
break
|
|
}
|
|
|
|
var sb strings.Builder
|
|
for i, p := range prompts {
|
|
// last prompt should leave the response unrendered (for completion)
|
|
rendered, err := Prompt(tmpl, p.System, p.Prompt, p.Response, i == len(prompts)-1)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sb.WriteString(rendered)
|
|
}
|
|
|
|
return sb.String(), nil
|
|
}
|