minds

package module
v0.0.5 Latest Latest
Warning

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

Go to latest
Published: Jan 18, 2025 License: Apache-2.0 Imports: 15 Imported by: 7

README

Minds Toolkit

A lightweight Go library for building LLM-based applications through the composition of handlers, inspired by the http.Handler middleware pattern.

This toolkit takes inspiration from LangChain's runnables and addresses the need for a modular, extensible framework for conversational AI development in Go. By leveraging Go's idiomatic patterns, the library provides a composable middleware design tailored to processing message threads.

The framework applies the same handler-based design to both LLMs and tool integrations. It includes implementations for OpenAI and Google's Gemini, as well as a suite of tools in the minds/openai, minds/gemini, and minds/tools modules.

Features

  • Composable Middleware: Build complex pipelines for handling message threads with composable, reusable handlers.
  • Extensible Design: Add custom handlers and integrate external APIs with ease.
  • Integration Tools: Built-in support for LLM providers and tools for generative AI workflows.
  • Testing-Friendly: Well-structured interfaces and unit-tested components for robust development.

Installation

go get github.com/chriscow/minds

The Minds Toolkit is designed to minimize dependencies in your project. You can selectively include only the providers you need. For example, if you need the OpenAI or Gemini providers, you can install them separately:

go get github.com/chriscow/minds/openai
go get github.com/chriscow/minds/gemini

Similarly, tools are available as separate modules:

go get github.com/chriscow/minds/tools

Since the examples have many dependencies, you should:

cd _examples
go mod tidy
go run ./chat-completion-provider/

Usage

Basic Example

Here’s how you can compose handlers for processing a thread. This demonstrates a Joke Competition where two LLMs battle it out telling jokes to each other. We use the For handler to limit the number of rounds to 5. See the _examples/middleware-ratelimit example for the full code.

---
title: Joke Competition
config:
 look: handDrawn
---
flowchart TD
A[Initial Message] --> |HandleThread| FOR("`**For** _handler_`")
FOR --> C{i < 5}
C -->|False| Next[Next Handler]
C -->|True| D[Gemini]
D -->|Joke| F[OpenAI]
F -->|Joke 2| C
func main() {
	ctx := context.Background()
	geminiJoker, _ := gemini.NewProvider(ctx)
	openAIJoker, _ := openai.NewProvider()

	// Create a rate limiter that allows 1 request every 5 seconds
	limiter := NewRateLimiter("rate_limiter", 1, 5*time.Second)

	printJoke := minds.ThreadHandlerFunc(func(tc minds.ThreadContext, next minds.ThreadHandler) (minds.ThreadContext, error) {
		fmt.Printf("Joke: %s\n", tc.Messages().Last().Content)
		return tc, nil
	})

	// Create a cycle that alternates between both LLMs, each followed by printing the joke
	jokeExchange := handlers.For("joke_exchange", 5,
		geminiJoker,
		printJoke,
		openAIJoker,
		printJoke,
	)
	jokeExchange.Use(limiter)

	// Initial prompt
	prompt := "Tell me a clean, family-friendly joke. Keep it clean and make me laugh!"
	initialThread := minds.NewThreadContext(ctx).WithMessages(minds.Messages{
		{Role: minds.RoleUser, Content: prompt},
	})

	// Let them exchange jokes until context is canceled
	if _, err := jokeExchange.HandleThread(initialThread, nil); err != nil {
		log.Fatalf("Error in joke exchange: %v", err)
	}
}
Adding a Calculator Tool

The library supports Lua and Starlark as tools for LLMs to perform mathematical operations. Here's how to integrate a calculator:

func main() {
	calc, _ := calculator.NewCalculator(calculator.Starlark)
	req := minds.Request{
		Messages: minds.Messages{{Role: minds.RoleUser, Content: "calculate 3+7*4"}},
	}

	llm, _ := openai.NewProvider(openai.WithTool(calc))
	resp, _ := llm.GenerateContent(ctx, req)
	print(resp.Text())
}

