minds

package module
v0.0.7 Latest Latest
Warning

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

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

README

Minds Toolkit

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

Minds Toolkit provides a modular and extensible framework for conversational AI development in Go. It leverages Go's idiomatic patterns to create a composable middleware design tailored for processing message threads. The framework supports both LLMs and tool integrations, with built-in implementations for OpenAI, Google's Gemini, and 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 reusable handlers.
  • Extensible Design: Easily add custom handlers and integrate external APIs.
  • 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. You can selectively include only the providers you need:

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

For tools, install the minds/tools module:

go get github.com/chriscow/minds/tools

To run examples:

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

Usage

Basic Example: Joke Competition

This example demonstrates a Joke Competition where two LLMs exchange jokes. The For handler limits the interaction to 5 rounds. 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()

	// Rate limiter: 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 "For" loop alternating between Gemini and OpenAI
	jokeExchange := handlers.For("joke_exchange", 5,
		geminiJoker,
		printJoke,
		openAIJoker,
		printJoke,
	)

    	flow := handlers.ThreadFlow("joke_competition")
	flow.Use(limiter)
    	flow.Handle(jokeExchange)

	// 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},
	})

	// Run the joke exchange
	if _, err := flow.HandleThread(initialThread, nil); err != nil {
		log.Fatalf("Error in joke exchange: %v", err)
	}
}

Adding a Calculator Tool

The library supports Lua and Starlark for 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())
}

ThreadFlow: Managing Middleware and Handlers

The ThreadFlow handler acts as a top-level component for managing middleware and handlers. It allows you to:

  • Add global middleware that applies to all handlers.
  • Group handlers and apply middleware specific to that group.

This is particularly useful for organizing complex conversation processing pipelines, where different stages may require different middleware (e.g., logging, retries, timeouts).


Example: ThreadFlow with Middleware
---
title: ThreadFlow with Middleware
config:
 look: handDrawn
---
flowchart TD
A[Initial Message] --> B[ThreadFlow]
B --> C[Global Middleware: Logging]
C --> D{Group?}
D -->|Yes| E[Group Middleware: Retry]
E --> F[Handler 1: Validate Input]
F --> G[Handler 2: Generate Response]
D -->|No| H[Handler 3: Process Output]
H --> I[Next Handler]
func main() {
	flow := handlers.NewThreadFlow("conversation")
	flow.Use(Logging("audit")) // Global middleware

	// Base handler for input validation
	flow.Handle(validateInput)

	// Group with specific middleware
	flow.Group(func(f *ThreadFlow) {
		f.Use(Retry("retry", 3))    // Retry up to 3 times
		f.Use(Timeout("timeout", 5)) // Timeout after 5 seconds
		f.Handle(generateResponse)      // Handler for LLM response
		f.Handle(validateOutput)        // Handler for output validation
	})

	// Initial thread
	initialThread := minds.NewThreadContext(context.Background()).WithMessages(minds.Messages{
		{Role: minds.RoleUser, Content: "Hello, world!"},
	})

	// Process the thread
	result, err := flow.HandleThread(initialThread, nil)
	if err != nil {
		log.Fatalf("Error in flow: %v", err)
	}
	fmt.Println("Result:", result.Messages().Last().Content)
}

Key Features of ThreadFlow
  1. Global Middleware: Middleware added with Use() applies to all handlers in the flow.

    flow.Use(Logging("audit"))
    
  2. Grouped Middleware: Middleware added within a Group() applies only to handlers in that group.

    flow.Group(func(f *ThreadFlow) {
        f.Use(middleware.Retry("retry", 3))
        f.Handle(generateResponse)
    })
    
  3. Sequential Execution: Handlers are executed in the order they are added, with middleware wrapping each handler.

  4. Error Handling: If a handler fails, the error is propagated, and the flow stops unless middleware (e.g., retry) handles it.


Why Use ThreadFlow?
  • Modularity: Organize your conversation processing logic into reusable groups.
  • Flexibility: Apply different middleware to different parts of the pipeline.
  • Control: Fine-tune how and when middleware is applied.
  • Debugging: Logging and other middleware make it easier to trace issues.

