conversation

package
v0.4.24 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 28, 2024 License: MIT Imports: 12 Imported by: 13

README

Conversation Package

The conversation package provides a sophisticated system for managing complex conversation flows in LLM chatbots. It implements a tree-based conversation structure that supports branching dialogues, message threading, and various types of content.

Core Concepts

Message Structure

Messages are the fundamental units of conversation. Each message has:

  • ID: A unique identifier for the message
  • Role: The participant role (system, user, assistant, tool)
  • Content: The actual content of the message (chat text, tool calls, results, etc.)
  • Time: Timestamp of the message
  • Metadata: Additional information about the message
Tree-Based Conversation Model

Conversations are structured as trees, allowing for:

  • Linear Conversations: Simple back-and-forth dialogues
  • Branching Dialogues: Multiple conversation paths from a single point
  • Message Threading: Linking related messages in a conversation
  • Context Preservation: Maintaining conversation history and state
Message Content Types

The package supports multiple content types:

  1. ChatMessageContent: Standard text messages
  2. ToolCallContent: Requests for tool operations
  3. ToolResultContent: Results from tool executions
  4. ImageContent: Image data and metadata
  5. Custom Content: Extensible for additional content types

Key Components

1. ConversationTree

The core data structure managing message relationships:

type ConversationTree struct {
    Root   NodeID
    Nodes  map[NodeID]*Node
    Edges  map[NodeID][]NodeID
}

Key operations:

  • InsertMessages: Add new messages to the tree
  • GetMessageByID: Retrieve specific messages
  • GetConversationThread: Get a sequence of related messages
  • GetLeftMostThread: Get the primary conversation path
2. Manager

High-level interface for conversation operations:

type Manager interface {
    GetConversation() Conversation
    AppendMessages(msgs ...*Message)
    AttachMessages(parentID NodeID, msgs ...*Message)
    GetMessage(ID NodeID) (*Message, bool)
    SaveToFile(filename string) error
}

Features:

  • Message management and organization
  • Conversation state tracking
  • Template-based prompt handling
  • Persistence operations
3. Context

Handles conversation persistence and loading:

type Context interface {
    LoadFromFile(filename string) error
    SaveToFile(filename string) error
    GetConversation() Conversation
}

Implementation Guide

1. Setting Up a Conversation Manager
// Initialize with system prompt and configuration
manager := conversation.NewManager(
    conversation.WithManagerConversationID(uuid.New()),
)

// Add system prompt
manager.AppendMessages(conversation.NewChatMessage(
    conversation.RoleSystem,
    "System initialization prompt",
))
2. Handling User Messages
// Add user message
userMsg := conversation.NewChatMessage(
    conversation.RoleUser,
    "User input text",
)
manager.AppendMessages(userMsg)

// Get the current conversation state
conv := manager.GetConversation()
3. Managing Assistant Responses
// Add assistant response
response := conversation.NewChatMessage(
    conversation.RoleAssistant,
    "Assistant response",
)
manager.AppendMessages(response)

// Add tool calls if needed
toolCall := conversation.NewToolCallMessage(
    "tool_name",
    map[string]interface{}{"param": "value"},
)
manager.AppendMessages(toolCall)
4. Branching Conversations
// Create a new branch from an existing message
parentID := existingMessage.ID
newBranch := conversation.NewChatMessage(
    conversation.RoleAssistant,
    "Alternative response",
)
manager.AttachMessages(parentID, newBranch)
5. Persistence
// Save conversation state
err := manager.SaveToFile("conversation.json")
if err != nil {
    log.Fatal(err)
}

// Load existing conversation
loadedManager := conversation.NewManager()
err = loadedManager.LoadFromFile("conversation.json")

Best Practices

  1. Message Organization

    • Use appropriate roles for messages
    • Maintain clear parent-child relationships
    • Include relevant metadata
  2. State Management

    • Regularly save conversation state
    • Handle branching conversations carefully
    • Track conversation context
  3. Error Handling

    • Validate message content and structure
    • Handle loading/saving errors gracefully
    • Maintain conversation integrity
  4. Performance Considerations

    • Limit conversation tree depth
    • Implement cleanup for old branches
    • Optimize message content size