Documentation

Refer to the _examples provided for guidance on how to use the modules.

Handler Examples

The Minds toolkit uses composable handlers that implement the ThreadHandler interface:

type ThreadHandler interface {
    HandleThread(ThreadContext, ThreadHandler) (ThreadContext, error) 
}

Handlers can include middleware through the Use() method, allowing for cross-cutting concerns like logging, rate limiting, or validation:

limiter := NewRateLimiter(1, 5*time.Second)
handler := Sequential("example",
    validateHandler,
    llmHandler,
)
handler.Use(limiter) // Apply rate limiting to all handlers in sequence

The core handler types include:

  • Sequential: Runs a set of handlers in order
  • For: Repeats a handler chain for a specified number of iterations
  • Must: Runs multiple handlers in parallel, requiring all to succeed
  • First: Executes handlers in parallel, using the first successful result
  • Range: Processes a sequence of values through a handler chain
  • Policy: Uses LLM to validate thread content against policies

The following examples demonstrate some common handler composition patterns...

Parallel Validation

All handlers will be executed in parallel and must all succeed otherwise an error is returned.

validate := handlers.Must("validation",
    handlers.NewFormatValidator(),      // you provide these handlers
    handlers.NewLengthValidator(1000),  // ...
    handlers.NewContentScanner(),       // ...
)
---
title: Validation in Parallel
config:
 look: handDrawn
---
graph TD
    B{Must}
    B -->|Parallel| C1[Validate Format]
    B -->|Parallel| C2[Check Length]
    B -->|Parallel| C3[Scan Content]
Fallback Processing

All handlers executed in parallel. First handler to succceed cancels all others.

gpt4 := openai.NewProvider()
claude := anthropic.NewProvider()
gemini := gemini.NewProvider()

// first successfull response cancels others
first := handlers.First("generate", gpt4, claude, gemini)
---
title: Fastest Result
config:
 look: handDrawn
---
graph TD
D{First}
D -->|Parallel| E1[Try GPT4]
D -->|Parallel| E2[Try Claude]
D -->|Parallel| E3[Try Gemini]
Iterative Processing

Use the For handler to iterate over handlers N-times or infinately. Provide a conditional function to break early.

llm, _ := openai.NewProvider()
const iterations = 3
process := handlers.For("process", iterations, 
    handlers.Summarize(llm, "Be concise"),
    llm,
)
---
title: Looping Over Handlers
config:
 look: handDrawn
---
graph LR
G[For] --> C{Condition?}
C -->|false| End((next))
C -->|true| H1[Summarize]
H1 --> H2[LLM]
H2 --> C
Conditional Processing

Use the Switch handler to route messages based on conditions. This example shows how to handle different types of user requests using metadata and LLM-based routing:

func main() {
    llm, _ := openai.NewProvider()
    
    // Create specialized handlers for different tasks
    calculator := tools.NewCalculator()
    questionHandler := handlers.NewQuestionHandler(llm)
    summaryHandler := handlers.NewSummaryHandler(llm)
    
    // Define conditions and their handlers
    intentSwitch := handlers.Switch("intent-router",
        handlers.NewDefaultHandler(llm), // fallback handler
        handlers.SwitchCase{
            // Use LLM to check if message is a math question
            Condition: handlers.LLMCondition{
                Generator: llm,
                Prompt:   "Does this message contain a mathematical calculation?",
            },
            Handler: calculator,
        },
        handlers.SwitchCase{
            // Check metadata for specific routing
            Condition: handlers.MetadataEquals{
                Key:   "type",
                Value: "question",
            },
            Handler: questionHandler,
        },
        handlers.SwitchCase{
            // Use Lua for complex condition
            Condition: extensions.LuaCondition{
                Script: `
                    -- Check if message is long and needs summarization
                    return string.len(last_message) > 500
                `,
            },
            Handler: summaryHandler,
        },
    )

    // Initial thread with metadata
    thread := minds.NewThreadContext(context.Background()).
        WithMessages(minds.Messages{
            {Role: minds.RoleUser, Content: "What is 7 * 12 + 5?"},
        }).
        WithMetadata(map[string]interface{}{
            "type": "calculation",
        })

    // Process the thread
    result, err := intentSwitch.HandleThread(thread, nil)
    if err != nil {
        log.Fatalf("Error processing thread: %v", err)
    }
    
    fmt.Println("Response:", result.Messages().Last().Content)
}
Batch Processing

