250 lines
5.1 KiB
Go
250 lines
5.1 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 ImageMessageUrl `json:"image_url"`
|
|
}
|
|
|
|
type ImageMessageUrl struct {
|
|
Url string `json:"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"`
|
|
Type string `json:"type,omitzero"`
|
|
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")
|
|
// Hacky! It seems apple doesnt add extension.
|
|
// BIG TODO: take better metadata from the image.
|
|
extension = "png"
|
|
}
|
|
|
|
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: ImageMessageUrl{
|
|
Url: 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
|
|
}
|