Extensions and Customization

Custom Content Types

Implement the MessageContent interface for new content types:

type CustomContent struct {
    Type    string
    Payload interface{}
}

func (c *CustomContent) ContentType() ContentType {
    return ContentTypeCustom
}

func (c *CustomContent) String() string {
    return fmt.Sprintf("%v", c.Payload)
}
Custom Manager Options

Add specialized behavior with manager options:

func WithCustomBehavior(config interface{}) conversation.ManagerOption {
    return func(m *conversation.ManagerImpl) {
        // Implement custom behavior
    }
}

Error Handling

Common errors and their handling:

  1. Invalid Message Structure

    if !message.IsValid() {
        return fmt.Errorf("invalid message structure: %v", message)
    }
    
  2. Tree Manipulation Errors

    if _, exists := tree.GetMessageByID(parentID); !exists {
        return fmt.Errorf("parent message not found: %v", parentID)
    }
    
  3. Persistence Errors

    if err := manager.SaveToFile(filename); err != nil {
        log.Printf("Failed to save conversation: %v", err)
        // Implement recovery strategy
    }
    

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ChatMessageContent

type ChatMessageContent struct {
	Role   Role            `json:"role"`
	Text   string          `json:"text"`
	Images []*ImageContent `json:"images"`
}

func (*ChatMessageContent) ContentType

func (c *ChatMessageContent) ContentType() ContentType

func (*ChatMessageContent) String

func (c *ChatMessageContent) String() string

func (*ChatMessageContent) View

func (c *ChatMessageContent) View() string

type ContentType

type ContentType string
const (
	ContentTypeChatMessage ContentType = "chat-message"
	// TODO(manuel, 2024-06-04) This needs to also handle tool call and tool response blocks (tool use block in claude API)
	// See also the comment to refactor this in openai/helpers.go, where tool use information is actually stored in the metadata of the message
	ContentTypeToolUse    ContentType = "tool-use"
	ContentTypeToolResult ContentType = "tool-result"
	ContentTypeImage      ContentType = "image"
)

TODO(manuel, 2024-07-04) Unify this with the events types that we added for the claude API

type Conversation

type Conversation []*Message

func (Conversation) GetSinglePrompt

func (messages Conversation) GetSinglePrompt() string

GetSinglePrompt concatenates all the messages together with a prompt in front. It just concatenates all the messages together with a prompt in front (if there are more than one message).

type ConversationTree

type ConversationTree struct {
	Nodes  map[NodeID]*Message
	RootID NodeID
	LastID NodeID
}

ConversationTree represents a tree-like structure for storing and managing conversation messages.

The tree consists of nodes (messages) connected by parent-child links. These relationships are done through the parent ID field in each message. The root node is the starting point of the conversation, and each node can have multiple children. The tree allows for traversing the conversation in various ways.

Node relationships are stored in the Message datastructure as `Children []*Message`.

Each node has a unique ID, and the tree keeps track of the root node ID and the last inserted node ID.

func NewConversationTree

func NewConversationTree() *ConversationTree

func (*ConversationTree) AppendMessages

func (ct *ConversationTree) AppendMessages(thread Conversation)

AppendMessages appends a conversation thread to the end of the tree. It attaches the thread to the last inserted node in the tree, making it the parent of the thread. The messages in the thread are inserted as nodes, extending the parent-child chain.

func (*ConversationTree) AttachThread

func (ct *ConversationTree) AttachThread(parentID NodeID, thread Conversation)

AttachThread attaches a conversation thread to a specified parent message. It updates the parent IDs of the messages in the thread to link them to the parent message. The last message in the thread becomes the new last inserted node ID.

func (*ConversationTree) FindChildren

func (ct *ConversationTree) FindChildren(id NodeID) []NodeID

FindChildren returns the IDs of all child messages for a given message ID.

