Mistral's models seem to do something really strange if you allow for `tool_choice` to be anything but `any`. They start putting the tool call inside the `content` instead of an actual tool call. This means that I need this `stop` mechanism using a tool call instead because I cannot trust the model to do it by itself. I quite like this model though, it's cheap, it's fast and it's open source. And all the answers are pretty good!
240 lines
4.9 KiB
Go
240 lines
4.9 KiB
Go
package client
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
)
|
|
|
|
type Chat struct {
|
|
Messages []ChatMessage `json:"messages"`
|
|
}
|
|
|
|
type ChatMessage interface {
|
|
IsResponse() bool
|
|
}
|
|
|
|
// TODO: the role could be inferred from the type.
|
|
// This would solve some bugs.
|
|
|
|
/*
|
|
|
|
Is there a world where this actually becomes the product?
|
|
Where we build such a resilient system of AI calls that we
|
|
can build some app builder, or even just an API system,
|
|
with a fancy UI?
|
|
|
|
Manage all the complexity for the user?
|
|
|
|
*/
|
|
|
|
// =============================================
|
|
// Messages from us to the AI.
|
|
// =============================================
|
|
|
|
type UserRole = string
|
|
|
|
const (
|
|
User UserRole = "user"
|
|
System UserRole = "system"
|
|
)
|
|
|
|
type ToolRole = string
|
|
|
|
const (
|
|
Tool ToolRole = "tool"
|
|
)
|
|
|
|
type ChatUserMessage struct {
|
|
Role UserRole `json:"role"`
|
|
|
|
MessageContent `json:"MessageContent"`
|
|
}
|
|
|
|
func (m ChatUserMessage) MarshalJSON() ([]byte, error) {
|
|
switch t := m.MessageContent.(type) {
|
|
case SingleMessage:
|
|
return json.Marshal(&struct {
|
|
Role UserRole `json:"role"`
|
|
Content string `json:"content"`
|
|
}{
|
|
Role: User,
|
|
Content: t.Content,
|
|
})
|
|
case ArrayMessage:
|
|
return json.Marshal(&struct {
|
|
Role UserRole `json:"role"`
|
|
Content []MessageContentMessage `json:"content"`
|
|
}{
|
|
Role: User,
|
|
Content: t.Content,
|
|
})
|
|
}
|
|
|
|
return []byte{}, errors.New("Unreachable")
|
|
}
|
|
|
|
func (r ChatUserMessage) IsResponse() bool {
|
|
return false
|
|
}
|
|
|
|
type ChatUserToolResponse struct {
|
|
Role ToolRole `json:"role"`
|
|
|
|
// The name of the function we are responding to.
|
|
Name string `json:"name"`
|
|
Content string `json:"content"`
|
|
ToolCallId string `json:"tool_call_id"`
|
|
}
|
|
|
|
func (r ChatUserToolResponse) IsResponse() bool {
|
|
return false
|
|
}
|
|
|
|
type ChatAiMessage struct {
|
|
Role string `json:"role"`
|
|
ToolCalls *[]ToolCall `json:"tool_calls,omitempty"`
|
|
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
func (m ChatAiMessage) IsResponse() bool {
|
|
return true
|
|
}
|
|
|
|
// =============================================
|
|
// Unique interface for message content.
|
|
// =============================================
|
|
|
|
type MessageContent interface {
|
|
IsSingleMessage() bool
|
|
}
|
|
|
|
type SingleMessage struct {
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
func (m SingleMessage) IsSingleMessage() bool {
|
|
return true
|
|
}
|
|
|
|
type ArrayMessage struct {
|
|
Content []MessageContentMessage `json:"content"`
|
|
}
|
|
|
|
func (m ArrayMessage) IsSingleMessage() bool {
|
|
return false
|
|
}
|
|
|
|
type MessageContentMessage interface {
|
|
IsImageMessage() bool
|
|
}
|
|
|
|
type TextMessageContent struct {
|
|
TextType string `json:"type"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
func (m TextMessageContent) IsImageMessage() bool {
|
|
return false
|
|
}
|
|
|
|
type ImageMessageContent struct {
|
|
ImageType string `json:"type"`
|
|
ImageUrl string `json:"image_url"`
|
|
}
|
|
|
|
func (m ImageMessageContent) IsImageMessage() bool {
|
|
return true
|
|
}
|
|
|
|
type ImageContentUrl struct {
|
|
Url string `json:"url"`
|
|
}
|
|
|
|
// =============================================
|
|
// Adjacent interfaces.
|
|
// =============================================
|
|
|
|
type ToolCall struct {
|
|
Index int `json:"index"`
|
|
Id string `json:"id"`
|
|
Function FunctionCall `json:"function"`
|
|
}
|
|
|
|
type FunctionCall struct {
|
|
Name string `json:"name"`
|
|
Arguments string `json:"arguments"`
|
|
}
|
|
|
|
// =============================================
|
|
// Chat methods
|
|
// =============================================
|
|
|
|
func (chat *Chat) AddSystem(prompt string) {
|
|
chat.Messages = append(chat.Messages, ChatUserMessage{
|
|
Role: System,
|
|
MessageContent: SingleMessage{
|
|
Content: prompt,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (chat *Chat) AddImage(imageName string, image []byte, query *string) error {
|
|
extension := filepath.Ext(imageName)
|
|
if len(extension) == 0 {
|
|
// TODO: could also validate for image types we support.
|
|
return errors.New("Image does not have extension")
|
|
}
|
|
|
|
extension = extension[1:]
|
|
encodedString := base64.StdEncoding.EncodeToString(image)
|
|
|
|
contentLength := 1
|
|
if query != nil {
|
|
contentLength += 1
|
|
}
|
|
|
|
messageContent := ArrayMessage{
|
|
Content: make([]MessageContentMessage, contentLength),
|
|
}
|
|
|
|
index := 0
|
|
|
|
if query != nil {
|
|
messageContent.Content[index] = TextMessageContent{
|
|
TextType: "text",
|
|
Text: *query,
|
|
}
|
|
index += 1
|
|
}
|
|
|
|
messageContent.Content[index] = ImageMessageContent{
|
|
ImageType: "image_url",
|
|
ImageUrl: fmt.Sprintf("data:image/%s;base64,%s", extension, encodedString),
|
|
}
|
|
|
|
arrayMessage := ChatUserMessage{Role: User, MessageContent: messageContent}
|
|
chat.Messages = append(chat.Messages, arrayMessage)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (chat *Chat) AddAiResponse(res ChatAiMessage) {
|
|
chat.Messages = append(chat.Messages, res)
|
|
}
|
|
|
|
func (chat *Chat) AddToolResponse(res ChatUserToolResponse) {
|
|
chat.Messages = append(chat.Messages, res)
|
|
}
|
|
|
|
func (chat Chat) GetLatest() (ChatMessage, error) {
|
|
if len(chat.Messages) == 0 {
|
|
return nil, errors.New("Not enough messages")
|
|
}
|
|
|
|
return chat.Messages[len(chat.Messages)-1], nil
|
|
}
|