Integration with Other Handlers

ThreadFlow can be combined with other handlers like First, Must, and For to build even more powerful pipelines. For example:

flow := handlers.NewThreadFlow("complex-pipeline")
flow.Use(middleware.Retry("retry"), 3)

flow.Group(func(f *handlers.ThreadFlow) {
    f.Use(middleware.Timeout("timeout", 10))
    f.Handle(handlers.First("fallback",
        generateResponseWithGPT4,
        generateResponseWithGemini,
    ))
})

result, err := flow.HandleThread(initialThread, nil)

Handler Examples

Minds Toolkit uses composable handlers that implement the ThreadHandler interface:

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

Handlers can include middleware via the Use() method, enabling 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
Core Handler Types
  • Sequential: Runs handlers in order.
  • For: Repeats a handler chain for a specified number of iterations.
  • Must: Runs 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.

Parallel Validation

All handlers execute in parallel and must succeed; otherwise, an error is returned.

validate := handlers.Must("validation",
    NewFormatValidator(),     // your own handler implementations
    NewLengthValidator(1000), // ...
    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

Executes handlers in parallel; the first successful result cancels the rest.

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

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 infinitely.

llm, _ := openai.NewProvider()
process := handlers.For("process", 3, 
    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.

intentSwitch := handlers.Switch("intent-router",
    handlers.NewDefaultHandler(llm), // fallback
    handlers.SwitchCase{
        Condition: handlers.LLMCondition{
            Generator: llm,
            Prompt:   "Does this message contain a mathematical calculation?",
        },
        Handler: calculator,
    },
    handlers.SwitchCase{
        Condition: handlers.MetadataEquals{
            Key:   "type",
            Value: "question",
        },
        Handler: questionHandler,
    },
)

Batch Processing

Execute a handler for every value in a slice.

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

Contributing

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


License

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


Acknowledgements

Inspired by the http.Handler middleware pattern and the need for modular, 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

func WrapFunction

func WrapFunction(name, description string, args any, fn CallableFunc) (*functionWrapper, error)

WrapFunction takes a `CallableFunc` and wraps it as a `minds.Tool` with the provided name and description.

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 HandlerExecutor added in v0.0.6

type HandlerExecutor func(tc ThreadContext, handlers []ThreadHandler, next ThreadHandler) (ThreadContext, error)

HandlerExecutor defines a strategy for executing child handlers. It receives the current ThreadContext, a slice of handlers to execute, and an optional next handler to call after all handlers have been processed. The executor is responsible for defining how and when each handler is executed.

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 Middleware added in v0.0.6

type Middleware interface {
	Wrap(next ThreadHandler) ThreadHandler
}

Middleware represents a function that can wrap a ThreadHandler

func NewMiddleware added in v0.0.6

func NewMiddleware(name string, fn func(tc ThreadContext) error) Middleware

NewMiddleware creates a new middleware that runs a function before passing control to the next handler. The provided function can modify the ThreadContext and return an error to halt processing.

type MiddlewareFunc added in v0.0.6

type MiddlewareFunc func(next ThreadHandler) ThreadHandler

MiddlewareFunc is a function that implements the Middleware interface

func (MiddlewareFunc) Wrap added in v0.0.6

type MiddlewareHandler added in v0.0.6

type MiddlewareHandler interface {
	ThreadHandler

	// Use adds middleware to the handler
	Use(middleware ...Middleware)

	// WithMiddleware returns a new handler with the provided middleware applied
	With(middleware ...Middleware) ThreadHandler
}

func SupportsMiddleware added in v0.0.6

func SupportsMiddleware(h ThreadHandler) (MiddlewareHandler, bool)

SupportsMiddleware checks if the given handler supports middleware operations

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                 `yaml:"version,ignoreempty"`
	Format  string                 `yaml:"format,ignoreempty"`
	SHA256  string                 `yaml:"sha256,ignoreempty"`
	Extra   map[string]interface{} `yaml:",inline"`
}

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"
	RoleDeveloper Role = "developer"
)

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.

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