func (*ConversationTree) FindSiblings

func (ct *ConversationTree) FindSiblings(id NodeID) []NodeID

FindSiblings returns the IDs of all sibling messages for a given message ID. Sibling messages are the nodes that share the same parent as the given message.

func (*ConversationTree) GetConversationThread

func (ct *ConversationTree) GetConversationThread(id NodeID) Conversation

GetConversationThread retrieves the linear conversation thread from root to the specified message.

func (*ConversationTree) GetLeftMostThread

func (ct *ConversationTree) GetLeftMostThread(id NodeID) Conversation

GetLeftMostThread returns the thread starting from a given message ID by always choosing the first child. It traverses the tree downwards, selecting the leftmost child at each level, until a leaf node is reached. The returned conversation is a linear sequence of messages from the given message to the leftmost leaf.

func (*ConversationTree) GetMessageByID

func (ct *ConversationTree) GetMessageByID(id NodeID) (*Message, bool)

func (*ConversationTree) InsertMessages

func (ct *ConversationTree) InsertMessages(msgs ...*Message)

InsertMessages adds new messages to the conversation tree. It updates the root ID if the tree is empty and sets the last inserted node ID. If a message has a parent ID that exists in the tree, it is added as a child of that parent node.

func (*ConversationTree) LoadFromFile

func (ct *ConversationTree) LoadFromFile(filename string) error

func (*ConversationTree) PrependThread

func (ct *ConversationTree) PrependThread(thread Conversation)

PrependThread prepends a conversation thread to the beginning of the tree. It updates the root ID to the first message in the thread and adjusts the parent-child relationships accordingly. The previous root node becomes a child of the new root node.

func (*ConversationTree) SaveToFile

func (ct *ConversationTree) SaveToFile(filename string) error

type ImageContent

type ImageContent struct {
	ImageURL     string      `json:"imageURL"`
	ImageContent []byte      `json:"imageContent"`
	ImageName    string      `json:"imageName"`
	MediaType    string      `json:"mediaType"`
	Detail       ImageDetail `json:"detail"`
}

func NewImageContentFromFile

func NewImageContentFromFile(path string) (*ImageContent, error)

func (*ImageContent) ContentType

func (i *ImageContent) ContentType() ContentType

func (*ImageContent) String

func (i *ImageContent) String() string

func (*ImageContent) View

func (i *ImageContent) View() string

type ImageDetail

type ImageDetail string
const (
	ImageDetailLow  ImageDetail = "low"
	ImageDetailHigh ImageDetail = "high"
	ImageDetailAuto ImageDetail = "auto"
)

type Manager

type Manager interface {
	GetConversation() Conversation
	AppendMessages(msgs ...*Message)
	AttachMessages(parentID NodeID, msgs ...*Message)
	GetMessage(ID NodeID) (*Message, bool)
	SaveToFile(filename string) error
}

Manager defines the interface for high-level conversation management operations.

type ManagerImpl

type ManagerImpl struct {
	Tree           *ConversationTree
	ConversationID uuid.UUID
	// contains filtered or unexported fields
}

func CreateManager

func CreateManager(
	systemPrompt string,
	prompt string,
	messages []*Message,
	params interface{},
	options ...ManagerOption,
) (*ManagerImpl, error)

CreateManager initializes a Manager implementation with system prompts, initial messages, and customizable options. It handles template rendering for prompts and messages.

NOTE(manuel, 2024-04-07) This currently seems to only be used by the codegen tests, while the main geppetto command uses NewManager. Unclear if this is just a legacy helper.

The systemPrompt and prompt templates are rendered using the params. Messages are also rendered using the params before being added to the manager.

ManagerOptions can be passed to further customize the manager on creation.

func NewManager

func NewManager(options ...ManagerOption) *ManagerImpl

func (*ManagerImpl) AppendMessages

func (c *ManagerImpl) AppendMessages(messages ...*Message)

func (*ManagerImpl) AttachMessages

func (c *ManagerImpl) AttachMessages(parentID NodeID, messages ...*Message)

