mirror of https://github.com/ollama/ollama.git
				
				
				
			
		
			
				
	
	
		
			331 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			331 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			Go
		
	
	
	
| package parser
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"bytes"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 
 | |
| 	"golang.org/x/text/encoding/unicode"
 | |
| 	"golang.org/x/text/transform"
 | |
| )
 | |
| 
 | |
| type File struct {
 | |
| 	Commands []Command
 | |
| }
 | |
| 
 | |
| func (f File) String() string {
 | |
| 	var sb strings.Builder
 | |
| 	for _, cmd := range f.Commands {
 | |
| 		fmt.Fprintln(&sb, cmd.String())
 | |
| 	}
 | |
| 
 | |
| 	return sb.String()
 | |
| }
 | |
| 
 | |
| type Command struct {
 | |
| 	Name string
 | |
| 	Args string
 | |
| }
 | |
| 
 | |
| func (c Command) String() string {
 | |
| 	var sb strings.Builder
 | |
| 	switch c.Name {
 | |
| 	case "model":
 | |
| 		fmt.Fprintf(&sb, "FROM %s", c.Args)
 | |
| 	case "license", "template", "system", "adapter":
 | |
| 		fmt.Fprintf(&sb, "%s %s", strings.ToUpper(c.Name), quote(c.Args))
 | |
| 	case "message":
 | |
| 		role, message, _ := strings.Cut(c.Args, ": ")
 | |
| 		fmt.Fprintf(&sb, "MESSAGE %s %s", role, quote(message))
 | |
| 	default:
 | |
| 		fmt.Fprintf(&sb, "PARAMETER %s %s", c.Name, quote(c.Args))
 | |
| 	}
 | |
| 
 | |
| 	return sb.String()
 | |
| }
 | |
| 
 | |
| type state int
 | |
| 
 | |
| const (
 | |
| 	stateNil state = iota
 | |
| 	stateName
 | |
| 	stateValue
 | |
| 	stateParameter
 | |
| 	stateMessage
 | |
| 	stateComment
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	errMissingFrom        = errors.New("no FROM line")
 | |
| 	errInvalidMessageRole = errors.New("message role must be one of \"system\", \"user\", or \"assistant\"")
 | |
| 	errInvalidCommand     = errors.New("command must be one of \"from\", \"license\", \"template\", \"system\", \"adapter\", \"parameter\", or \"message\"")
 | |
| )
 | |
| 
 | |
| type ParserError struct {
 | |
| 	LineNumber int
 | |
| 	Msg        string
 | |
| }
 | |
| 
 | |
| func (e *ParserError) Error() string {
 | |
| 	if e.LineNumber > 0 {
 | |
| 		return fmt.Sprintf("(line %d): %s", e.LineNumber, e.Msg)
 | |
| 	}
 | |
| 	return e.Msg
 | |
| }
 | |
| 
 | |