Execute a handler for every value in a slice of values. Each valud is placed in metadata for access.

values := []string{"value1", "value2", "value3"}
process := handlers.Range("batch", processor, values)

Contributing

Contributions are welcome! Please see the CONTRIBUTING.md for guidelines.

License

This project is licensed under the Apache 2.0 License. See the LICENSE file for details.

Acknowledgements

This project is inspired by the http.Handler middleware pattern and the need for modular and extensible LLM application development in Go.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNoMessages = errors.New("no messages in thread")
)

Functions

func SavePromptTemplate

func SavePromptTemplate(filepath string, header PromptHeader, body string) error

func Validate

func Validate(schema Definition, data any) bool

func VerifySchemaAndUnmarshal

func VerifySchemaAndUnmarshal(schema Definition, content []byte, v any) error

Types

type CallableFunc

type CallableFunc func(context.Context, []byte) ([]byte, error)

type ContentGenerator

type ContentGenerator interface {
	ModelName() string
	GenerateContent(context.Context, Request) (Response, error)
	Close()
}

type DataType

type DataType string
const (
	Object  DataType = "object"
	Number  DataType = "number"
	Integer DataType = "integer"
	String  DataType = "string"
	Array   DataType = "array"
	Null    DataType = "null"
	Boolean DataType = "boolean"
)

type Definition

type Definition struct {
	// Type specifies the data type of the schema.
	Type DataType `json:"type,omitempty"`
	// Description is the description of the schema.
	Description string `json:"description,omitempty"`
	// Enum is used to restrict a value to a fixed set of values. It must be an array with at least
	// one element, where each element is unique. You will probably only use this with strings.
	Enum []string `json:"enum,omitempty"`
	// Properties describes the properties of an object, if the schema type is Object.
	Properties map[string]Definition `json:"properties,omitempty"`
	// Required specifies which properties are required, if the schema type is Object.
	Required []string `json:"required,omitempty"`
	// Items specifies which data type an array contains, if the schema type is Array.
	Items *Definition `json:"items,omitempty"`
	// AdditionalProperties is used to control the handling of properties in an object
	// that are not explicitly defined in the properties section of the schema. example:
	// additionalProperties: true
	// additionalProperties: false
	// additionalProperties: Definition{Type: String}
	AdditionalProperties any `json:"additionalProperties,omitempty"`
}

Definition is a struct for describing a JSON Schema. It is fairly limited, and you may have better luck using a third-party library.

func GenerateSchema

func GenerateSchema(v any) (*Definition, error)

func (*Definition) MarshalJSON

func (d *Definition) MarshalJSON() ([]byte, error)

func (*Definition) Unmarshal

func (d *Definition) Unmarshal(content string, v any) error

type Embedder

type Embedder interface {
	CreateEmbeddings(model string, input []string) ([][]float32, error)
}

type FunctionCall

type FunctionCall struct {
	Name        string `json:"name,omitempty"`
	Description string `json:"description,omitempty"`
	// call function with arguments in JSON format
	Parameters []byte `json:"parameters,omitempty"`
	Result     []byte `json:"result,omitempty"`
}

type KVStore

type KVStore interface {
	Save(ctx context.Context, key []byte, value []byte) error
	Load(ctx context.Context, key []byte) ([]byte, error)
}

type MergeStrategy added in v0.0.5

type MergeStrategy int

