minds

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Dec 30, 2024 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

Use go get to install the library:

go get github.com/chriscow/minds

Usage

Basic Example

Here’s how you can compose handlers for processing a thread:

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/chriscow/minds"
	"github.com/chriscow/minds/gemini"
	"github.com/chriscow/minds/handlers"
	"github.com/chriscow/minds/openai"
)

const prompt = "What is the meaning of life?"

// This example demonstrates how to compose multiple handlers into a single pipeline
// using the familiar "middleware" pattern of Go's `net/http` package.
func main() {
	if os.Getenv("GEMINI_API_KEY") == "" {
		log.Fatalf("GEMINI_API_KEY is not set")
	}

	ctx := context.Background()

    // ContentGenerator providers implement the ThreadHandler interface
	llm, err := gemini.NewProvider(ctx)
	if err != nil {
		log.Fatalf("failed to create LLM provider: %v", err)
	}
	runPipeline(ctx, llm)

    // Try it with OpenAI ...
	llm, err := openai.NewProvider()
	if err != nil {
		log.Fatalf("failed to create LLM provider: %v", err)
	}
	runPipeline(ctx, llm)
}

func runPipeline(ctx context.Context, llm minds.ThreadHandler) {
	// Compose the handlers into a single pipeline.
	// The pipeline is an ordered list of handlers that each process the thread in some way.
	// The final handler in the pipeline is responsible for handling the final result.
	pipeline := handlers.Sequential("pipeline", exampleHandler(), llm) // Add more handlers ...
	pipeline.Use(validateMiddlware())

	// Initial message thread to start things off
	initialThread := minds.NewThreadContext(ctx).WithMessages(minds.Messages{
		{Role: minds.RoleUser, Content: prompt},
	})

	// Final handler (end of the pipeline)
	finalHandler := minds.ThreadHandlerFunc(func(tc minds.ThreadContext, next minds.ThreadHandler) (minds.ThreadContext, error) {
		fmt.Println("[finalHandler]: \n\n" + tc.Messages().Last().Content)
		return tc, nil
	})

	// Execute the pipeline
	if _, err := pipeline.HandleThread(initialThread, finalHandler); err != nil {
		log.Fatalf("Handler chain failed: %v", err)
	}
}

func exampleHandler() minds.ThreadHandlerFunc {
	return func(tc minds.ThreadContext, next minds.ThreadHandler) (minds.ThreadContext, error) {
		fmt.Println("[exampleHandler]")

		//
		// Pass the tread to the next handler in the chain
		//
		if next != nil {
			return next.HandleThread(tc, nil)
		}

		return tc, nil
	}
}

// Middleware are ThreadHandlers like any other, but they are used to wrap other handlers
// to provide additional functionality, such as validation, logging, or error handling.
func validateMiddlware() minds.ThreadHandlerFunc {
	return func(tc minds.ThreadContext, next minds.ThreadHandler) (minds.ThreadContext, error) {
		if len(tc.Messages()) == 0 {
			return tc, fmt.Errorf("thread has no messages")
		}

		// TODO: validate the input thread before processing
		fmt.Println("[validator in] Validated thread before processing")

		if next != nil {
			var err error
			tc, err = next.HandleThread(tc, nil)
			if err != nil {
				return tc, err
			}
		}

		// TODO: validate the output thread after processing
		fmt.Println("[validator out] Validated thread after processing")

		return tc, nil
	}
}
Adding a Calculator Tool

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

package main

import (
    "context"
    "github.com/chriscow/minds"
    "github.com/chriscow/minds/calculator"
)

func main() {
    calc, _ := calculator.NewCalculator(calculator.Starlark)
    result, _ := calc.Call(context.Background(), []byte("2 + 3"))
    println(string(result)) // Outputs: 5
}

Documentation

Refer to the documentation for detailed guides and API references.

Contributing

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

License

This project is licensed under the MIT 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"`
	// call function with arguments in JSON format
	Arguments []byte `json:"arguments,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 Message

type Message struct {
	Role       Role           `json:"role"`
	Content    string         `json:"content"`
	Name       string         `json:"name,omitempty"`     // For function calls
	Metadata   map[string]any `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

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

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
}

type Response

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

	// Type returns the type of this response (text, function call, etc.)
	Type() ResponseType

	// Text returns the text content if this is a text response.
	Text() (string, bool)

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

	ToMessages() (Messages, error)
}

func HandleFunctionCalls

func HandleFunctionCalls(ctx context.Context, resp Response, registry ToolRegistry) (Response, error)

func NewToolCallResponse

func NewToolCallResponse(calls []ToolCall) (Response, error)

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 {
	Context() context.Context
	UUID() string
	Messages() Messages
	Metadata() Metadata

	WithUUID(uuid string) ThreadContext
	WithMessages(messages Messages) ThreadContext
	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"`
}

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