| func ParseFile(r io.Reader) (*File, error) {
 | |
| 	var cmd Command
 | |
| 	var curr state
 | |
| 	var currLine int = 1
 | |
| 	var b bytes.Buffer
 | |
| 	var role string
 | |
| 
 | |
| 	var f File
 | |
| 
 | |
| 	tr := unicode.BOMOverride(unicode.UTF8.NewDecoder())
 | |
| 	br := bufio.NewReader(transform.NewReader(r, tr))
 | |
| 
 | |
| 	for {
 | |
| 		r, _, err := br.ReadRune()
 | |
| 		if errors.Is(err, io.EOF) {
 | |
| 			break
 | |
| 		} else if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		if isNewline(r) {
 | |
| 			currLine++
 | |
| 		}
 | |
| 
 | |
| 		next, r, err := parseRuneForState(r, curr)
 | |
| 		if errors.Is(err, io.ErrUnexpectedEOF) {
 | |
| 			return nil, fmt.Errorf("%w: %s", err, b.String())
 | |
| 		} else if err != nil {
 | |
| 			return nil, &ParserError{
 | |
| 				LineNumber: currLine,
 | |
| 				Msg:        err.Error(),
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// process the state transition, some transitions need to be intercepted and redirected
 | |
| 		if next != curr {
 | |
| 			switch curr {
 | |
| 			case stateName:
 | |
| 				if !isValidCommand(b.String()) {
 | |
| 					return nil, &ParserError{
 | |
| 						LineNumber: currLine,
 | |
| 						Msg:        errInvalidCommand.Error(),
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				// next state sometimes depends on the current buffer value
 | |
| 				switch s := strings.ToLower(b.String()); s {
 | |
| 				case "from":
 | |
| 					cmd.Name = "model"
 | |
| 				case "parameter":
 | |
| 					// transition to stateParameter which sets command name
 | |
| 					next = stateParameter
 | |
| 				case "message":
 | |
| 					// transition to stateMessage which validates the message role
 | |
| 					next = stateMessage
 | |
| 					fallthrough
 | |
| 				default:
 | |
| 					cmd.Name = s
 | |
| 				}
 | |
| 			case stateParameter:
 | |
| 				cmd.Name = b.String()
 | |
| 			case stateMessage:
 | |
| 				if !isValidMessageRole(b.String()) {
 | |
| 					return nil, &ParserError{
 | |
| 						LineNumber: currLine,
 | |
| 						Msg:        errInvalidMessageRole.Error(),
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				role = b.String()
 | |
| 			case stateComment, stateNil:
 | |
| 				// pass
 | |
| 			case stateValue:
 | |
| 				s, ok := unquote(strings.TrimSpace(b.String()))
 | |
| 				if !ok || isSpace(r) {
 | |
| 					if _, err := b.WriteRune(r); err != nil {
 | |
| 						return nil, err
 | |
| 					}
 | |
| 
 | |
| 					continue
 | |
| 				}
 | |
| 
 | |
| 				if role != "" {
 | |
| 					s = role + ": " + s
 | |
| 					role = ""
 | |
| 				}
 | |
| 
 | |
| 				cmd.Args = s
 | |
| 				f.Commands = append(f.Commands, cmd)
 | |
| 			}
 | |
| 
 | |
| 			b.Reset()
 | |
| 			curr = next
 | |
| 		}
 | |
| 
 | |
| 		if strconv.IsPrint(r) {
 | |
| 			if _, err := b.WriteRune(r); err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// flush the buffer
 | |
| 	switch curr {
 | |
| 	case stateComment, stateNil:
 | |
| 		// pass; nothing to flush
 | |
| 	case stateValue:
 | |
| 		s, ok := unquote(strings.TrimSpace(b.String()))
 | |
| 		if !ok {
 | |
| 			return nil, io.ErrUnexpectedEOF
 | |
| 		}
 | |
| 
 | |
| 		if role != "" {
 | |
| 			s = role + ": " + s
 | |
| 		}
 | |
| 
 | |
| 		cmd.Args = s
 | |
| 		f.Commands = append(f.Commands, cmd)
 | |
| 	default:
 | |
| 		return nil, io.ErrUnexpectedEOF
 | |
| 	}
 | |
| 
 | |
| 	for _, cmd := range f.Commands {
 | |
| 		if cmd.Name == "model" {
 | |
| 			return &f, nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil, errMissingFrom
 | |
| }
 | |
| 
 | |
| func parseRuneForState(r rune, cs state) (state, rune, error) {
 | |
| 	switch cs {
 | |
| 	case stateNil:
 | |
| 		switch {
 | |
| 		case r == '#':
 | |
| 			return stateComment, 0, nil
 | |
| 		case isSpace(r), isNewline(r):
 | |
| 			return stateNil, 0, nil
 | |
| 		default:
 | |
| 			return stateName, r, nil
 | |
| 		}
 | |
| 	case stateName:
 | |
| 		switch {
 | |
| 		case isAlpha(r):
 | |
| 			return stateName, r, nil
 | |
| 		case isSpace(r):
 | |
| 			return stateValue, 0, nil
 | |
| 		default:
 | |
| 			return stateNil, 0, errInvalidCommand
 | |
| 		}
 | |
| 	case stateValue:
 | |
| 		switch {
 | |
| 		case isNewline(r):
 | |
| 			return stateNil, r, nil
 | |
| 		case isSpace(r):
 | |
| 			return stateNil, r, nil
 | |
| 		default:
 | |
| 			return stateValue, r, nil
 | |
| 		}
 | |
| 	case stateParameter:
 | |
| 		switch {
 | |
| 		case isAlpha(r), isNumber(r), r == '_':
 | |
| 			return stateParameter, r, nil
 | |
| 		case isSpace(r):
 | |
| 			return stateValue, 0, nil
 | |
| 		default:
 | |
| 			return stateNil, 0, io.ErrUnexpectedEOF
 | |
| 		}
 | |
| 	case stateMessage:
 | |
| 		switch {
 | |
| 		case isAlpha(r):
 | |
| 			return stateMessage, r, nil
 | |
| 		case isSpace(r):
 | |
| 			return stateValue, 0, nil
 | |
| 		default:
 | |
| 			return stateNil, 0, io.ErrUnexpectedEOF
 | |
| 		}
 | |
| 	case stateComment:
 | |
| 		switch {
 | |
| 		case isNewline(r):
 | |
| 			return stateNil, 0, nil
 | |
| 		default:
 | |
| 			return stateComment, 0, nil
 | |
| 		}
 | |
| 	default:
 | |
| 		return stateNil, 0, errors.New("")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func quote(s string) string {
 | |
| 	if strings.Contains(s, "\n") || strings.HasPrefix(s, " ") || strings.HasSuffix(s, " ") {
 | |
| 		if strings.Contains(s, "\"") {
 | |
| 			return `"""` + s + `"""`
 | |
| 		}
 | |
| 
 | |
| 		return `"` + s + `"`
 | |
| 	}
 | |
| 
 | |
| 	return s
 | |
| }
 | |
| 
 | |
| func unquote(s string) (string, bool) {
 | |
| 	// TODO: single quotes
 | |
| 	if len(s) >= 3 && s[:3] == `"""` {
 | |
| 		if len(s) >= 6 && s[len(s)-3:] == `"""` {
 | |
| 			return s[3 : len(s)-3], true
 | |
| 		}
 | |
| 
 | |
| 		return "", false
 | |
| 	}
 | |
| 
 | |
| 	if len(s) >= 1 && s[0] == '"' {
 | |
| 		if len(s) >= 2 && s[len(s)-1] == '"' {
 | |
| 			return s[1 : len(s)-1], true
 | |
| 		}
 | |
| 
 | |
| 		return "", false
 | |
| 	}
 | |
| 
 | |
| 	return s, true
 | |
| }
 | |
| 
 | |
| func isAlpha(r rune) bool {
 | |
| 	return r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z'
 | |
| }
 | |
| 
 | |
| func isNumber(r rune) bool {
 | |
| 	return r >= '0' && r <= '9'
 | |
| }
 | |
| 
 | |
| func isSpace(r rune) bool {
 | |
| 	return r == ' ' || r == '\t'
 | |
| }
 | |
| 
 | |
| func isNewline(r rune) bool {
 | |
| 	return r == '\r' || r == '\n'
 | |
| }
 | |
| 
 | |
| func isValidMessageRole(role string) bool {
 | |
| 	return role == "system" || role == "user" || role == "assistant"
 | |
| }
 | |
| 
 | |
| func isValidCommand(cmd string) bool {
 | |
| 	switch strings.ToLower(cmd) {
 | |
| 	case "from", "license", "template", "system", "adapter", "parameter", "message":
 | |
| 		return true
 | |
| 	default:
 | |
| 		return false
 | |
| 	}
 | |
| }
 |