MergeStrategy defines how to handle metadata key conflicts

const (
	// KeepExisting keeps the existing value on conflict
	KeepExisting MergeStrategy = iota
	// KeepNew overwrites with the new value on conflict
	KeepNew
	// Combine attempts to combine values (slice/map/string)
	Combine
	// Skip ignores conflicting keys
	Skip
)

type Message

type Message struct {
	Role       Role       `json:"role"`
	Content    string     `json:"content"`
	Name       string     `json:"name,omitempty"`     // For function calls
	Metadata   Metadata   `json:"metadata,omitempty"` // For additional context
	ToolCallID string     `json:"tool_call_id,omitempty"`
	ToolCalls  []ToolCall `json:"func_response,omitempty"`
}

func (Message) TokenCount

func (m Message) TokenCount(tokenizer TokenCounter) (int, error)

type Messages

type Messages []Message

func (Messages) Copy

func (m Messages) Copy() Messages

Copy returns a deep copy of the messages

func (Messages) Exclude

func (m Messages) Exclude(roles ...Role) Messages

Exclude returns a new slice of Message with messages with the specified roles removed

func (Messages) Last

func (m Messages) Last() Message

Last returns the last message in the slice of messages. NOTE: This will return an empty message if there are no messages in the slice.

func (Messages) Only

func (m Messages) Only(roles ...Role) Messages

func (Messages) TokenCount

func (m Messages) TokenCount(tokenizer TokenCounter) (int, error)

type Metadata

type Metadata map[string]any

func (Metadata) Copy

func (m Metadata) Copy() Metadata

Copy creates a deep copy of the metadata

func (Metadata) Merge added in v0.0.5

func (m Metadata) Merge(other Metadata, strategy MergeStrategy) Metadata

Merge combines the current metadata with another, using the specified strategy

func (Metadata) MergeWithCustom added in v0.0.5

func (m Metadata) MergeWithCustom(other Metadata, strategy MergeStrategy,
	customMerge map[string]func(existing, new any) any) Metadata

MergeWithCustom combines metadata with custom handlers for specific keys

type NoopThreadHandler

type NoopThreadHandler struct{}

func (NoopThreadHandler) HandleThread

func (h NoopThreadHandler) HandleThread(tc ThreadContext, next ThreadHandler) (ThreadContext, error)

type Prompt

type Prompt struct {
	Header   PromptHeader
	Template *template.Template
}

func CreateTemplate

func CreateTemplate(fs embed.FS, filepath string) (Prompt, error)

func (Prompt) Execute

func (p Prompt) Execute(data interface{}) (string, error)

type PromptHeader

type PromptHeader struct {
	Name    string
	Version string
	Format  string
	SHA256  string
}

type Request

type Request struct {
	Options  RequestOptions
	Messages Messages `json:"messages"`
}

func NewRequest

func NewRequest(messages Messages, opts ...RequestOption) Request

func (Request) TokenCount

func (r Request) TokenCount(tokenizer TokenCounter) (int, error)

type RequestOption

type RequestOption func(*RequestOptions)

func WithMaxOutputTokens

func WithMaxOutputTokens(tokens int) RequestOption

func WithModel

func WithModel(model string) RequestOption

func WithResponseSchema

func WithResponseSchema(schema ResponseSchema) RequestOption

func WithTemperature

func WithTemperature(temperature float32) RequestOption

type RequestOptions

type RequestOptions struct {
	ModelName       *string
	Temperature     *float32
	MaxOutputTokens *int
	ResponseSchema  *ResponseSchema
	ToolRegistry    ToolRegistry
	ToolChoice      string
}

type Response

type Response interface {
	// String returns a string representation of the response
	String() string

	// ToolCall returns the tool call details if this is a tool call response.
	ToolCalls() []ToolCall
}

type ResponseHandler

type ResponseHandler func(resp Response) error

func (ResponseHandler) HandleResponse

func (h ResponseHandler) HandleResponse(resp Response) error

type ResponseSchema

type ResponseSchema struct {
	Name        string     `json:"name"`
	Description string     `json:"description"`
	Definition  Definition `json:"schema"`
}

func NewResponseSchema

func NewResponseSchema(name, desc string, v any) (*ResponseSchema, error)

type ResponseType

type ResponseType int

ResponseType indicates what kind of response we received

const (
	ResponseTypeUnknown ResponseType = iota
	ResponseTypeText
	ResponseTypeToolCall
)

type Role

type Role string
const (
	RoleUser      Role = "user"
	RoleAssistant Role = "assistant"
	RoleSystem    Role = "system"
	RoleFunction  Role = "function"
	RoleTool      Role = "tool"
	RoleAI        Role = "ai"
	RoleModel     Role = "model"
)

type ThreadContext

type ThreadContext interface {
	// Clone returns a deep copy of the ThreadContext.
	Clone() ThreadContext
	Context() context.Context
	UUID() string

	// Messages returns a copy of the messages in the context.
	Messages() Messages

	// Metadata returns a copy of the metadata in the context.
	Metadata() Metadata

	AppendMessages(message ...Message)

	// SetKeyValue sets a key-value pair in the metadata.
	SetKeyValue(key string, value any)

	// WithContext returns a new ThreadContext with the provided context.
	WithContext(ctx context.Context) ThreadContext

	// WithUUID returns a new ThreadContext with the provided UUID.
	WithUUID(uuid string) ThreadContext

	// WithMessages returns a new ThreadContext with the provided messages.
	WithMessages(message ...Message) ThreadContext

	// WithMetadata returns a new ThreadContext with the provided metadata.
	WithMetadata(metadata Metadata) ThreadContext
}

func NewThreadContext

func NewThreadContext(ctx context.Context) ThreadContext

type ThreadHandler

type ThreadHandler interface {
	HandleThread(thread ThreadContext, next ThreadHandler) (ThreadContext, error)
}

type ThreadHandlerFunc

type ThreadHandlerFunc func(thread ThreadContext, next ThreadHandler) (ThreadContext, error)

func (ThreadHandlerFunc) HandleThread

func (f ThreadHandlerFunc) HandleThread(thread ThreadContext, next ThreadHandler) (ThreadContext, error)

type TokenCounter

type TokenCounter interface {
	CountTokens(text string) (int, error)
}

TokenCounter defines how to count tokens for different models

type Tool

type Tool interface {
	Type() string
	Name() string
	Description() string
	Parameters() Definition
	Call(context.Context, []byte) ([]byte, error)
}

Tool is an interface for a tool that can be executed by an LLM. It is similar to a function in that it takes input and produces output, but it can be more complex than a simple function and doesn't require a wrapper.

func WrapFunction

func WrapFunction(name, description string, args interface{}, fn CallableFunc) (Tool, error)

type ToolCall

type ToolCall struct {
	ID       string       `json:"id,omitempty"`
	Type     string       `json:"type,omitempty"`
	Function FunctionCall `json:"function,omitempty"`
}

func HandleFunctionCalls

func HandleFunctionCalls(ctx context.Context, calls []ToolCall, registry ToolRegistry) ([]ToolCall, error)

HandleFunctionCalls takes an array of ToolCalls and executes the functions they represent using the provided ToolRegistry. It returns an array of ToolCalls with the results of the function calls.

type ToolRegistry

type ToolRegistry interface {
	// Register adds a new function to the registry
	Register(t Tool) error
	// Lookup retrieves a function by name
	Lookup(name string) (Tool, bool)
	// List returns all registered functions
	List() []Tool
}

func NewToolRegistry

func NewToolRegistry() ToolRegistry

type ToolType

type ToolType string
const (
	ToolTypeFunction ToolType = "function"
)

Directories

Path Synopsis
internal
providers
gemini module
openai module
tools module

Jump to

Keyboard shortcuts

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