func (*ManagerImpl) GetConversation

func (c *ManagerImpl) GetConversation() Conversation

func (*ManagerImpl) GetMessage

func (c *ManagerImpl) GetMessage(ID NodeID) (*Message, bool)

func (*ManagerImpl) PrependMessages

func (c *ManagerImpl) PrependMessages(messages ...*Message)

func (*ManagerImpl) SaveToFile

func (c *ManagerImpl) SaveToFile(s string) error

SaveToFile persists the current conversation state to a JSON file, enabling conversation continuity across sessions.

type ManagerOption

type ManagerOption func(*ManagerImpl)

func WithAutosave added in v0.4.24

func WithAutosave(enabled string, format string, dir string) ManagerOption

func WithManagerConversationID

func WithManagerConversationID(conversationID uuid.UUID) ManagerOption

func WithMessages

func WithMessages(messages ...*Message) ManagerOption

type Message

type Message struct {
	ParentID   NodeID    `json:"parentID"`
	ID         NodeID    `json:"id"`
	Time       time.Time `json:"time"`
	LastUpdate time.Time `json:"lastUpdate"`

	Content  MessageContent         `json:"content"`
	Metadata map[string]interface{} `json:"metadata"` // Flexible metadata field

	// TODO(manuel, 2024-04-07) Add Parent and Sibling lists
	// omit in json
	Children []*Message `json:"-"`
}

Message represents a single message node in the conversation tree.

func LoadFromFile

func LoadFromFile(filename string) ([]*Message, error)

LoadFromFile reads messages from a JSON or YAML file, facilitating conversation initialization from saved states.

func NewChatMessage

func NewChatMessage(role Role, text string, options ...MessageOption) *Message

func NewMessage

func NewMessage(content MessageContent, options ...MessageOption) *Message

func (*Message) MarshalJSON

func (mn *Message) MarshalJSON() ([]byte, error)

func (*Message) UnmarshalJSON

func (mn *Message) UnmarshalJSON(data []byte) error

UnmarshalJSON custom unmarshaler for Message.

type MessageContent

type MessageContent interface {
	ContentType() ContentType
	String() string
	View() string
}

MessageContent is an interface for different types of node content.

type MessageOption

type MessageOption func(*Message)

func WithID

func WithID(id NodeID) MessageOption

func WithMetadata

func WithMetadata(metadata map[string]interface{}) MessageOption

func WithParentID

func WithParentID(parentID NodeID) MessageOption

func WithTime

func WithTime(time time.Time) MessageOption

type NodeID

type NodeID uuid.UUID
var NullNode NodeID = NodeID(uuid.Nil)

func NewNodeID

func NewNodeID() NodeID

func (NodeID) MarshalJSON

func (id NodeID) MarshalJSON() ([]byte, error)

func (NodeID) String

func (id NodeID) String() string

func (*NodeID) UnmarshalJSON

func (id *NodeID) UnmarshalJSON(data []byte) error

type Role

type Role string
const (
	RoleSystem    Role = "system"
	RoleAssistant Role = "assistant"
	RoleUser      Role = "user"
	RoleTool      Role = "tool"
)

type ToolResultContent

type ToolResultContent struct {
	ToolID string `json:"toolID"`
	Result string `json:"result"`
}

func (*ToolResultContent) ContentType

func (t *ToolResultContent) ContentType() ContentType

func (*ToolResultContent) String

func (t *ToolResultContent) String() string

func (*ToolResultContent) View

func (t *ToolResultContent) View() string

type ToolUseContent

type ToolUseContent struct {
	ToolID string          `json:"toolID"`
	Name   string          `json:"name"`
	Input  json.RawMessage `json:"input"`
	// used by openai currently (only function)
	Type string `json:"type"`
}

func (*ToolUseContent) ContentType

func (t *ToolUseContent) ContentType() ContentType

func (*ToolUseContent) String

func (t *ToolUseContent) String() string

func (*ToolUseContent) View

func (t *ToolUseContent) View() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL