Repository: neurocult/agency Branch: main Commit: a1e4f35c5bda Files: 38 Total size: 70.2 KB Directory structure: gitextract_oea_xf51/ ├── .gitignore ├── LICENSE ├── README.md ├── agency.go ├── examples/ │ ├── README.md │ ├── chat/ │ │ └── main.go │ ├── cli/ │ │ └── main.go │ ├── custom_operation/ │ │ └── main.go │ ├── func_call/ │ │ └── main.go │ ├── image_to_stream/ │ │ └── main.go │ ├── image_to_text/ │ │ └── main.go │ ├── logging/ │ │ └── main.go │ ├── prompt_template/ │ │ └── main.go │ ├── rag_vector_database/ │ │ ├── data.go │ │ ├── docker-compose.yaml │ │ └── main.go │ ├── speech_to_text/ │ │ └── main.go │ ├── speech_to_text_multi_model/ │ │ └── main.go │ ├── speech_to_text_to_image/ │ │ └── main.go │ ├── text_to_image_dalle2/ │ │ └── main.go │ ├── text_to_speech/ │ │ └── main.go │ ├── text_to_stream/ │ │ └── main.go │ └── translate_text/ │ └── main.go ├── go.mod ├── go.sum ├── messages.go ├── process.go └── providers/ └── openai/ ├── helpers.go ├── helpers_test.go ├── image_to_text.go ├── provider.go ├── speech_to_text.go ├── text_to_embedding.go ├── text_to_image.go ├── text_to_speech.go ├── text_to_stream.go ├── text_to_text.go └── tools.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file go.work # ide .idea .vscode # env .env # generation artifacts example.mp3 speech.ogg example.png speech.mp3 # mac os .DS_Store ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Meta Platforms, Inc. and affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Agency: The Go Way to AI Library designed for developers eager to explore the potential of Large Language Models (LLMs) and other generative AI through a clean, effective, and Go-idiomatic approach. **Welcome to the agency!** 🕵️‍♂️ ![Dracula-agent, mascot of the "agency" library.](./assets/dracula.png) ## 💻 Quick Start Install package: ```bash go get github.com/neurocult/agency ``` Chat example: ```go package main import ( "bufio" "context" "fmt" "os" _ "github.com/joho/godotenv/autoload" "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) func main() { assistant := openai. New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}). TextToText(openai.TextToTextParams{Model: "gpt-4o-mini"}). SetPrompt("You are helpful assistant.") messages := []agency.Message{} reader := bufio.NewReader(os.Stdin) ctx := context.Background() for { fmt.Print("User: ") text, err := reader.ReadString('\n') if err != nil { panic(err) } input := agency.NewTextMessage(agency.UserRole, text) answer, err := assistant.SetMessages(messages).Execute(ctx, input) if err != nil { panic(err) } fmt.Println("Assistant:", string(answer.Content())) messages = append(messages, input, answer) } } ``` That's it! See [examples](./examples/) to find out more complex usecases including RAGs and multimodal operations. ## 🚀 Features ✨ **Pure Go**: fast and lightweight, statically typed, no need to mess with Python or JavaScript ✨ Write **clean code** and follow **clean architecture** by separating business logic from concrete implementations ✨ Easily create **custom operations** by implementing simple interface ✨ **Compose operations** together into **processes** with the ability to observe each step via **interceptors** ✨ **OpenAI API bindings** (can be used for any openai-compatable API: text to text (completion), text to image, text to speech, speech to text ## 🤔 Why need Agency? At the heart of Agency is the ambition to empower users to build autonomous agents. While **perfect for all range of generative AI applications**, from chat interfaces to complex data analysis, our library's ultimate goal is to simplify the creation of autonomous AI systems. Whether you're building individual assistant or coordinating agent swarms, Agency provides the tools and flexibility needed to bring these advanced concepts to life with ease and efficiency. In the generative AI landscape, Go-based libraries are rare. The most notable is [LangChainGo](https://github.com/tmc/langchaingo), a Go port of the Python LangChain. However, translating Python to Go can be clunky and may not fit well with Go's idiomatic style. Plus, some question LangChain's design, even in Python. This situation reveals a clear need for an idiomatic Go alternative. Our goal is to fill this gap with a Go-centric library that emphasizes clean, simple code and avoids unnecessary complexities. Agency is designed with a small, robust core, easy to extend and perfectly suited to Go's strengths in static typing and performance. It's our answer to the lack of Go-native solutions in generative AI. ## Tutorial - [Part 1](https://dev.to/emil14/agency-the-go-way-to-ai-part-1-1lhe) ([Russian translation](https://habr.com/ru/sandbox/204508/)) ## 🛣 Roadmap In the next versions: - [x] Support for external function calls - [ ] Metadata (tokens used, audio duration, etc) - [ ] More provider-adapters, not only openai - [x] Image to text operations - [ ] Powerful API for autonomous agents - [ ] Tagging and JSON output parser ================================================ FILE: agency.go ================================================ package agency import ( "context" "fmt" ) // Operation is basic building block. type Operation struct { handler OperationHandler config *OperationConfig } // OperationHandler is a function that implements operation's logic. // It could be thought of as an interface that providers must implement. type OperationHandler func(context.Context, Message, *OperationConfig) (Message, error) // OperationConfig represents abstract operation configuration for all possible models. type OperationConfig struct { Prompt string Messages []Message } func (p *Operation) Config() *OperationConfig { return p.config } // NewOperation allows to create an operation from a function. func NewOperation(handler OperationHandler) *Operation { return &Operation{ handler: handler, config: &OperationConfig{}, } } // Execute executes operation handler with input message and current configuration. func (p *Operation) Execute(ctx context.Context, input Message) (Message, error) { output, err := p.handler(ctx, input, p.config) if err != nil { return nil, err } return output, nil } func (p *Operation) SetPrompt(prompt string, args ...any) *Operation { p.config.Prompt = fmt.Sprintf(prompt, args...) return p } func (p *Operation) SetMessages(msgs []Message) *Operation { p.config.Messages = msgs return p } ================================================ FILE: examples/README.md ================================================ To run an example: ```shell export OPENAI_API_KEY="" go run ./example_name ``` ================================================ FILE: examples/chat/main.go ================================================ package main import ( "bufio" "context" "fmt" "os" _ "github.com/joho/godotenv/autoload" "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) func main() { assistant := openai. New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}). TextToText(openai.TextToTextParams{Model: "gpt-4o-mini"}). SetPrompt("You are helpful assistant.") messages := []agency.Message{} reader := bufio.NewReader(os.Stdin) ctx := context.Background() for { fmt.Print("User: ") text, err := reader.ReadString('\n') if err != nil { panic(err) } input := agency.NewTextMessage(agency.UserRole, text) answer, err := assistant.SetMessages(messages).Execute(ctx, input) if err != nil { panic(err) } fmt.Println("Assistant:", string(answer.Content())) messages = append(messages, input, answer) } } ================================================ FILE: examples/cli/main.go ================================================ package main import ( "context" "flag" "fmt" "os" _ "github.com/joho/godotenv/autoload" "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) // usage example: go to the repo root and execute // go run examples/cli/main.go -prompt "You are professional translator, translate everything you see to Russian" -model "gpt-4o-mini" -maxTokens=1000 "I love winter" func main() { provider := openai.New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}) temp := flag.Float64("temp", 0.0, "Temperature value") maxTokens := flag.Int("maxTokens", 0, "Maximum number of tokens") model := flag.String("model", "gpt-4o-mini", "Model name") prompt := flag.String("prompt", "You are a helpful assistant", "System message") flag.Parse() args := flag.Args() if len(args) < 1 { fmt.Println("content argument is required") os.Exit(1) } content := args[0] result, err := provider. TextToText(openai.TextToTextParams{ Model: *model, Temperature: openai.Temperature(float32(*temp)), MaxTokens: *maxTokens, }). SetPrompt(*prompt). Execute(context.Background(), agency.NewMessage(agency.UserRole, agency.TextKind, []byte(content))) if err != nil { fmt.Println(err) os.Exit(1) } fmt.Println(result.Content()) } ================================================ FILE: examples/custom_operation/main.go ================================================ package main import ( "context" "fmt" "strconv" "github.com/neurocult/agency" ) func main() { increment := agency.NewOperation(incrementFunc) msg, err := agency.NewProcess( increment, increment, increment, ).Execute( context.Background(), agency.NewMessage( agency.UserRole, agency.TextKind, []byte("0"), ), ) if err != nil { panic(err) } fmt.Println(string(msg.Content())) } func incrementFunc(ctx context.Context, msg agency.Message, _ *agency.OperationConfig) (agency.Message, error) { i, err := strconv.ParseInt(string(msg.Content()), 10, 10) if err != nil { return nil, err } inc := strconv.Itoa(int(i) + 1) return agency.NewMessage(agency.ToolRole, agency.TextKind, []byte(inc)), nil } ================================================ FILE: examples/func_call/main.go ================================================ package main import ( "context" "encoding/json" "fmt" "os" _ "github.com/joho/godotenv/autoload" go_openai "github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai/jsonschema" "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) func main() { t2tOp := openai. New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}). TextToText(openai.TextToTextParams{ Model: go_openai.GPT4oMini, FuncDefs: []openai.FuncDef{ // function without parameters { Name: "GetMeaningOfLife", Description: "Answer questions about meaning of life", Body: func(ctx context.Context, _ []byte) (agency.Message, error) { // because we don't need any arguments return agency.NewTextMessage(agency.ToolRole, "42"), nil }, }, // function with parameters { Name: "ChangeNumbers", Description: "Change given numbers when asked", Parameters: &jsonschema.Definition{ Type: "object", Properties: map[string]jsonschema.Definition{ "a": {Type: "integer"}, "b": {Type: "integer"}, }, }, Body: func(ctx context.Context, params []byte) (agency.Message, error) { var pp struct{ A, B int } if err := json.Unmarshal(params, &pp); err != nil { return nil, err } return agency.NewTextMessage( agency.ToolRole, fmt.Sprintf("%d", (pp.A+pp.B)*10), ), nil // *10 is just to distinguish from normal response }, }, }, }). SetPrompt(` Answer questions about meaning of life and summing numbers. Always use GetMeaningOfLife and ChangeNumbers functions results as answers. Examples: - User: what is the meaning of life? - Assistant: 42 - User: 1+1 - Assistant: 20 - User: 1+1 and what is the meaning of life? - Assistant: 20 and 42`) ctx := context.Background() // test for first function call answer, err := t2tOp.Execute( ctx, agency.NewMessage(agency.UserRole, agency.TextKind, []byte("what is the meaning of life?")), ) if err != nil { panic(err) } printAnswer(answer) // test for second function call answer, err = t2tOp.Execute( ctx, agency.NewMessage(agency.UserRole, agency.TextKind, []byte("1+1?")), ) if err != nil { panic(err) } printAnswer(answer) // test for both function calls at the same time answer, err = t2tOp.Execute( ctx, agency.NewMessage(agency.UserRole, agency.TextKind, []byte("1+1 and what is the meaning of life?")), ) if err != nil { panic(err) } printAnswer(answer) } func printAnswer(message agency.Message) { fmt.Printf( "Role: %s; Type: %s; Data: %s\n", message.Role(), message.Kind(), string(message.Content()), ) } ================================================ FILE: examples/image_to_stream/main.go ================================================ package main import ( "context" "fmt" "os" _ "github.com/joho/godotenv/autoload" "github.com/neurocult/agency" providers "github.com/neurocult/agency/providers/openai" ) func main() { imgBytes, err := os.ReadFile("example.png") if err != nil { panic(err) } _, err = providers.New(providers.Params{Key: os.Getenv("OPENAI_API_KEY")}). TextToStream(providers.TextToStreamParams{ TextToTextParams: providers.TextToTextParams{MaxTokens: 300, Model: "gpt-4o"}, StreamHandler: func(delta, total string, isFirst, isLast bool) error { fmt.Println(delta) return nil }}). SetPrompt("describe what you see"). Execute( context.Background(), agency.NewMessage(agency.UserRole, agency.ImageKind, imgBytes), ) if err != nil { panic(err) } } ================================================ FILE: examples/image_to_text/main.go ================================================ package main import ( "context" "fmt" "os" _ "github.com/joho/godotenv/autoload" "github.com/neurocult/agency" openAIProvider "github.com/neurocult/agency/providers/openai" "github.com/sashabaranov/go-openai" ) func main() { imgBytes, err := os.ReadFile("example.png") if err != nil { panic(err) } result, err := openAIProvider.New(openAIProvider.Params{Key: os.Getenv("OPENAI_API_KEY")}). ImageToText(openAIProvider.ImageToTextParams{Model: openai.GPT4o, MaxTokens: 300}). SetPrompt("describe what you see"). Execute( context.Background(), agency.NewMessage(agency.UserRole, agency.ImageKind, imgBytes), ) if err != nil { panic(err) } fmt.Println(string(result.Content())) } ================================================ FILE: examples/logging/main.go ================================================ package main import ( "context" "fmt" "os" _ "github.com/joho/godotenv/autoload" "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) func main() { factory := openai.New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}) params := openai.TextToTextParams{Model: "gpt-4o-mini"} _, err := agency.NewProcess( factory.TextToText(params).SetPrompt("explain what that means"), factory.TextToText(params).SetPrompt("translate to russian"), factory.TextToText(params).SetPrompt("replace all spaces with '_'"), ). Execute( context.Background(), agency.NewMessage(agency.UserRole, agency.TextKind, []byte("Kazakhstan alga!")), Logger, ) if err != nil { panic(err) } } func Logger(input, output agency.Message, cfg *agency.OperationConfig) { fmt.Printf( "in: %v\nprompt: %v\nout: %v\n\n", string(input.Content()), cfg.Prompt, string(output.Content()), ) } ================================================ FILE: examples/prompt_template/main.go ================================================ package main import ( "context" "fmt" "os" _ "github.com/joho/godotenv/autoload" "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) func main() { factory := openai.New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}) resultMsg, err := factory. TextToText(openai.TextToTextParams{Model: "gpt-4o-mini"}). SetPrompt( "You are a helpful assistant that translates %s to %s", "English", "French", ). Execute( context.Background(), agency.NewMessage(agency.UserRole, agency.TextKind, []byte("I love programming.")), ) if err != nil { panic(err) } fmt.Println(string(resultMsg.Content())) } ================================================ FILE: examples/rag_vector_database/data.go ================================================ package main import "github.com/weaviate/weaviate/entities/models" // first 5 objects contain something about programming without word 'programming' while other are random topics. var data []*models.Object = []*models.Object{ { Class: "Records", Properties: map[string]string{"content": "Debugging Java applications can be challenging."}, }, { Class: "Records", Properties: map[string]string{"content": "Writing code in Python is both fun and efficient."}, }, { Class: "Records", Properties: map[string]string{"content": "HTML and CSS are the building blocks of web development."}, }, { Class: "Records", Properties: map[string]string{"content": "Using version control systems like Git is essential for collaborative coding."}, }, { Class: "Records", Properties: map[string]string{"content": "Understanding algorithms is crucial for effective software development."}, }, { Class: "Records", Properties: map[string]string{"content": "Swimming is a fun and beneficial form of exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Playing musical instruments can be both challenging and fulfilling."}, }, { Class: "Records", Properties: map[string]string{"content": "Traveling to new places is an adventure worth having."}, }, { Class: "Records", Properties: map[string]string{"content": "Cycling is both an eco-friendly transport and a great way to exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Playing musical instruments can be both challenging and fulfilling."}, }, { Class: "Records", Properties: map[string]string{"content": "Learning a new language opens up a world of opportunities."}, }, { Class: "Records", Properties: map[string]string{"content": "Playing musical instruments can be both challenging and fulfilling."}, }, { Class: "Records", Properties: map[string]string{"content": "Cycling is both an eco-friendly transport and a great way to exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Traveling to new places is an adventure worth having."}, }, { Class: "Records", Properties: map[string]string{"content": "Meditation helps in reducing stress and improving focus."}, }, { Class: "Records", Properties: map[string]string{"content": "Cycling is both an eco-friendly transport and a great way to exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Playing musical instruments can be both challenging and fulfilling."}, }, { Class: "Records", Properties: map[string]string{"content": "Cycling is both an eco-friendly transport and a great way to exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Traveling to new places is an adventure worth having."}, }, { Class: "Records", Properties: map[string]string{"content": "Gardening is a peaceful and rewarding hobby."}, }, { Class: "Records", Properties: map[string]string{"content": "Running is a simple and effective way to stay fit."}, }, { Class: "Records", Properties: map[string]string{"content": "Playing musical instruments can be both challenging and fulfilling."}, }, { Class: "Records", Properties: map[string]string{"content": "Hiking in nature is both invigorating and relaxing."}, }, { Class: "Records", Properties: map[string]string{"content": "The art of cooking is a blend of flavors and techniques."}, }, { Class: "Records", Properties: map[string]string{"content": "Watching movies is a popular form of entertainment."}, }, { Class: "Records", Properties: map[string]string{"content": "Traveling to new places is an adventure worth having."}, }, { Class: "Records", Properties: map[string]string{"content": "Practicing yoga promotes physical and mental well-being."}, }, { Class: "Records", Properties: map[string]string{"content": "Reading books is a journey through knowledge and imagination."}, }, { Class: "Records", Properties: map[string]string{"content": "The art of cooking is a blend of flavors and techniques."}, }, { Class: "Records", Properties: map[string]string{"content": "Running is a simple and effective way to stay fit."}, }, { Class: "Records", Properties: map[string]string{"content": "Practicing yoga promotes physical and mental well-being."}, }, { Class: "Records", Properties: map[string]string{"content": "Photography captures moments and memories."}, }, { Class: "Records", Properties: map[string]string{"content": "Cycling is both an eco-friendly transport and a great way to exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Gardening is a peaceful and rewarding hobby."}, }, { Class: "Records", Properties: map[string]string{"content": "Traveling to new places is an adventure worth having."}, }, { Class: "Records", Properties: map[string]string{"content": "Swimming is a fun and beneficial form of exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Reading books is a journey through knowledge and imagination."}, }, { Class: "Records", Properties: map[string]string{"content": "Painting allows for creative expression and relaxation."}, }, { Class: "Records", Properties: map[string]string{"content": "The art of cooking is a blend of flavors and techniques."}, }, { Class: "Records", Properties: map[string]string{"content": "Photography captures moments and memories."}, }, { Class: "Records", Properties: map[string]string{"content": "Reading books is a journey through knowledge and imagination."}, }, { Class: "Records", Properties: map[string]string{"content": "Running is a simple and effective way to stay fit."}, }, { Class: "Records", Properties: map[string]string{"content": "Playing musical instruments can be both challenging and fulfilling."}, }, { Class: "Records", Properties: map[string]string{"content": "Hiking in nature is both invigorating and relaxing."}, }, { Class: "Records", Properties: map[string]string{"content": "Learning a new language opens up a world of opportunities."}, }, { Class: "Records", Properties: map[string]string{"content": "Painting allows for creative expression and relaxation."}, }, { Class: "Records", Properties: map[string]string{"content": "Hiking in nature is both invigorating and relaxing."}, }, { Class: "Records", Properties: map[string]string{"content": "The art of cooking is a blend of flavors and techniques."}, }, { Class: "Records", Properties: map[string]string{"content": "Meditation helps in reducing stress and improving focus."}, }, { Class: "Records", Properties: map[string]string{"content": "Practicing yoga promotes physical and mental well-being."}, }, { Class: "Records", Properties: map[string]string{"content": "Swimming is a fun and beneficial form of exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Traveling to new places is an adventure worth having."}, }, { Class: "Records", Properties: map[string]string{"content": "Photography captures moments and memories."}, }, { Class: "Records", Properties: map[string]string{"content": "Practicing yoga promotes physical and mental well-being."}, }, { Class: "Records", Properties: map[string]string{"content": "Running is a simple and effective way to stay fit."}, }, { Class: "Records", Properties: map[string]string{"content": "Hiking in nature is both invigorating and relaxing."}, }, { Class: "Records", Properties: map[string]string{"content": "Swimming is a fun and beneficial form of exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Running is a simple and effective way to stay fit."}, }, { Class: "Records", Properties: map[string]string{"content": "Meditation helps in reducing stress and improving focus."}, }, { Class: "Records", Properties: map[string]string{"content": "Hiking in nature is both invigorating and relaxing."}, }, { Class: "Records", Properties: map[string]string{"content": "Photography captures moments and memories."}, }, { Class: "Records", Properties: map[string]string{"content": "Running is a simple and effective way to stay fit."}, }, { Class: "Records", Properties: map[string]string{"content": "Cycling is both an eco-friendly transport and a great way to exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Practicing yoga promotes physical and mental well-being."}, }, { Class: "Records", Properties: map[string]string{"content": "Practicing yoga promotes physical and mental well-being."}, }, { Class: "Records", Properties: map[string]string{"content": "Hiking in nature is both invigorating and relaxing."}, }, { Class: "Records", Properties: map[string]string{"content": "Cycling is both an eco-friendly transport and a great way to exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Hiking in nature is both invigorating and relaxing."}, }, { Class: "Records", Properties: map[string]string{"content": "Reading books is a journey through knowledge and imagination."}, }, { Class: "Records", Properties: map[string]string{"content": "Watching movies is a popular form of entertainment."}, }, { Class: "Records", Properties: map[string]string{"content": "Hiking in nature is both invigorating and relaxing."}, }, { Class: "Records", Properties: map[string]string{"content": "Playing musical instruments can be both challenging and fulfilling."}, }, { Class: "Records", Properties: map[string]string{"content": "Traveling to new places is an adventure worth having."}, }, { Class: "Records", Properties: map[string]string{"content": "Painting allows for creative expression and relaxation."}, }, { Class: "Records", Properties: map[string]string{"content": "Traveling to new places is an adventure worth having."}, }, { Class: "Records", Properties: map[string]string{"content": "Hiking in nature is both invigorating and relaxing."}, }, { Class: "Records", Properties: map[string]string{"content": "Meditation helps in reducing stress and improving focus."}, }, { Class: "Records", Properties: map[string]string{"content": "Watching movies is a popular form of entertainment."}, }, { Class: "Records", Properties: map[string]string{"content": "Traveling to new places is an adventure worth having."}, }, { Class: "Records", Properties: map[string]string{"content": "Photography captures moments and memories."}, }, { Class: "Records", Properties: map[string]string{"content": "Learning a new language opens up a world of opportunities."}, }, { Class: "Records", Properties: map[string]string{"content": "Painting allows for creative expression and relaxation."}, }, { Class: "Records", Properties: map[string]string{"content": "Photography captures moments and memories."}, }, { Class: "Records", Properties: map[string]string{"content": "Traveling to new places is an adventure worth having."}, }, { Class: "Records", Properties: map[string]string{"content": "Practicing yoga promotes physical and mental well-being."}, }, { Class: "Records", Properties: map[string]string{"content": "Traveling to new places is an adventure worth having."}, }, { Class: "Records", Properties: map[string]string{"content": "Practicing yoga promotes physical and mental well-being."}, }, { Class: "Records", Properties: map[string]string{"content": "The art of cooking is a blend of flavors and techniques."}, }, { Class: "Records", Properties: map[string]string{"content": "Running is a simple and effective way to stay fit."}, }, { Class: "Records", Properties: map[string]string{"content": "Running is a simple and effective way to stay fit."}, }, { Class: "Records", Properties: map[string]string{"content": "Photography captures moments and memories."}, }, { Class: "Records", Properties: map[string]string{"content": "The art of cooking is a blend of flavors and techniques."}, }, { Class: "Records", Properties: map[string]string{"content": "Photography captures moments and memories."}, }, { Class: "Records", Properties: map[string]string{"content": "Cycling is both an eco-friendly transport and a great way to exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Playing musical instruments can be both challenging and fulfilling."}, }, { Class: "Records", Properties: map[string]string{"content": "The art of cooking is a blend of flavors and techniques."}, }, { Class: "Records", Properties: map[string]string{"content": "Swimming is a fun and beneficial form of exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Cycling is both an eco-friendly transport and a great way to exercise."}, }, { Class: "Records", Properties: map[string]string{"content": "Traveling to new places is an adventure worth having."}, }, { Class: "Records", Properties: map[string]string{"content": "Painting allows for creative expression and relaxation."}, }, } ================================================ FILE: examples/rag_vector_database/docker-compose.yaml ================================================ --- version: "3.4" services: weaviate: command: - --host - 0.0.0.0 - --port - "8080" - --scheme - http image: cr.weaviate.io/semitechnologies/weaviate:1.22.4 ports: - 8080:8080 - 50051:50051 restart: on-failure:0 environment: QUERY_DEFAULTS_LIMIT: 25 AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "true" PERSISTENCE_DATA_PATH: "/var/lib/weaviate" DEFAULT_VECTORIZER_MODULE: "none" ENABLE_MODULES: "text2vec-cohere,text2vec-huggingface,text2vec-palm,text2vec-openai,generative-openai,generative-cohere,generative-palm,ref2vec-centroid,reranker-cohere,qna-openai" CLUSTER_HOSTNAME: "node1" volumes: weaviate_data: ================================================ FILE: examples/rag_vector_database/main.go ================================================ package main import ( "context" "encoding/json" "os" _ "github.com/joho/godotenv/autoload" "github.com/neurocult/agency" "github.com/weaviate/weaviate-go-client/v4/weaviate" "github.com/weaviate/weaviate-go-client/v4/weaviate/graphql" "github.com/weaviate/weaviate/entities/models" "github.com/neurocult/agency/providers/openai" ) // natural langauge query -> weaviate RAG -> speech func main() { openAPIKey := os.Getenv("OPENAI_API_KEY") ctx := context.Background() client, err := prepareDB(openAPIKey, ctx) if err != nil { panic(err) } factory := openai.New(openai.Params{Key: openAPIKey}) retrieve := RAGoperation(client) summarize := factory.TextToText(openai.TextToTextParams{Model: "gpt-4o-mini"}).SetPrompt("summarize") voice := factory.TextToSpeech(openai.TextToSpeechParams{ Model: "tts-1", ResponseFormat: "mp3", Speed: 1, Voice: "onyx", }) result, err := agency.NewProcess( retrieve, summarize, voice, ).Execute(ctx, agency.NewMessage(agency.UserRole, agency.TextKind, []byte("programming"))) if err != nil { panic(err) } if err := saveToDisk(result); err != nil { panic(err) } } // RAGoperation retrieves relevant objects from vector store and builds a text message to pass further to the process func RAGoperation(client *weaviate.Client) *agency.Operation { return agency.NewOperation(func(ctx context.Context, msg agency.Message, po *agency.OperationConfig) (agency.Message, error) { input := string(msg.Content()) result, err := client.GraphQL().Get(). WithClassName("Records"). WithFields(graphql.Field{Name: "content"}). WithNearText( client.GraphQL(). NearTextArgBuilder(). WithConcepts( []string{input}, ), ). WithLimit(10). Do(ctx) if err != nil { panic(err) } var content string for _, obj := range result.Data { bb, err := json.Marshal(&obj) if err != nil { return nil, err } content += string(bb) } return agency.NewMessage( agency.AssistantRole, agency.TextKind, []byte(content), ), nil }) } func prepareDB(openAPIKey string, ctx context.Context) (*weaviate.Client, error) { client, err := weaviate.NewClient(weaviate.Config{ Host: "localhost:8080", Scheme: "http", Headers: map[string]string{ "X-OpenAI-Api-Key": openAPIKey, }, }) if err != nil { return nil, err } if err := client.Schema().AllDeleter().Do(ctx); err != nil { return nil, err } classObj := &models.Class{ Class: "Records", Vectorizer: "text2vec-openai", ModuleConfig: map[string]interface{}{ "text2vec-openai": map[string]interface{}{}, "generative-openai": map[string]interface{}{}, }, } if err = client.Schema().ClassCreator().WithClass(classObj).Do(context.Background()); err != nil { return nil, err } if _, err := client.Batch().ObjectsBatcher().WithObjects(data...).Do(ctx); err != nil { return nil, err } return client, nil } func saveToDisk(msg agency.Message) error { file, err := os.Create("speech.mp3") if err != nil { return err } defer file.Close() _, err = file.Write(msg.Content()) if err != nil { return err } return nil } ================================================ FILE: examples/speech_to_text/main.go ================================================ // To make this example work make sure you have speech.ogg file in the root of directory. // You can use text to speech example to generate speech file. package main import ( "context" "fmt" "os" _ "github.com/joho/godotenv/autoload" goopenai "github.com/sashabaranov/go-openai" "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) func main() { factory := openai.New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}) data, err := os.ReadFile("speech.mp3") if err != nil { panic(err) } result, err := factory.SpeechToText(openai.SpeechToTextParams{ Model: goopenai.Whisper1, }).Execute( context.Background(), agency.NewMessage(agency.UserRole, agency.VoiceKind, data), ) if err != nil { panic(err) } fmt.Println(string(result.Content())) } ================================================ FILE: examples/speech_to_text_multi_model/main.go ================================================ // To make this example work make sure you have speech.ogg file in the root of directory package main import ( "context" "fmt" "os" _ "github.com/joho/godotenv/autoload" goopenai "github.com/sashabaranov/go-openai" "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) type Saver []agency.Message func (s *Saver) Save(input, output agency.Message, _ *agency.OperationConfig) { *s = append(*s, output) } func main() { factory := openai.New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}) // step 1 hear := factory. SpeechToText(openai.SpeechToTextParams{ Model: goopenai.Whisper1, }) // step2 translate := factory. TextToText(openai.TextToTextParams{ Model: "gpt-4o-mini", Temperature: openai.Temperature(0.5), }). SetPrompt("translate to russian") // step 3 uppercase := factory. TextToText(openai.TextToTextParams{ Model: "gpt-4o-mini", Temperature: openai.Temperature(1), }). SetPrompt("uppercase every letter of the text") saver := Saver{} sound, err := os.ReadFile("speech.mp3") if err != nil { panic(err) } ctx := context.Background() speechMsg := agency.NewMessage(agency.UserRole, agency.VoiceKind, sound) _, err = agency.NewProcess( hear, translate, uppercase, ).Execute(ctx, speechMsg, saver.Save) if err != nil { panic(err) } for _, msg := range saver { fmt.Println(string(msg.Content())) } } ================================================ FILE: examples/speech_to_text_to_image/main.go ================================================ // To make this example work make sure you have speech.ogg file in the root of directory package main import ( "bytes" "context" "image/png" "os" _ "github.com/joho/godotenv/autoload" goopenai "github.com/sashabaranov/go-openai" "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) func main() { factory := openai.New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}) data, err := os.ReadFile("speech.mp3") if err != nil { panic(err) } msg, err := agency.NewProcess( factory.SpeechToText(openai.SpeechToTextParams{Model: goopenai.Whisper1}), factory.TextToImage(openai.TextToImageParams{ Model: goopenai.CreateImageModelDallE2, ImageSize: goopenai.CreateImageSize256x256, }), ).Execute(context.Background(), agency.NewMessage(agency.UserRole, agency.VoiceKind, data)) if err != nil { panic(err) } if err := saveImgToDisk(msg); err != nil { panic(err) } } func saveImgToDisk(msg agency.Message) error { r := bytes.NewReader(msg.Content()) imgData, err := png.Decode(r) if err != nil { return err } file, err := os.Create("example.png") if err != nil { return err } defer file.Close() if err := png.Encode(file, imgData); err != nil { return err } return nil } ================================================ FILE: examples/text_to_image_dalle2/main.go ================================================ package main import ( "bytes" "context" "fmt" "image" "image/png" "os" _ "github.com/joho/godotenv/autoload" "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) func main() { provider := openai.New(openai.Params{ Key: os.Getenv("OPENAI_API_KEY"), }) result, err := provider.TextToImage(openai.TextToImageParams{ Model: "dall-e-2", ImageSize: "512x512", Quality: "standard", Style: "vivid", }).Execute( context.Background(), agency.NewMessage(agency.UserRole, agency.TextKind, []byte("Halloween night at a haunted museum")), ) if err != nil { panic(err) } if err := saveToDisk(result); err != nil { panic(err) } fmt.Println("Image has been saved!") } func saveToDisk(msg agency.Message) error { r := bytes.NewReader(msg.Content()) // for dall-e-3 use third party libraries due to lack of webp support in go stdlib imgData, format, err := image.Decode(r) if err != nil { return err } file, err := os.Create("example." + format) if err != nil { return err } defer file.Close() if err := png.Encode(file, imgData); err != nil { return err } return nil } ================================================ FILE: examples/text_to_speech/main.go ================================================ package main import ( "context" "os" _ "github.com/joho/godotenv/autoload" "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) func main() { input := agency.NewMessage( agency.UserRole, agency.TextKind, []byte(`One does not simply walk into Mordor. Its black gates are guarded by more than just Orcs. There is evil there that does not sleep, and the Great Eye is ever watchful.`)) msg, err := openai.New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}). TextToSpeech(openai.TextToSpeechParams{ Model: "tts-1", ResponseFormat: "mp3", Speed: 1, Voice: "alloy", }). Execute(context.Background(), input) if err != nil { panic(err) } if err := saveToDisk(msg); err != nil { panic(err) } } func saveToDisk(msg agency.Message) error { file, err := os.Create("speech.mp3") if err != nil { return err } defer file.Close() _, err = file.Write(msg.Content()) if err != nil { return err } return nil } ================================================ FILE: examples/text_to_stream/main.go ================================================ package main import ( "context" "fmt" "os" _ "github.com/joho/godotenv/autoload" goopenai "github.com/sashabaranov/go-openai" "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) func main() { factory := openai.New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}) result, err := factory. TextToStream(openai.TextToStreamParams{ TextToTextParams: openai.TextToTextParams{Model: goopenai.GPT4oMini}, StreamHandler: func(delta, total string, isFirst, isLast bool) error { if isFirst { fmt.Println("====Start streaming====") } fmt.Print(delta) if isLast { fmt.Println("\n====Finish streaming====") } return nil }, }). SetPrompt("Write a few sentences about topic"). Execute( context.Background(), agency.NewMessage( agency.UserRole, agency.TextKind, []byte("I love programming."), ), ) if err != nil { panic(err) } fmt.Println("\nResult:", string(result.Content())) } ================================================ FILE: examples/translate_text/main.go ================================================ package main import ( "context" "fmt" "os" _ "github.com/joho/godotenv/autoload" goopenai "github.com/sashabaranov/go-openai" "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) func main() { factory := openai.New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}) result, err := factory. TextToText(openai.TextToTextParams{Model: goopenai.GPT4oMini}). SetPrompt("You are a helpful assistant that translates English to French"). Execute( context.Background(), agency.NewMessage( agency.UserRole, agency.TextKind, []byte("I love programming."), ), ) if err != nil { panic(err) } fmt.Println(string(result.Content())) } ================================================ FILE: go.mod ================================================ module github.com/neurocult/agency go 1.21.0 require ( github.com/sashabaranov/go-openai v1.36.1 github.com/weaviate/weaviate v1.24.8 github.com/weaviate/weaviate-go-client/v4 v4.13.1 ) require ( github.com/google/uuid v1.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect ) require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/go-openapi/analysis v0.23.0 // indirect github.com/go-openapi/errors v0.22.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/loads v0.22.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/strfmt v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect github.com/joho/godotenv v1.5.1 github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/oklog/ulid v1.3.1 // indirect go.mongodb.org/mongo-driver v1.15.0 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/oauth2 v0.19.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240412170617-26222e5d3d56 // indirect google.golang.org/grpc v1.63.2 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/sashabaranov/go-openai v1.36.1 h1:EVfRXwIlW2rUzpx6vR+aeIKCK/xylSrVYAx1TMTSX3g= github.com/sashabaranov/go-openai v1.36.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/weaviate/weaviate v1.24.8 h1:obeBOJuXScDvUlbTKuqPwJl/cUB5csRhCN6q4smcQiM= github.com/weaviate/weaviate v1.24.8/go.mod h1:LkAk+xUwF8DKKRb9dEI9DrquwJ/tUzfgd2NN+KEDTYU= github.com/weaviate/weaviate-go-client/v4 v4.13.1 h1:7PuK/hpy6Q0b9XaVGiUg5OD1MI/eF2ew9CJge9XdBEE= github.com/weaviate/weaviate-go-client/v4 v4.13.1/go.mod h1:B2m6g77xWDskrCq1GlU6CdilS0RG2+YXEgzwXRADad0= go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240412170617-26222e5d3d56 h1:zviK8GX4VlMstrK3JkexM5UHjH1VOkRebH9y3jhSBGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20240412170617-26222e5d3d56/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: messages.go ================================================ package agency import "encoding/json" type Message interface { Role() Role Content() []byte Kind() Kind } type Kind string const ( TextKind Kind = "text" ImageKind Kind = "image" VoiceKind Kind = "voice" EmbeddingKind Kind = "embedding" ) type Role string const ( UserRole Role = "user" SystemRole Role = "system" AssistantRole Role = "assistant" ToolRole Role = "tool" ) type BaseMessage struct { content []byte role Role kind Kind } func (bm BaseMessage) Role() Role { return bm.role } func (bm BaseMessage) Kind() Kind { return bm.kind } func (bm BaseMessage) Content() []byte { return bm.content } // NewMessage creates new `Message` with the specified `Role` and `Kind` func NewMessage(role Role, kind Kind, content []byte) BaseMessage { return BaseMessage{ content: content, role: role, kind: kind, } } // NewTextMessage creates new `Message` with Text kind and the specified `Role` func NewTextMessage(role Role, content string) BaseMessage { return BaseMessage{ content: []byte(content), role: role, kind: TextKind, } } // NewJsonMessage marshals content and creates new `Message` with text kind and the specified `Role` func NewJsonMessage(role Role, content any) (BaseMessage, error) { data, err := json.Marshal(content) if err != nil { return BaseMessage{}, err } return BaseMessage{ content: data, role: role, kind: TextKind, }, nil } ================================================ FILE: process.go ================================================ package agency import ( "context" ) // Process is a chain of operations that can be executed in sequence. type Process struct { operations []*Operation } // NewProcess creates a new Process with given operations. func NewProcess(operations ...*Operation) *Process { return &Process{ operations: operations, } } // Interceptor is a function that is called by Process after one operation finished but before next one is started. type Interceptor func(in Message, out Message, cfg *OperationConfig) // Execute iterates over Process's operations and sequentially executes them. // After first operation is executed it uses its output as an input to the second one and so on until the whole chain is finished. // It also executes all given interceptors, if they are provided, so for every N operations and M interceptors it's N x M executions. func (p *Process) Execute(ctx context.Context, input Message, interceptors ...Interceptor) (Message, error) { for _, operation := range p.operations { output, err := operation.Execute(ctx, input) if err != nil { return nil, err } // FIXME while these are called AFTER operation and not before it's impossible to modify configuration for _, interceptor := range interceptors { interceptor(input, output, operation.Config()) } input = output } return input, nil } ================================================ FILE: providers/openai/helpers.go ================================================ package openai import ( "encoding/binary" "fmt" "math" ) type Embedding []float32 func EmbeddingToBytes(dimensions int, embeddings []Embedding) ([]byte, error) { if len(embeddings) == 0 { return nil, fmt.Errorf("embeddings is empty") } buf := make([]byte, len(embeddings)*dimensions*4) for i, embedding := range embeddings { if len(embedding) != dimensions { return nil, fmt.Errorf("invalid embedding length: %d, expected %d", len(embedding), dimensions) } for j, f := range embedding { u := math.Float32bits(f) binary.LittleEndian.PutUint32(buf[(i*dimensions+j)*4:], u) } } return buf, nil } func BytesToEmbedding(dimensions int, buf []byte) ([]Embedding, error) { if mltp := len(buf) % (dimensions * 4); mltp != 0 { return nil, fmt.Errorf("invalid buffer length: got %d, but expected multiple of %d", len(buf), dimensions*4) } embeddings := make([]Embedding, len(buf)/dimensions/4) for i := range embeddings { embeddings[i] = make([]float32, dimensions) for j := 0; j < dimensions; j++ { index := (i*dimensions + j) * 4 if index+4 > len(buf) { return nil, fmt.Errorf("buffer is too small for expected number of embeddings") } embeddings[i][j] = math.Float32frombits(binary.LittleEndian.Uint32(buf[index:])) } } return embeddings, nil } // NullableFloat32 is a type that exists to distinguish between undefined values and real zeros. // It fixes sashabaranov/go-openai issue with zero temp not included in api request due to how json unmarshal work. type NullableFloat32 *float32 // Temperature is just a tiny helper to create nullable float32 value from regular float32 func Temperature(v float32) NullableFloat32 { return &v } // nullableToFloat32 replaces nil with zero (in this case value won't be included in api request) // and for real zeros it returns math.SmallestNonzeroFloat32 that is as close to zero as possible. func nullableToFloat32(v NullableFloat32) float32 { if v == nil { return 0 } if *v == 0 { return math.SmallestNonzeroFloat32 } return *v } ================================================ FILE: providers/openai/helpers_test.go ================================================ package openai import ( "reflect" "testing" ) func TestEmbeddingToBytes(t *testing.T) { floats := []Embedding{{1.1, 2.2, 3.3}, {4.4, 5.5, 6.6}} bytes, err := EmbeddingToBytes(3, floats) if err != nil { t.Errorf("EmbeddingToBytes error %v", err) } newFloats, err := BytesToEmbedding(3, bytes) if err != nil { t.Errorf("EmbeddingToBytes error %v", err) } if !reflect.DeepEqual(floats, newFloats) { t.Errorf("floats and newFloats are not equal %v %v", floats, newFloats) } wrongFloats := []Embedding{{4.4, 5.5, 6.6, 7.7}} _, err = EmbeddingToBytes(3, wrongFloats) if err == nil { t.Errorf("EmbeddingToBytes should has error") } bytes, err = EmbeddingToBytes(4, wrongFloats) if err != nil { t.Errorf("EmbeddingToBytes error %v", err) } _, err = BytesToEmbedding(3, bytes) if err == nil { t.Errorf("BytesToEmbedding should has error") } } ================================================ FILE: providers/openai/image_to_text.go ================================================ package openai import ( "context" "encoding/base64" "errors" "fmt" "github.com/neurocult/agency" "github.com/sashabaranov/go-openai" ) type ImageToTextParams struct { Model string MaxTokens int Temperature NullableFloat32 TopP NullableFloat32 FrequencyPenalty NullableFloat32 PresencePenalty NullableFloat32 } // ImageToText is an operation builder that creates operation than can convert image to text. func (f *Provider) ImageToText(params ImageToTextParams) *agency.Operation { return agency.NewOperation(func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { openaiMsg := openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleUser, MultiContent: make([]openai.ChatMessagePart, 0, len(cfg.Messages)+2), } openaiMsg.MultiContent = append(openaiMsg.MultiContent, openai.ChatMessagePart{ Type: openai.ChatMessagePartTypeText, Text: cfg.Prompt, }) for _, cfgMsg := range cfg.Messages { openaiMsg.MultiContent = append( openaiMsg.MultiContent, openAIBase64ImageMessage(cfgMsg.Content()), ) } openaiMsg.MultiContent = append( openaiMsg.MultiContent, openAIBase64ImageMessage(msg.Content()), ) resp, err := f.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ MaxTokens: params.MaxTokens, Model: params.Model, Messages: []openai.ChatCompletionMessage{openaiMsg}, Temperature: nullableToFloat32(params.Temperature), TopP: nullableToFloat32(params.TopP), FrequencyPenalty: nullableToFloat32(params.FrequencyPenalty), PresencePenalty: nullableToFloat32(params.PresencePenalty), }) if err != nil { return nil, err } if len(resp.Choices) < 1 { return nil, errors.New("no choice") } choice := resp.Choices[0].Message return agency.NewMessage(agency.AssistantRole, agency.TextKind, []byte(choice.Content)), nil }) } func openAIBase64ImageMessage(bb []byte) openai.ChatMessagePart { imgBase64Str := base64.StdEncoding.EncodeToString(bb) return openai.ChatMessagePart{ Type: openai.ChatMessagePartTypeImageURL, ImageURL: &openai.ChatMessageImageURL{ URL: fmt.Sprintf("data:image/jpeg;base64,%s", imgBase64Str), Detail: openai.ImageURLDetailAuto, }, } } ================================================ FILE: providers/openai/provider.go ================================================ package openai import ( "github.com/sashabaranov/go-openai" ) // Provider is a set of operation builders. type Provider struct { client *openai.Client } // Params is a set of parameters specific for creating this concrete provider. // They are shared across all operation builders. type Params struct { Key string // Required if not using local LLM. BaseURL string // Optional. If not set then default openai base url is used } // New creates a new Provider instance. func New(params Params) *Provider { cfg := openai.DefaultConfig(params.Key) if params.BaseURL != "" { cfg.BaseURL = params.BaseURL } return &Provider{ client: openai.NewClientWithConfig(cfg), } } ================================================ FILE: providers/openai/speech_to_text.go ================================================ package openai import ( "bytes" "context" "github.com/neurocult/agency" "github.com/sashabaranov/go-openai" ) type SpeechToTextParams struct { Model string Temperature NullableFloat32 } // SpeechToText is an operation builder that creates operation than can convert speech to text. func (f Provider) SpeechToText(params SpeechToTextParams) *agency.Operation { return agency.NewOperation( func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { resp, err := f.client.CreateTranscription(ctx, openai.AudioRequest{ Model: params.Model, Prompt: cfg.Prompt, FilePath: "speech.ogg", Reader: bytes.NewReader(msg.Content()), Temperature: nullableToFloat32(params.Temperature), }) if err != nil { return nil, err } return agency.NewMessage(agency.AssistantRole, agency.TextKind, []byte(resp.Text)), nil }, ) } ================================================ FILE: providers/openai/text_to_embedding.go ================================================ package openai import ( "context" "fmt" "github.com/sashabaranov/go-openai" "github.com/neurocult/agency" ) type EmbeddingModel = openai.EmbeddingModel const AdaEmbeddingV2 EmbeddingModel = openai.AdaEmbeddingV2 type TextToEmbeddingParams struct { Model EmbeddingModel Dimensions EmbeddingDimensions } type EmbeddingDimensions *int func NewDimensions(v int) EmbeddingDimensions { return &v } func (p Provider) TextToEmbedding(params TextToEmbeddingParams) *agency.Operation { var dimensions int if params.Dimensions != nil { dimensions = *params.Dimensions } return agency.NewOperation(func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { // TODO: we have to convert string to model and then model to string. Can we optimize it? messages := append(cfg.Messages, msg) texts := make([]string, len(messages)) for i, m := range messages { texts[i] = string(m.Content()) } resp, err := p.client.CreateEmbeddings( ctx, openai.EmbeddingRequest{ Input: texts, Model: params.Model, Dimensions: dimensions, }, ) if err != nil { return nil, err } vectors := make([]Embedding, len(resp.Data)) for i, vector := range resp.Data { vectors[i] = vector.Embedding } bytes, err := EmbeddingToBytes(1536, vectors) if err != nil { return nil, fmt.Errorf("failed to convert embedding to bytes: %w", err) } // TODO: we have to convert []float32 to []byte. Can we optimize it? return agency.NewMessage(agency.AssistantRole, agency.EmbeddingKind, bytes), nil }) } ================================================ FILE: providers/openai/text_to_image.go ================================================ package openai import ( "context" "encoding/base64" "fmt" "github.com/neurocult/agency" "github.com/sashabaranov/go-openai" ) type TextToImageParams struct { Model string ImageSize string Quality string Style string } // TextToImage is an operation builder that creates operation than can convert text to image. func (p Provider) TextToImage(params TextToImageParams) *agency.Operation { return agency.NewOperation( func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { reqBase64 := openai.ImageRequest{ Prompt: fmt.Sprintf("%s\n\n%s", cfg.Prompt, string(msg.Content())), Size: params.ImageSize, ResponseFormat: openai.CreateImageResponseFormatB64JSON, N: 1, // DALL·E-3 only support n=1, for other models support needed Model: params.Model, Quality: params.Quality, Style: params.Style, } respBase64, err := p.client.CreateImage(ctx, reqBase64) if err != nil { return nil, err } imgBytes, err := base64.StdEncoding.DecodeString(respBase64.Data[0].B64JSON) if err != nil { return nil, err } return agency.NewMessage(agency.AssistantRole, agency.ImageKind, imgBytes), nil }, ) } ================================================ FILE: providers/openai/text_to_speech.go ================================================ package openai import ( "context" "io" "github.com/neurocult/agency" "github.com/sashabaranov/go-openai" ) type TextToSpeechParams struct { Model string ResponseFormat string Speed float64 Voice string } // TextToSpeech is an operation builder that creates operation than can convert text to speech. func (f Provider) TextToSpeech(params TextToSpeechParams) *agency.Operation { return agency.NewOperation( func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { resp, err := f.client.CreateSpeech(ctx, openai.CreateSpeechRequest{ Model: openai.SpeechModel(params.Model), Input: string(msg.Content()), Voice: openai.SpeechVoice(params.Voice), ResponseFormat: openai.SpeechResponseFormat(params.ResponseFormat), Speed: params.Speed, }) if err != nil { return nil, err } bb, err := io.ReadAll(resp) if err != nil { return nil, err } return agency.NewMessage(agency.AssistantRole, agency.VoiceKind, bb), nil }, ) } ================================================ FILE: providers/openai/text_to_stream.go ================================================ package openai import ( "context" "errors" "fmt" "io" "github.com/neurocult/agency" "github.com/sashabaranov/go-openai" ) type TextToStreamParams struct { TextToTextParams StreamHandler func(delta, total string, isFirst, isLast bool) error } func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { openAITools := castFuncDefsToOpenAITools(params.FuncDefs) return agency.NewOperation( func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { openAIMessages, err := agencyToOpenaiMessages(cfg, msg) if err != nil { return nil, fmt.Errorf("text to stream: %w", err) } for { // streaming loop openAIResponse, err := p.client.CreateChatCompletionStream( ctx, openai.ChatCompletionRequest{ Model: params.Model, Temperature: nullableToFloat32(params.Temperature), MaxTokens: params.MaxTokens, Messages: openAIMessages, Tools: openAITools, Stream: params.StreamHandler != nil, ToolChoice: params.ToolCallRequired(), Seed: params.Seed, ResponseFormat: params.Format, }, ) if err != nil { return nil, fmt.Errorf("create chat completion stream: %w", err) } var content string var accumulatedStreamedFunctions = make([]openai.ToolCall, 0, len(openAITools)) var isFirstDelta = true var isLastDelta = false var lastDelta string for { recv, err := openAIResponse.Recv() isLastDelta = errors.Is(err, io.EOF) if len(lastDelta) > 0 || (isLastDelta && len(content) > 0) { if err = params.StreamHandler(lastDelta, content, isFirstDelta, isLastDelta); err != nil { return nil, fmt.Errorf("handing stream: %w", err) } isFirstDelta = false } if isLastDelta { if len(accumulatedStreamedFunctions) == 0 { return agency.NewTextMessage( agency.AssistantRole, content, ), nil } break } if err != nil { return nil, err } if len(recv.Choices) < 1 { return nil, errors.New("no choice") } firstChoice := recv.Choices[0] if len(firstChoice.Delta.Content) > 0 { lastDelta = firstChoice.Delta.Content content += lastDelta } else { lastDelta = "" } for index, toolCall := range firstChoice.Delta.ToolCalls { if len(accumulatedStreamedFunctions) < index+1 { accumulatedStreamedFunctions = append(accumulatedStreamedFunctions, openai.ToolCall{ Index: toolCall.Index, ID: toolCall.ID, Type: toolCall.Type, Function: openai.FunctionCall{ Name: toolCall.Function.Name, Arguments: toolCall.Function.Arguments, }, }) } accumulatedStreamedFunctions[index].Function.Arguments += toolCall.Function.Arguments } if firstChoice.FinishReason != openai.FinishReasonToolCalls { continue } // Saving tool call to history openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleAssistant, ToolCalls: accumulatedStreamedFunctions, }) for _, call := range accumulatedStreamedFunctions { toolResponse, err := callTool(ctx, call, params.FuncDefs) if err != nil { return nil, fmt.Errorf("text to text call tool: %w", err) } if toolResponse.Role() != agency.ToolRole { return toolResponse, nil } openAIMessages = append(openAIMessages, toolMessageToOpenAI(toolResponse, call.ID)) } } openAIResponse.Close() } }, ) } func messageToOpenAI(message agency.Message) openai.ChatCompletionMessage { wrappedMessage := openai.ChatCompletionMessage{ Role: string(message.Role()), } switch message.Kind() { case agency.ImageKind: wrappedMessage.MultiContent = append( wrappedMessage.MultiContent, openAIBase64ImageMessage(message.Content()), ) default: wrappedMessage.Content = string(message.Content()) } return wrappedMessage } func toolMessageToOpenAI(message agency.Message, toolID string) openai.ChatCompletionMessage { wrappedMessage := messageToOpenAI(message) wrappedMessage.ToolCallID = toolID return wrappedMessage } ================================================ FILE: providers/openai/text_to_text.go ================================================ package openai import ( "context" "errors" "fmt" "github.com/sashabaranov/go-openai" "github.com/neurocult/agency" ) // TextToTextParams represents parameters that are specific for this operation. type TextToTextParams struct { Model string Temperature NullableFloat32 MaxTokens int FuncDefs []FuncDef Seed *int IsToolsCallRequired bool Format *openai.ChatCompletionResponseFormat } func (p TextToTextParams) ToolCallRequired() *string { var toolChoice *string if p.IsToolsCallRequired { v := "required" toolChoice = &v } return toolChoice } // TextToText is an operation builder that creates operation than can convert text to text. // It can also call provided functions if needed, as many times as needed until the final answer is generated. func (p Provider) TextToText(params TextToTextParams) *agency.Operation { openAITools := castFuncDefsToOpenAITools(params.FuncDefs) return agency.NewOperation( func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { openAIMessages, err := agencyToOpenaiMessages(cfg, msg) if err != nil { return nil, fmt.Errorf("text to stream: %w", err) } for { openAIResponse, err := p.client.CreateChatCompletion( ctx, openai.ChatCompletionRequest{ Model: params.Model, Temperature: nullableToFloat32(params.Temperature), MaxTokens: params.MaxTokens, Messages: openAIMessages, Tools: openAITools, Seed: params.Seed, ToolChoice: params.ToolCallRequired(), ResponseFormat: params.Format, }, ) if err != nil { return nil, err } if len(openAIResponse.Choices) == 0 { return nil, errors.New("get text to text response: no choice") } responseMessage := openAIResponse.Choices[0].Message if len(responseMessage.ToolCalls) == 0 { return OpenaiToAgencyMessage(responseMessage), nil } openAIMessages = append(openAIMessages, responseMessage) for _, call := range responseMessage.ToolCalls { toolResponse, err := callTool(ctx, call, params.FuncDefs) if err != nil { return nil, fmt.Errorf("text to text call tool: %w", err) } if toolResponse.Role() != agency.ToolRole { return toolResponse, nil } openAIMessages = append(openAIMessages, toolMessageToOpenAI(toolResponse, call.ID)) } } }, ) } // === Helpers === func castFuncDefsToOpenAITools(funcDefs []FuncDef) []openai.Tool { tools := make([]openai.Tool, 0, len(funcDefs)) for _, f := range funcDefs { tool := openai.Tool{ Type: openai.ToolTypeFunction, Function: &openai.FunctionDefinition{ Name: f.Name, Description: f.Description, }, } if f.Parameters != nil { tool.Function.Parameters = f.Parameters } tools = append(tools, tool) } return tools } func agencyToOpenaiMessages(cfg *agency.OperationConfig, msg agency.Message) ([]openai.ChatCompletionMessage, error) { openAIMessages := make([]openai.ChatCompletionMessage, 0, len(cfg.Messages)+2) openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleSystem, Content: cfg.Prompt, }) for _, cfgMsg := range cfg.Messages { openAIMessages = append(openAIMessages, messageToOpenAI(cfgMsg)) } openaiMsg := openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleUser, } switch msg.Kind() { case agency.TextKind: openaiMsg.Content = string(msg.Content()) case agency.ImageKind: openaiMsg.MultiContent = append( openaiMsg.MultiContent, openAIBase64ImageMessage(msg.Content()), ) default: return nil, fmt.Errorf("operator doesn't support %s kind", msg.Kind()) } openAIMessages = append(openAIMessages, openaiMsg) return openAIMessages, nil } func callTool( ctx context.Context, call openai.ToolCall, defs FuncDefs, ) (agency.Message, error) { funcToCall := defs.getFuncDefByName(call.Function.Name) if funcToCall == nil { return nil, errors.New("function not found") } funcResult, err := funcToCall.Body(ctx, []byte(call.Function.Arguments)) if err != nil { return funcResult, fmt.Errorf("call function %s: %w", funcToCall.Name, err) } return funcResult, nil } func OpenaiToAgencyMessage(msg openai.ChatCompletionMessage) agency.Message { return agency.NewTextMessage( agency.Role(msg.Role), msg.Content, ) } ================================================ FILE: providers/openai/tools.go ================================================ package openai import ( "context" "github.com/neurocult/agency" "github.com/sashabaranov/go-openai/jsonschema" ) type ToolResultMessage struct { agency.Message ToolID string ToolName string } // FuncDef represents a function definition that can be called during the conversation. type FuncDef struct { Name string Description string // Parameters is an optional structure that defines the schema of the parameters that the function accepts. Parameters *jsonschema.Definition // Body is the actual function that get's called. // Parameters passed are bytes that can be unmarshalled to type that implements provided json schema. // Returned result must be anything that can be marshalled, including primitive values. Body func(ctx context.Context, params []byte) (agency.Message, error) } type FuncDefs []FuncDef func (ds FuncDefs) getFuncDefByName(name string) *FuncDef { for _, f := range ds { if f.Name == name { return &f } } return nil }