[
  {
    "path": ".gitignore",
    "content": "# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore\n#\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\n# Go workspace file\ngo.work\n\n# ide\n.idea\n.vscode\n\n# env\n.env\n\n# generation artifacts\nexample.mp3\nspeech.ogg\nexample.png\nspeech.mp3\n\n# mac os\n.DS_Store"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) Meta Platforms, Inc. and affiliates.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# Agency: The Go Way to AI\n\nLibrary 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.\n\n**Welcome to the agency!** 🕵️‍♂️\n\n![Dracula-agent, mascot of the \"agency\" library.](./assets/dracula.png)\n\n## 💻 Quick Start\n\nInstall package:\n\n```bash\ngo get github.com/neurocult/agency\n```\n\nChat example:\n\n```go\npackage main\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\nfunc main() {\n\tassistant := openai.\n\t\tNew(openai.Params{Key: os.Getenv(\"OPENAI_API_KEY\")}).\n\t\tTextToText(openai.TextToTextParams{Model: \"gpt-4o-mini\"}).\n\t\tSetPrompt(\"You are helpful assistant.\")\n\n\tmessages := []agency.Message{}\n\treader := bufio.NewReader(os.Stdin)\n\tctx := context.Background()\n\n\tfor {\n\t\tfmt.Print(\"User: \")\n\n\t\ttext, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tinput := agency.NewTextMessage(agency.UserRole, text)\n\t\tanswer, err := assistant.SetMessages(messages).Execute(ctx, input)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tfmt.Println(\"Assistant:\", string(answer.Content()))\n\n\t\tmessages = append(messages, input, answer)\n\t}\n}\n```\n\nThat's it!\n\nSee [examples](./examples/) to find out more complex usecases including RAGs and multimodal operations.\n\n## 🚀 Features\n\n✨ **Pure Go**: fast and lightweight, statically typed, no need to mess with Python or JavaScript\n\n✨ Write **clean code** and follow **clean architecture** by separating business logic from concrete implementations\n\n✨ Easily create **custom operations** by implementing simple interface\n\n✨ **Compose operations** together into **processes** with the ability to observe each step via **interceptors**\n\n✨ **OpenAI API bindings** (can be used for any openai-compatable API: text to text (completion), text to image, text to speech, speech to text\n\n<!-- TODO v0.1.0\n- [ ] Name the organization\n- [ ] Reorganize folders and packages -->\n\n## 🤔 Why need Agency?\n\nAt 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.\n\nIn 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.\n\nOur 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.\n\n## Tutorial\n\n- [Part 1](https://dev.to/emil14/agency-the-go-way-to-ai-part-1-1lhe) ([Russian translation](https://habr.com/ru/sandbox/204508/))\n\n## 🛣 Roadmap\n\nIn the next versions:\n\n- [x] Support for external function calls\n- [ ] Metadata (tokens used, audio duration, etc)\n- [ ] More provider-adapters, not only openai\n- [x] Image to text operations\n- [ ] Powerful API for autonomous agents\n- [ ] Tagging and JSON output parser\n"
  },
  {
    "path": "agency.go",
    "content": "package agency\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\n// Operation is basic building block.\ntype Operation struct {\n\thandler OperationHandler\n\tconfig  *OperationConfig\n}\n\n// OperationHandler is a function that implements operation's logic.\n// It could be thought of as an interface that providers must implement.\ntype OperationHandler func(context.Context, Message, *OperationConfig) (Message, error)\n\n// OperationConfig represents abstract operation configuration for all possible models.\ntype OperationConfig struct {\n\tPrompt   string\n\tMessages []Message\n}\n\nfunc (p *Operation) Config() *OperationConfig {\n\treturn p.config\n}\n\n// NewOperation allows to create an operation from a function.\nfunc NewOperation(handler OperationHandler) *Operation {\n\treturn &Operation{\n\t\thandler: handler,\n\t\tconfig:  &OperationConfig{},\n\t}\n}\n\n// Execute executes operation handler with input message and current configuration.\nfunc (p *Operation) Execute(ctx context.Context, input Message) (Message, error) {\n\toutput, err := p.handler(ctx, input, p.config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn output, nil\n}\n\nfunc (p *Operation) SetPrompt(prompt string, args ...any) *Operation {\n\tp.config.Prompt = fmt.Sprintf(prompt, args...)\n\treturn p\n}\n\nfunc (p *Operation) SetMessages(msgs []Message) *Operation {\n\tp.config.Messages = msgs\n\treturn p\n}\n"
  },
  {
    "path": "examples/README.md",
    "content": "To run an example:\n\n```shell\nexport OPENAI_API_KEY=\"<your key here>\"\ngo run ./example_name\n```\n"
  },
  {
    "path": "examples/chat/main.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\nfunc main() {\n\tassistant := openai.\n\t\tNew(openai.Params{Key: os.Getenv(\"OPENAI_API_KEY\")}).\n\t\tTextToText(openai.TextToTextParams{Model: \"gpt-4o-mini\"}).\n\t\tSetPrompt(\"You are helpful assistant.\")\n\n\tmessages := []agency.Message{}\n\treader := bufio.NewReader(os.Stdin)\n\tctx := context.Background()\n\n\tfor {\n\t\tfmt.Print(\"User: \")\n\n\t\ttext, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tinput := agency.NewTextMessage(agency.UserRole, text)\n\t\tanswer, err := assistant.SetMessages(messages).Execute(ctx, input)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tfmt.Println(\"Assistant:\", string(answer.Content()))\n\n\t\tmessages = append(messages, input, answer)\n\t}\n}\n"
  },
  {
    "path": "examples/cli/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\n// usage example: go to the repo root and execute\n// 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\"\nfunc main() {\n\tprovider := openai.New(openai.Params{Key: os.Getenv(\"OPENAI_API_KEY\")})\n\n\ttemp := flag.Float64(\"temp\", 0.0, \"Temperature value\")\n\tmaxTokens := flag.Int(\"maxTokens\", 0, \"Maximum number of tokens\")\n\tmodel := flag.String(\"model\", \"gpt-4o-mini\", \"Model name\")\n\tprompt := flag.String(\"prompt\", \"You are a helpful assistant\", \"System message\")\n\n\tflag.Parse()\n\n\targs := flag.Args()\n\tif len(args) < 1 {\n\t\tfmt.Println(\"content argument is required\")\n\t\tos.Exit(1)\n\t}\n\tcontent := args[0]\n\n\tresult, err := provider.\n\t\tTextToText(openai.TextToTextParams{\n\t\t\tModel:       *model,\n\t\t\tTemperature: openai.Temperature(float32(*temp)),\n\t\t\tMaxTokens:   *maxTokens,\n\t\t}).\n\t\tSetPrompt(*prompt).\n\t\tExecute(context.Background(), agency.NewMessage(agency.UserRole, agency.TextKind, []byte(content)))\n\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\tos.Exit(1)\n\t}\n\n\tfmt.Println(result.Content())\n}\n"
  },
  {
    "path": "examples/custom_operation/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/neurocult/agency\"\n)\n\nfunc main() {\n\tincrement := agency.NewOperation(incrementFunc)\n\n\tmsg, err := agency.NewProcess(\n\t\tincrement,\n\t\tincrement,\n\t\tincrement,\n\t).Execute(\n\t\tcontext.Background(),\n\t\tagency.NewMessage(\n\t\t\tagency.UserRole,\n\t\t\tagency.TextKind,\n\t\t\t[]byte(\"0\"),\n\t\t),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(string(msg.Content()))\n}\n\nfunc incrementFunc(ctx context.Context, msg agency.Message, _ *agency.OperationConfig) (agency.Message, error) {\n\ti, err := strconv.ParseInt(string(msg.Content()), 10, 10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tinc := strconv.Itoa(int(i) + 1)\n\treturn agency.NewMessage(agency.ToolRole, agency.TextKind, []byte(inc)), nil\n}\n"
  },
  {
    "path": "examples/func_call/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\tgo_openai \"github.com/sashabaranov/go-openai\"\n\t\"github.com/sashabaranov/go-openai/jsonschema\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\nfunc main() {\n\tt2tOp := openai.\n\t\tNew(openai.Params{Key: os.Getenv(\"OPENAI_API_KEY\")}).\n\t\tTextToText(openai.TextToTextParams{\n\t\t\tModel: go_openai.GPT4oMini,\n\t\t\tFuncDefs: []openai.FuncDef{\n\t\t\t\t// function without parameters\n\t\t\t\t{\n\t\t\t\t\tName:        \"GetMeaningOfLife\",\n\t\t\t\t\tDescription: \"Answer questions about meaning of life\",\n\t\t\t\t\tBody: func(ctx context.Context, _ []byte) (agency.Message, error) {\n\t\t\t\t\t\t// because we don't need any arguments\n\t\t\t\t\t\treturn agency.NewTextMessage(agency.ToolRole, \"42\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t// function with parameters\n\t\t\t\t{\n\t\t\t\t\tName:        \"ChangeNumbers\",\n\t\t\t\t\tDescription: \"Change given numbers when asked\",\n\t\t\t\t\tParameters: &jsonschema.Definition{\n\t\t\t\t\t\tType: \"object\",\n\t\t\t\t\t\tProperties: map[string]jsonschema.Definition{\n\t\t\t\t\t\t\t\"a\": {Type: \"integer\"},\n\t\t\t\t\t\t\t\"b\": {Type: \"integer\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tBody: func(ctx context.Context, params []byte) (agency.Message, error) {\n\t\t\t\t\t\tvar pp struct{ A, B int }\n\t\t\t\t\t\tif err := json.Unmarshal(params, &pp); err != nil {\n\t\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn agency.NewTextMessage(\n\t\t\t\t\t\t\tagency.ToolRole,\n\t\t\t\t\t\t\tfmt.Sprintf(\"%d\", (pp.A+pp.B)*10),\n\t\t\t\t\t\t), nil // *10 is just to distinguish from normal response\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}).\n\t\tSetPrompt(`\nAnswer questions about meaning of life and summing numbers.\nAlways use GetMeaningOfLife and ChangeNumbers functions results as answers.\nExamples:\n- User: what is the meaning of life?\n- Assistant: 42\n- User: 1+1\n- Assistant: 20\n- User: 1+1 and what is the meaning of life?\n- Assistant: 20 and 42`)\n\n\tctx := context.Background()\n\n\t// test for first function call\n\tanswer, err := t2tOp.Execute(\n\t\tctx,\n\t\tagency.NewMessage(agency.UserRole, agency.TextKind, []byte(\"what is the meaning of life?\")),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tprintAnswer(answer)\n\n\t// test for second function call\n\tanswer, err = t2tOp.Execute(\n\t\tctx,\n\t\tagency.NewMessage(agency.UserRole, agency.TextKind, []byte(\"1+1?\")),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tprintAnswer(answer)\n\n\t// test for both function calls at the same time\n\tanswer, err = t2tOp.Execute(\n\t\tctx,\n\t\tagency.NewMessage(agency.UserRole, agency.TextKind, []byte(\"1+1 and what is the meaning of life?\")),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tprintAnswer(answer)\n}\n\nfunc printAnswer(message agency.Message) {\n\tfmt.Printf(\n\t\t\"Role: %s; Type: %s; Data: %s\\n\",\n\t\tmessage.Role(),\n\t\tmessage.Kind(),\n\t\tstring(message.Content()),\n\t)\n}\n"
  },
  {
    "path": "examples/image_to_stream/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\t\"github.com/neurocult/agency\"\n\n\tproviders \"github.com/neurocult/agency/providers/openai\"\n)\n\nfunc main() {\n\timgBytes, err := os.ReadFile(\"example.png\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = providers.New(providers.Params{Key: os.Getenv(\"OPENAI_API_KEY\")}).\n\t\tTextToStream(providers.TextToStreamParams{\n\t\t\tTextToTextParams: providers.TextToTextParams{MaxTokens: 300, Model: \"gpt-4o\"},\n\t\t\tStreamHandler: func(delta, total string, isFirst, isLast bool) error {\n\t\t\t\tfmt.Println(delta)\n\t\t\t\treturn nil\n\t\t\t}}).\n\t\tSetPrompt(\"describe what you see\").\n\t\tExecute(\n\t\t\tcontext.Background(),\n\t\t\tagency.NewMessage(agency.UserRole, agency.ImageKind, imgBytes),\n\t\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "examples/image_to_text/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\t\"github.com/neurocult/agency\"\n\topenAIProvider \"github.com/neurocult/agency/providers/openai\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc main() { \n\timgBytes, err := os.ReadFile(\"example.png\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tresult, err := openAIProvider.New(openAIProvider.Params{Key: os.Getenv(\"OPENAI_API_KEY\")}).\n\t\tImageToText(openAIProvider.ImageToTextParams{Model: openai.GPT4o, MaxTokens: 300}).\n\t\tSetPrompt(\"describe what you see\").\n\t\tExecute(\n\t\t\tcontext.Background(),\n\t\t\tagency.NewMessage(agency.UserRole, agency.ImageKind, imgBytes),\n\t\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(string(result.Content()))\n}\n"
  },
  {
    "path": "examples/logging/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\nfunc main() {\n\tfactory := openai.New(openai.Params{Key: os.Getenv(\"OPENAI_API_KEY\")})\n\tparams := openai.TextToTextParams{Model: \"gpt-4o-mini\"}\n\n\t_, err := agency.NewProcess(\n\t\tfactory.TextToText(params).SetPrompt(\"explain what that means\"),\n\t\tfactory.TextToText(params).SetPrompt(\"translate to russian\"),\n\t\tfactory.TextToText(params).SetPrompt(\"replace all spaces with '_'\"),\n\t).\n\t\tExecute(\n\t\t\tcontext.Background(),\n\t\t\tagency.NewMessage(agency.UserRole, agency.TextKind, []byte(\"Kazakhstan alga!\")),\n\t\t\tLogger,\n\t\t)\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Logger(input, output agency.Message, cfg *agency.OperationConfig) {\n\tfmt.Printf(\n\t\t\"in: %v\\nprompt: %v\\nout: %v\\n\\n\",\n\t\tstring(input.Content()),\n\t\tcfg.Prompt,\n\t\tstring(output.Content()),\n\t)\n}\n"
  },
  {
    "path": "examples/prompt_template/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\nfunc main() {\n\tfactory := openai.New(openai.Params{Key: os.Getenv(\"OPENAI_API_KEY\")})\n\n\tresultMsg, err := factory.\n\t\tTextToText(openai.TextToTextParams{Model: \"gpt-4o-mini\"}).\n\t\tSetPrompt(\n\t\t\t\"You are a helpful assistant that translates %s to %s\",\n\t\t\t\"English\", \"French\",\n\t\t).\n\t\tExecute(\n\t\t\tcontext.Background(),\n\t\t\tagency.NewMessage(agency.UserRole, agency.TextKind, []byte(\"I love programming.\")),\n\t\t)\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(string(resultMsg.Content()))\n}\n"
  },
  {
    "path": "examples/rag_vector_database/data.go",
    "content": "package main\n\nimport \"github.com/weaviate/weaviate/entities/models\"\n\n// first 5 objects contain something about programming without word 'programming' while other are random topics.\nvar data []*models.Object = []*models.Object{\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Debugging Java applications can be challenging.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Writing code in Python is both fun and efficient.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"HTML and CSS are the building blocks of web development.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Using version control systems like Git is essential for collaborative coding.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Understanding algorithms is crucial for effective software development.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Swimming is a fun and beneficial form of exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Playing musical instruments can be both challenging and fulfilling.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Traveling to new places is an adventure worth having.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Cycling is both an eco-friendly transport and a great way to exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Playing musical instruments can be both challenging and fulfilling.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Learning a new language opens up a world of opportunities.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Playing musical instruments can be both challenging and fulfilling.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Cycling is both an eco-friendly transport and a great way to exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Traveling to new places is an adventure worth having.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Meditation helps in reducing stress and improving focus.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Cycling is both an eco-friendly transport and a great way to exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Playing musical instruments can be both challenging and fulfilling.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Cycling is both an eco-friendly transport and a great way to exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Traveling to new places is an adventure worth having.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Gardening is a peaceful and rewarding hobby.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Running is a simple and effective way to stay fit.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Playing musical instruments can be both challenging and fulfilling.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Hiking in nature is both invigorating and relaxing.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"The art of cooking is a blend of flavors and techniques.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Watching movies is a popular form of entertainment.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Traveling to new places is an adventure worth having.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Practicing yoga promotes physical and mental well-being.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Reading books is a journey through knowledge and imagination.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"The art of cooking is a blend of flavors and techniques.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Running is a simple and effective way to stay fit.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Practicing yoga promotes physical and mental well-being.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Photography captures moments and memories.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Cycling is both an eco-friendly transport and a great way to exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Gardening is a peaceful and rewarding hobby.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Traveling to new places is an adventure worth having.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Swimming is a fun and beneficial form of exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Reading books is a journey through knowledge and imagination.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Painting allows for creative expression and relaxation.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"The art of cooking is a blend of flavors and techniques.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Photography captures moments and memories.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Reading books is a journey through knowledge and imagination.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Running is a simple and effective way to stay fit.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Playing musical instruments can be both challenging and fulfilling.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Hiking in nature is both invigorating and relaxing.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Learning a new language opens up a world of opportunities.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Painting allows for creative expression and relaxation.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Hiking in nature is both invigorating and relaxing.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"The art of cooking is a blend of flavors and techniques.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Meditation helps in reducing stress and improving focus.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Practicing yoga promotes physical and mental well-being.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Swimming is a fun and beneficial form of exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Traveling to new places is an adventure worth having.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Photography captures moments and memories.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Practicing yoga promotes physical and mental well-being.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Running is a simple and effective way to stay fit.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Hiking in nature is both invigorating and relaxing.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Swimming is a fun and beneficial form of exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Running is a simple and effective way to stay fit.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Meditation helps in reducing stress and improving focus.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Hiking in nature is both invigorating and relaxing.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Photography captures moments and memories.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Running is a simple and effective way to stay fit.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Cycling is both an eco-friendly transport and a great way to exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Practicing yoga promotes physical and mental well-being.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Practicing yoga promotes physical and mental well-being.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Hiking in nature is both invigorating and relaxing.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Cycling is both an eco-friendly transport and a great way to exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Hiking in nature is both invigorating and relaxing.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Reading books is a journey through knowledge and imagination.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Watching movies is a popular form of entertainment.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Hiking in nature is both invigorating and relaxing.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Playing musical instruments can be both challenging and fulfilling.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Traveling to new places is an adventure worth having.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Painting allows for creative expression and relaxation.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Traveling to new places is an adventure worth having.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Hiking in nature is both invigorating and relaxing.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Meditation helps in reducing stress and improving focus.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Watching movies is a popular form of entertainment.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Traveling to new places is an adventure worth having.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Photography captures moments and memories.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Learning a new language opens up a world of opportunities.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Painting allows for creative expression and relaxation.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Photography captures moments and memories.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Traveling to new places is an adventure worth having.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Practicing yoga promotes physical and mental well-being.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Traveling to new places is an adventure worth having.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Practicing yoga promotes physical and mental well-being.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"The art of cooking is a blend of flavors and techniques.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Running is a simple and effective way to stay fit.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Running is a simple and effective way to stay fit.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Photography captures moments and memories.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"The art of cooking is a blend of flavors and techniques.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Photography captures moments and memories.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Cycling is both an eco-friendly transport and a great way to exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Playing musical instruments can be both challenging and fulfilling.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"The art of cooking is a blend of flavors and techniques.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Swimming is a fun and beneficial form of exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Cycling is both an eco-friendly transport and a great way to exercise.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Traveling to new places is an adventure worth having.\"},\n\t},\n\t{\n\t\tClass:      \"Records\",\n\t\tProperties: map[string]string{\"content\": \"Painting allows for creative expression and relaxation.\"},\n\t},\n}\n"
  },
  {
    "path": "examples/rag_vector_database/docker-compose.yaml",
    "content": "---\nversion: \"3.4\"\nservices:\n  weaviate:\n    command:\n      - --host\n      - 0.0.0.0\n      - --port\n      - \"8080\"\n      - --scheme\n      - http\n    image: cr.weaviate.io/semitechnologies/weaviate:1.22.4\n    ports:\n      - 8080:8080\n      - 50051:50051\n    restart: on-failure:0\n    environment:\n      QUERY_DEFAULTS_LIMIT: 25\n      AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: \"true\"\n      PERSISTENCE_DATA_PATH: \"/var/lib/weaviate\"\n      DEFAULT_VECTORIZER_MODULE: \"none\"\n      ENABLE_MODULES: \"text2vec-cohere,text2vec-huggingface,text2vec-palm,text2vec-openai,generative-openai,generative-cohere,generative-palm,ref2vec-centroid,reranker-cohere,qna-openai\"\n      CLUSTER_HOSTNAME: \"node1\"\nvolumes:\n  weaviate_data:\n"
  },
  {
    "path": "examples/rag_vector_database/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\t\"github.com/neurocult/agency\"\n\t\"github.com/weaviate/weaviate-go-client/v4/weaviate\"\n\t\"github.com/weaviate/weaviate-go-client/v4/weaviate/graphql\"\n\t\"github.com/weaviate/weaviate/entities/models\"\n\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\n// natural langauge query -> weaviate RAG -> speech\nfunc main() {\n\topenAPIKey := os.Getenv(\"OPENAI_API_KEY\")\n\n\tctx := context.Background()\n\n\tclient, err := prepareDB(openAPIKey, ctx)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfactory := openai.New(openai.Params{Key: openAPIKey})\n\tretrieve := RAGoperation(client)\n\tsummarize := factory.TextToText(openai.TextToTextParams{Model: \"gpt-4o-mini\"}).SetPrompt(\"summarize\")\n\tvoice := factory.TextToSpeech(openai.TextToSpeechParams{\n\t\tModel: \"tts-1\", ResponseFormat: \"mp3\", Speed: 1, Voice: \"onyx\",\n\t})\n\n\tresult, err := agency.NewProcess(\n\t\tretrieve,\n\t\tsummarize,\n\t\tvoice,\n\t).Execute(ctx, agency.NewMessage(agency.UserRole, agency.TextKind, []byte(\"programming\")))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := saveToDisk(result); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// RAGoperation retrieves relevant objects from vector store and builds a text message to pass further to the process\nfunc RAGoperation(client *weaviate.Client) *agency.Operation {\n\treturn agency.NewOperation(func(ctx context.Context, msg agency.Message, po *agency.OperationConfig) (agency.Message, error) {\n\t\tinput := string(msg.Content())\n\n\t\tresult, err := client.GraphQL().Get().\n\t\t\tWithClassName(\"Records\").\n\t\t\tWithFields(graphql.Field{Name: \"content\"}).\n\t\t\tWithNearText(\n\t\t\t\tclient.GraphQL().\n\t\t\t\t\tNearTextArgBuilder().\n\t\t\t\t\tWithConcepts(\n\t\t\t\t\t\t[]string{input},\n\t\t\t\t\t),\n\t\t\t).\n\t\t\tWithLimit(10).\n\t\t\tDo(ctx)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tvar content string\n\t\tfor _, obj := range result.Data {\n\t\t\tbb, err := json.Marshal(&obj)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tcontent += string(bb)\n\t\t}\n\n\t\treturn agency.NewMessage(\n\t\t\tagency.AssistantRole,\n\t\t\tagency.TextKind,\n\t\t\t[]byte(content),\n\t\t), nil\n\t})\n}\n\nfunc prepareDB(openAPIKey string, ctx context.Context) (*weaviate.Client, error) {\n\tclient, err := weaviate.NewClient(weaviate.Config{\n\t\tHost:   \"localhost:8080\",\n\t\tScheme: \"http\",\n\t\tHeaders: map[string]string{\n\t\t\t\"X-OpenAI-Api-Key\": openAPIKey,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := client.Schema().AllDeleter().Do(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\tclassObj := &models.Class{\n\t\tClass:      \"Records\",\n\t\tVectorizer: \"text2vec-openai\",\n\t\tModuleConfig: map[string]interface{}{\n\t\t\t\"text2vec-openai\":   map[string]interface{}{},\n\t\t\t\"generative-openai\": map[string]interface{}{},\n\t\t},\n\t}\n\tif err = client.Schema().ClassCreator().WithClass(classObj).Do(context.Background()); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif _, err := client.Batch().ObjectsBatcher().WithObjects(data...).Do(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n\nfunc saveToDisk(msg agency.Message) error {\n\tfile, err := os.Create(\"speech.mp3\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\t_, err = file.Write(msg.Content())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "examples/speech_to_text/main.go",
    "content": "// To make this example work make sure you have speech.ogg file in the root of directory.\n// You can use text to speech example to generate speech file.\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\tgoopenai \"github.com/sashabaranov/go-openai\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\nfunc main() {\n\tfactory := openai.New(openai.Params{Key: os.Getenv(\"OPENAI_API_KEY\")})\n\n\tdata, err := os.ReadFile(\"speech.mp3\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tresult, err := factory.SpeechToText(openai.SpeechToTextParams{\n\t\tModel: goopenai.Whisper1,\n\t}).Execute(\n\t\tcontext.Background(),\n\t\tagency.NewMessage(agency.UserRole, agency.VoiceKind, data),\n\t)\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(string(result.Content()))\n}\n"
  },
  {
    "path": "examples/speech_to_text_multi_model/main.go",
    "content": "// To make this example work make sure you have speech.ogg file in the root of directory\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\tgoopenai \"github.com/sashabaranov/go-openai\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\ntype Saver []agency.Message\n\nfunc (s *Saver) Save(input, output agency.Message, _ *agency.OperationConfig) {\n\t*s = append(*s, output)\n}\n\nfunc main() {\n\tfactory := openai.New(openai.Params{Key: os.Getenv(\"OPENAI_API_KEY\")})\n\n\t// step 1\n\thear := factory.\n\t\tSpeechToText(openai.SpeechToTextParams{\n\t\t\tModel: goopenai.Whisper1,\n\t\t})\n\n\t// step2\n\ttranslate := factory.\n\t\tTextToText(openai.TextToTextParams{\n\t\t\tModel:       \"gpt-4o-mini\",\n\t\t\tTemperature: openai.Temperature(0.5),\n\t\t}).\n\t\tSetPrompt(\"translate to russian\")\n\n\t// step 3\n\tuppercase := factory.\n\t\tTextToText(openai.TextToTextParams{\n\t\t\tModel:       \"gpt-4o-mini\",\n\t\t\tTemperature: openai.Temperature(1),\n\t\t}).\n\t\tSetPrompt(\"uppercase every letter of the text\")\n\n\tsaver := Saver{}\n\n\tsound, err := os.ReadFile(\"speech.mp3\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tctx := context.Background()\n\tspeechMsg := agency.NewMessage(agency.UserRole, agency.VoiceKind, sound)\n\n\t_, err = agency.NewProcess(\n\t\thear,\n\t\ttranslate,\n\t\tuppercase,\n\t).Execute(ctx, speechMsg, saver.Save)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor _, msg := range saver {\n\t\tfmt.Println(string(msg.Content()))\n\t}\n}\n"
  },
  {
    "path": "examples/speech_to_text_to_image/main.go",
    "content": "// To make this example work make sure you have speech.ogg file in the root of directory\npackage main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"image/png\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\tgoopenai \"github.com/sashabaranov/go-openai\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\nfunc main() {\n\tfactory := openai.New(openai.Params{Key: os.Getenv(\"OPENAI_API_KEY\")})\n\n\tdata, err := os.ReadFile(\"speech.mp3\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmsg, err := agency.NewProcess(\n\t\tfactory.SpeechToText(openai.SpeechToTextParams{Model: goopenai.Whisper1}),\n\t\tfactory.TextToImage(openai.TextToImageParams{\n\t\t\tModel:     goopenai.CreateImageModelDallE2,\n\t\t\tImageSize: goopenai.CreateImageSize256x256,\n\t\t}),\n\t).Execute(context.Background(), agency.NewMessage(agency.UserRole, agency.VoiceKind, data))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := saveImgToDisk(msg); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc saveImgToDisk(msg agency.Message) error {\n\tr := bytes.NewReader(msg.Content())\n\n\timgData, err := png.Decode(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfile, err := os.Create(\"example.png\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\tif err := png.Encode(file, imgData); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "examples/text_to_image_dalle2/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"image\"\n\t\"image/png\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\nfunc main() {\n\tprovider := openai.New(openai.Params{\n\t\tKey: os.Getenv(\"OPENAI_API_KEY\"),\n\t})\n\n\tresult, err := provider.TextToImage(openai.TextToImageParams{\n\t\tModel:     \"dall-e-2\",\n\t\tImageSize: \"512x512\",\n\t\tQuality:   \"standard\",\n\t\tStyle:     \"vivid\",\n\t}).Execute(\n\t\tcontext.Background(),\n\t\tagency.NewMessage(agency.UserRole, agency.TextKind, []byte(\"Halloween night at a haunted museum\")),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := saveToDisk(result); err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(\"Image has been saved!\")\n}\n\nfunc saveToDisk(msg agency.Message) error {\n\tr := bytes.NewReader(msg.Content())\n\n\t// for dall-e-3 use third party libraries due to lack of webp support in go stdlib\n\timgData, format, err := image.Decode(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfile, err := os.Create(\"example.\" + format)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\tif err := png.Encode(file, imgData); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "examples/text_to_speech/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\nfunc main() {\n\tinput := agency.NewMessage(\n\t\tagency.UserRole,\n\t\tagency.TextKind,\n\t\t[]byte(`One does not simply walk into Mordor.\nIts black gates are guarded by more than just Orcs.\nThere is evil there that does not sleep, and the Great Eye is ever watchful.`))\n\n\tmsg, err := openai.New(openai.Params{Key: os.Getenv(\"OPENAI_API_KEY\")}).\n\t\tTextToSpeech(openai.TextToSpeechParams{\n\t\t\tModel:          \"tts-1\",\n\t\t\tResponseFormat: \"mp3\",\n\t\t\tSpeed:          1,\n\t\t\tVoice:          \"alloy\",\n\t\t}).\n\t\tExecute(context.Background(), input)\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := saveToDisk(msg); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc saveToDisk(msg agency.Message) error {\n\tfile, err := os.Create(\"speech.mp3\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\n\t_, err = file.Write(msg.Content())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "examples/text_to_stream/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\tgoopenai \"github.com/sashabaranov/go-openai\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\nfunc main() {\n\tfactory := openai.New(openai.Params{Key: os.Getenv(\"OPENAI_API_KEY\")})\n\n\tresult, err := factory.\n\t\tTextToStream(openai.TextToStreamParams{\n\t\t\tTextToTextParams: openai.TextToTextParams{Model: goopenai.GPT4oMini},\n\t\t\tStreamHandler: func(delta, total string, isFirst, isLast bool) error {\n\t\t\t\tif isFirst {\n\t\t\t\t\tfmt.Println(\"====Start streaming====\")\n\t\t\t\t}\n\t\t\t\tfmt.Print(delta)\n\t\t\t\tif isLast {\n\t\t\t\t\tfmt.Println(\"\\n====Finish streaming====\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}).\n\t\tSetPrompt(\"Write a few sentences about topic\").\n\t\tExecute(\n\t\t\tcontext.Background(),\n\t\t\tagency.NewMessage(\n\t\t\t\tagency.UserRole,\n\t\t\t\tagency.TextKind,\n\t\t\t\t[]byte(\"I love programming.\"),\n\t\t\t),\n\t\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(\"\\nResult:\", string(result.Content()))\n}\n"
  },
  {
    "path": "examples/translate_text/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t_ \"github.com/joho/godotenv/autoload\"\n\tgoopenai \"github.com/sashabaranov/go-openai\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/neurocult/agency/providers/openai\"\n)\n\nfunc main() {\n\tfactory := openai.New(openai.Params{Key: os.Getenv(\"OPENAI_API_KEY\")})\n\n\tresult, err := factory.\n\t\tTextToText(openai.TextToTextParams{Model: goopenai.GPT4oMini}).\n\t\tSetPrompt(\"You are a helpful assistant that translates English to French\").\n\t\tExecute(\n\t\t\tcontext.Background(),\n\t\t\tagency.NewMessage(\n\t\t\t\tagency.UserRole,\n\t\t\t\tagency.TextKind,\n\t\t\t\t[]byte(\"I love programming.\"),\n\t\t\t),\n\t\t)\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(string(result.Content()))\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/neurocult/agency\n\ngo 1.21.0\n\nrequire (\n\tgithub.com/sashabaranov/go-openai v1.36.1\n\tgithub.com/weaviate/weaviate v1.24.8\n\tgithub.com/weaviate/weaviate-go-client/v4 v4.13.1\n)\n\nrequire (\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n)\n\nrequire (\n\tgithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect\n\tgithub.com/go-openapi/analysis v0.23.0 // indirect\n\tgithub.com/go-openapi/errors v0.22.0 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.21.0 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.0 // indirect\n\tgithub.com/go-openapi/loads v0.22.0 // indirect\n\tgithub.com/go-openapi/spec v0.21.0 // indirect\n\tgithub.com/go-openapi/strfmt v0.23.0 // indirect\n\tgithub.com/go-openapi/swag v0.23.0 // indirect\n\tgithub.com/go-openapi/validate v0.24.0 // indirect\n\tgithub.com/joho/godotenv v1.5.1\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgo.mongodb.org/mongo-driver v1.15.0 // indirect\n\tgolang.org/x/net v0.24.0 // indirect\n\tgolang.org/x/oauth2 v0.19.0 // indirect\n\tgolang.org/x/sys v0.19.0 // indirect\n\tgolang.org/x/text v0.14.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240412170617-26222e5d3d56 // indirect\n\tgoogle.golang.org/grpc v1.63.2 // indirect\n\tgoogle.golang.org/protobuf v1.33.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=\ngithub.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=\ngithub.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=\ngithub.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=\ngithub.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=\ngithub.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=\ngithub.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=\ngithub.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=\ngithub.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=\ngithub.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=\ngithub.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=\ngithub.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=\ngithub.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=\ngithub.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=\ngithub.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=\ngithub.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=\ngithub.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=\ngithub.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=\ngithub.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=\ngithub.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=\ngithub.com/sashabaranov/go-openai v1.36.1 h1:EVfRXwIlW2rUzpx6vR+aeIKCK/xylSrVYAx1TMTSX3g=\ngithub.com/sashabaranov/go-openai v1.36.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/weaviate/weaviate v1.24.8 h1:obeBOJuXScDvUlbTKuqPwJl/cUB5csRhCN6q4smcQiM=\ngithub.com/weaviate/weaviate v1.24.8/go.mod h1:LkAk+xUwF8DKKRb9dEI9DrquwJ/tUzfgd2NN+KEDTYU=\ngithub.com/weaviate/weaviate-go-client/v4 v4.13.1 h1:7PuK/hpy6Q0b9XaVGiUg5OD1MI/eF2ew9CJge9XdBEE=\ngithub.com/weaviate/weaviate-go-client/v4 v4.13.1/go.mod h1:B2m6g77xWDskrCq1GlU6CdilS0RG2+YXEgzwXRADad0=\ngo.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc=\ngo.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=\ngolang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=\ngolang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=\ngolang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=\ngolang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=\ngolang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=\ngolang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240412170617-26222e5d3d56 h1:zviK8GX4VlMstrK3JkexM5UHjH1VOkRebH9y3jhSBGk=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240412170617-26222e5d3d56/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=\ngoogle.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=\ngoogle.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=\ngoogle.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=\ngoogle.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "messages.go",
    "content": "package agency\n\nimport \"encoding/json\"\n\ntype Message interface {\n\tRole() Role\n\tContent() []byte\n\tKind() Kind\n}\n\ntype Kind string\n\nconst (\n\tTextKind      Kind = \"text\"\n\tImageKind     Kind = \"image\"\n\tVoiceKind     Kind = \"voice\"\n\tEmbeddingKind Kind = \"embedding\"\n)\n\ntype Role string\n\nconst (\n\tUserRole      Role = \"user\"\n\tSystemRole    Role = \"system\"\n\tAssistantRole Role = \"assistant\"\n\tToolRole      Role = \"tool\"\n)\n\ntype BaseMessage struct {\n\tcontent []byte\n\trole    Role\n\tkind    Kind\n}\n\nfunc (bm BaseMessage) Role() Role {\n\treturn bm.role\n}\n\nfunc (bm BaseMessage) Kind() Kind {\n\treturn bm.kind\n}\nfunc (bm BaseMessage) Content() []byte {\n\treturn bm.content\n}\n\n// NewMessage creates new `Message` with the specified `Role` and `Kind`\nfunc NewMessage(role Role, kind Kind, content []byte) BaseMessage {\n\treturn BaseMessage{\n\t\tcontent: content,\n\t\trole:    role,\n\t\tkind:    kind,\n\t}\n}\n\n// NewTextMessage creates new `Message` with Text kind and the specified `Role`\nfunc NewTextMessage(role Role, content string) BaseMessage {\n\treturn BaseMessage{\n\t\tcontent: []byte(content),\n\t\trole:    role,\n\t\tkind:    TextKind,\n\t}\n}\n\n// NewJsonMessage marshals content and creates new `Message` with text kind and the specified `Role`\nfunc NewJsonMessage(role Role, content any) (BaseMessage, error) {\n\tdata, err := json.Marshal(content)\n\tif err != nil {\n\t\treturn BaseMessage{}, err\n\t}\n\n\treturn BaseMessage{\n\t\tcontent: data,\n\t\trole:    role,\n\t\tkind:    TextKind,\n\t}, nil\n}\n"
  },
  {
    "path": "process.go",
    "content": "package agency\n\nimport (\n\t\"context\"\n)\n\n// Process is a chain of operations that can be executed in sequence.\ntype Process struct {\n\toperations []*Operation\n}\n\n// NewProcess creates a new Process with given operations.\nfunc NewProcess(operations ...*Operation) *Process {\n\treturn &Process{\n\t\toperations: operations,\n\t}\n}\n\n// Interceptor is a function that is called by Process after one operation finished but before next one is started.\ntype Interceptor func(in Message, out Message, cfg *OperationConfig)\n\n// Execute iterates over Process's operations and sequentially executes them.\n// 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.\n// It also executes all given interceptors, if they are provided, so for every N operations and M interceptors it's N x M executions.\nfunc (p *Process) Execute(ctx context.Context, input Message, interceptors ...Interceptor) (Message, error) {\n\tfor _, operation := range p.operations {\n\t\toutput, err := operation.Execute(ctx, input)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// FIXME while these are called AFTER operation and not before it's impossible to modify configuration\n\t\tfor _, interceptor := range interceptors {\n\t\t\tinterceptor(input, output, operation.Config())\n\t\t}\n\n\t\tinput = output\n\t}\n\n\treturn input, nil\n}\n"
  },
  {
    "path": "providers/openai/helpers.go",
    "content": "package openai\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"math\"\n)\n\ntype Embedding []float32\n\nfunc EmbeddingToBytes(dimensions int, embeddings []Embedding) ([]byte, error) {\n\tif len(embeddings) == 0 {\n\t\treturn nil, fmt.Errorf(\"embeddings is empty\")\n\t}\n\n\tbuf := make([]byte, len(embeddings)*dimensions*4)\n\n\tfor i, embedding := range embeddings {\n\t\tif len(embedding) != dimensions {\n\t\t\treturn nil, fmt.Errorf(\"invalid embedding length: %d, expected %d\", len(embedding), dimensions)\n\t\t}\n\n\t\tfor j, f := range embedding {\n\t\t\tu := math.Float32bits(f)\n\t\t\tbinary.LittleEndian.PutUint32(buf[(i*dimensions+j)*4:], u)\n\t\t}\n\t}\n\n\treturn buf, nil\n}\n\nfunc BytesToEmbedding(dimensions int, buf []byte) ([]Embedding, error) {\n\tif mltp := len(buf) % (dimensions * 4); mltp != 0 {\n\t\treturn nil, fmt.Errorf(\"invalid buffer length: got %d, but expected multiple of %d\", len(buf), dimensions*4)\n\t}\n\n\tembeddings := make([]Embedding, len(buf)/dimensions/4)\n\tfor i := range embeddings {\n\t\tembeddings[i] = make([]float32, dimensions)\n\t\tfor j := 0; j < dimensions; j++ {\n\t\t\tindex := (i*dimensions + j) * 4\n\n\t\t\tif index+4 > len(buf) {\n\t\t\t\treturn nil, fmt.Errorf(\"buffer is too small for expected number of embeddings\")\n\t\t\t}\n\n\t\t\tembeddings[i][j] = math.Float32frombits(binary.LittleEndian.Uint32(buf[index:]))\n\t\t}\n\t}\n\n\treturn embeddings, nil\n}\n\n// NullableFloat32 is a type that exists to distinguish between undefined values and real zeros.\n// It fixes sashabaranov/go-openai issue with zero temp not included in api request due to how json unmarshal work.\ntype NullableFloat32 *float32\n\n// Temperature is just a tiny helper to create nullable float32 value from regular float32\nfunc Temperature(v float32) NullableFloat32 {\n\treturn &v\n}\n\n// nullableToFloat32 replaces nil with zero (in this case value won't be included in api request)\n// and for real zeros it returns math.SmallestNonzeroFloat32 that is as close to zero as possible.\nfunc nullableToFloat32(v NullableFloat32) float32 {\n\tif v == nil {\n\t\treturn 0\n\t}\n\tif *v == 0 {\n\t\treturn math.SmallestNonzeroFloat32\n\t}\n\treturn *v\n}\n"
  },
  {
    "path": "providers/openai/helpers_test.go",
    "content": "package openai\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestEmbeddingToBytes(t *testing.T) {\n\tfloats := []Embedding{{1.1, 2.2, 3.3}, {4.4, 5.5, 6.6}}\n\n\tbytes, err := EmbeddingToBytes(3, floats)\n\tif err != nil {\n\t\tt.Errorf(\"EmbeddingToBytes error %v\", err)\n\t}\n\n\tnewFloats, err := BytesToEmbedding(3, bytes)\n\tif err != nil {\n\t\tt.Errorf(\"EmbeddingToBytes error %v\", err)\n\t}\n\n\tif !reflect.DeepEqual(floats, newFloats) {\n\t\tt.Errorf(\"floats and newFloats are not equal %v %v\", floats, newFloats)\n\t}\n\n\twrongFloats := []Embedding{{4.4, 5.5, 6.6, 7.7}}\n\n\t_, err = EmbeddingToBytes(3, wrongFloats)\n\tif err == nil {\n\t\tt.Errorf(\"EmbeddingToBytes should has error\")\n\t}\n\n\tbytes, err = EmbeddingToBytes(4, wrongFloats)\n\tif err != nil {\n\t\tt.Errorf(\"EmbeddingToBytes error %v\", err)\n\t}\n\n\t_, err = BytesToEmbedding(3, bytes)\n\tif err == nil {\n\t\tt.Errorf(\"BytesToEmbedding should has error\")\n\t}\n}\n"
  },
  {
    "path": "providers/openai/image_to_text.go",
    "content": "package openai\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype ImageToTextParams struct {\n\tModel            string\n\tMaxTokens        int\n\tTemperature      NullableFloat32\n\tTopP             NullableFloat32\n\tFrequencyPenalty NullableFloat32\n\tPresencePenalty  NullableFloat32\n}\n\n// ImageToText is an operation builder that creates operation than can convert image to text.\nfunc (f *Provider) ImageToText(params ImageToTextParams) *agency.Operation {\n\treturn agency.NewOperation(func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) {\n\t\topenaiMsg := openai.ChatCompletionMessage{\n\t\t\tRole:         openai.ChatMessageRoleUser,\n\t\t\tMultiContent: make([]openai.ChatMessagePart, 0, len(cfg.Messages)+2),\n\t\t}\n\n\t\topenaiMsg.MultiContent = append(openaiMsg.MultiContent, openai.ChatMessagePart{\n\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\tText: cfg.Prompt,\n\t\t})\n\n\t\tfor _, cfgMsg := range cfg.Messages {\n\t\t\topenaiMsg.MultiContent = append(\n\t\t\t\topenaiMsg.MultiContent,\n\t\t\t\topenAIBase64ImageMessage(cfgMsg.Content()),\n\t\t\t)\n\t\t}\n\n\t\topenaiMsg.MultiContent = append(\n\t\t\topenaiMsg.MultiContent,\n\t\t\topenAIBase64ImageMessage(msg.Content()),\n\t\t)\n\n\t\tresp, err := f.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{\n\t\t\tMaxTokens:        params.MaxTokens,\n\t\t\tModel:            params.Model,\n\t\t\tMessages:         []openai.ChatCompletionMessage{openaiMsg},\n\t\t\tTemperature:      nullableToFloat32(params.Temperature),\n\t\t\tTopP:             nullableToFloat32(params.TopP),\n\t\t\tFrequencyPenalty: nullableToFloat32(params.FrequencyPenalty),\n\t\t\tPresencePenalty:  nullableToFloat32(params.PresencePenalty),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif len(resp.Choices) < 1 {\n\t\t\treturn nil, errors.New(\"no choice\")\n\t\t}\n\t\tchoice := resp.Choices[0].Message\n\n\t\treturn agency.NewMessage(agency.AssistantRole, agency.TextKind, []byte(choice.Content)), nil\n\t})\n}\n\nfunc openAIBase64ImageMessage(bb []byte) openai.ChatMessagePart {\n\timgBase64Str := base64.StdEncoding.EncodeToString(bb)\n\treturn openai.ChatMessagePart{\n\t\tType: openai.ChatMessagePartTypeImageURL,\n\t\tImageURL: &openai.ChatMessageImageURL{\n\t\t\tURL:    fmt.Sprintf(\"data:image/jpeg;base64,%s\", imgBase64Str),\n\t\t\tDetail: openai.ImageURLDetailAuto,\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "providers/openai/provider.go",
    "content": "package openai\n\nimport (\n\t\"github.com/sashabaranov/go-openai\"\n)\n\n// Provider is a set of operation builders.\ntype Provider struct {\n\tclient *openai.Client\n}\n\n// Params is a set of parameters specific for creating this concrete provider.\n// They are shared across all operation builders.\ntype Params struct {\n\tKey     string // Required if not using local LLM.\n\tBaseURL string // Optional. If not set then default openai base url is used\n}\n\n// New creates a new Provider instance.\nfunc New(params Params) *Provider {\n\tcfg := openai.DefaultConfig(params.Key)\n\tif params.BaseURL != \"\" {\n\t\tcfg.BaseURL = params.BaseURL\n\t}\n\treturn &Provider{\n\t\tclient: openai.NewClientWithConfig(cfg),\n\t}\n}\n"
  },
  {
    "path": "providers/openai/speech_to_text.go",
    "content": "package openai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype SpeechToTextParams struct {\n\tModel       string\n\tTemperature NullableFloat32\n}\n\n// SpeechToText is an operation builder that creates operation than can convert speech to text.\nfunc (f Provider) SpeechToText(params SpeechToTextParams) *agency.Operation {\n\treturn agency.NewOperation(\n\t\tfunc(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) {\n\t\t\tresp, err := f.client.CreateTranscription(ctx, openai.AudioRequest{\n\t\t\t\tModel:       params.Model,\n\t\t\t\tPrompt:      cfg.Prompt,\n\t\t\t\tFilePath:    \"speech.ogg\",\n\t\t\t\tReader:      bytes.NewReader(msg.Content()),\n\t\t\t\tTemperature: nullableToFloat32(params.Temperature),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn agency.NewMessage(agency.AssistantRole, agency.TextKind, []byte(resp.Text)), nil\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "providers/openai/text_to_embedding.go",
    "content": "package openai\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/sashabaranov/go-openai\"\n\n\t\"github.com/neurocult/agency\"\n)\n\ntype EmbeddingModel = openai.EmbeddingModel\n\nconst AdaEmbeddingV2 EmbeddingModel = openai.AdaEmbeddingV2\n\ntype TextToEmbeddingParams struct {\n\tModel      EmbeddingModel\n\tDimensions EmbeddingDimensions\n}\n\ntype EmbeddingDimensions *int\n\nfunc NewDimensions(v int) EmbeddingDimensions {\n\treturn &v\n}\n\nfunc (p Provider) TextToEmbedding(params TextToEmbeddingParams) *agency.Operation {\n\tvar dimensions int\n\tif params.Dimensions != nil {\n\t\tdimensions = *params.Dimensions\n\t}\n\n\treturn agency.NewOperation(func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) {\n\t\t// TODO: we have to convert string to model and then model to string. Can we optimize it?\n\t\tmessages := append(cfg.Messages, msg)\n\t\ttexts := make([]string, len(messages))\n\n\t\tfor i, m := range messages {\n\t\t\ttexts[i] = string(m.Content())\n\t\t}\n\n\t\tresp, err := p.client.CreateEmbeddings(\n\t\t\tctx,\n\t\t\topenai.EmbeddingRequest{\n\t\t\t\tInput:      texts,\n\t\t\t\tModel:      params.Model,\n\t\t\t\tDimensions: dimensions,\n\t\t\t},\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tvectors := make([]Embedding, len(resp.Data))\n\t\tfor i, vector := range resp.Data {\n\t\t\tvectors[i] = vector.Embedding\n\t\t}\n\n\t\tbytes, err := EmbeddingToBytes(1536, vectors)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to convert embedding to bytes: %w\", err)\n\t\t}\n\n\t\t// TODO: we have to convert []float32 to []byte. Can we optimize it?\n\t\treturn agency.NewMessage(agency.AssistantRole, agency.EmbeddingKind, bytes), nil\n\t})\n}\n"
  },
  {
    "path": "providers/openai/text_to_image.go",
    "content": "package openai\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype TextToImageParams struct {\n\tModel     string\n\tImageSize string\n\tQuality   string\n\tStyle     string\n}\n\n// TextToImage is an operation builder that creates operation than can convert text to image.\nfunc (p Provider) TextToImage(params TextToImageParams) *agency.Operation {\n\treturn agency.NewOperation(\n\t\tfunc(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) {\n\t\t\treqBase64 := openai.ImageRequest{\n\t\t\t\tPrompt:         fmt.Sprintf(\"%s\\n\\n%s\", cfg.Prompt, string(msg.Content())),\n\t\t\t\tSize:           params.ImageSize,\n\t\t\t\tResponseFormat: openai.CreateImageResponseFormatB64JSON,\n\t\t\t\tN:              1, // DALL·E-3 only support n=1, for other models support needed\n\t\t\t\tModel:          params.Model,\n\t\t\t\tQuality:        params.Quality,\n\t\t\t\tStyle:          params.Style,\n\t\t\t}\n\n\t\t\trespBase64, err := p.client.CreateImage(ctx, reqBase64)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\timgBytes, err := base64.StdEncoding.DecodeString(respBase64.Data[0].B64JSON)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn agency.NewMessage(agency.AssistantRole, agency.ImageKind, imgBytes), nil\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "providers/openai/text_to_speech.go",
    "content": "package openai\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype TextToSpeechParams struct {\n\tModel          string\n\tResponseFormat string\n\tSpeed          float64\n\tVoice          string\n}\n\n// TextToSpeech is an operation builder that creates operation than can convert text to speech.\nfunc (f Provider) TextToSpeech(params TextToSpeechParams) *agency.Operation {\n\treturn agency.NewOperation(\n\t\tfunc(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) {\n\t\t\tresp, err := f.client.CreateSpeech(ctx, openai.CreateSpeechRequest{\n\t\t\t\tModel:          openai.SpeechModel(params.Model),\n\t\t\t\tInput:          string(msg.Content()),\n\t\t\t\tVoice:          openai.SpeechVoice(params.Voice),\n\t\t\t\tResponseFormat: openai.SpeechResponseFormat(params.ResponseFormat),\n\t\t\t\tSpeed:          params.Speed,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tbb, err := io.ReadAll(resp)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn agency.NewMessage(agency.AssistantRole, agency.VoiceKind, bb), nil\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "providers/openai/text_to_stream.go",
    "content": "package openai\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype TextToStreamParams struct {\n\tTextToTextParams\n\tStreamHandler func(delta, total string, isFirst, isLast bool) error\n}\n\nfunc (p Provider) TextToStream(params TextToStreamParams) *agency.Operation {\n\topenAITools := castFuncDefsToOpenAITools(params.FuncDefs)\n\n\treturn agency.NewOperation(\n\t\tfunc(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) {\n\t\t\topenAIMessages, err := agencyToOpenaiMessages(cfg, msg)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"text to stream: %w\", err)\n\t\t\t}\n\n\t\t\tfor { // streaming loop\n\t\t\t\topenAIResponse, err := p.client.CreateChatCompletionStream(\n\t\t\t\t\tctx,\n\t\t\t\t\topenai.ChatCompletionRequest{\n\t\t\t\t\t\tModel:          params.Model,\n\t\t\t\t\t\tTemperature:    nullableToFloat32(params.Temperature),\n\t\t\t\t\t\tMaxTokens:      params.MaxTokens,\n\t\t\t\t\t\tMessages:       openAIMessages,\n\t\t\t\t\t\tTools:          openAITools,\n\t\t\t\t\t\tStream:         params.StreamHandler != nil,\n\t\t\t\t\t\tToolChoice:     params.ToolCallRequired(),\n\t\t\t\t\t\tSeed:           params.Seed,\n\t\t\t\t\t\tResponseFormat: params.Format,\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"create chat completion stream: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tvar content string\n\t\t\t\tvar accumulatedStreamedFunctions = make([]openai.ToolCall, 0, len(openAITools))\n\t\t\t\tvar isFirstDelta = true\n\t\t\t\tvar isLastDelta = false\n\t\t\t\tvar lastDelta string\n\n\t\t\t\tfor {\n\t\t\t\t\trecv, err := openAIResponse.Recv()\n\t\t\t\t\tisLastDelta = errors.Is(err, io.EOF)\n\n\t\t\t\t\tif len(lastDelta) > 0 || (isLastDelta && len(content) > 0) {\n\t\t\t\t\t\tif err = params.StreamHandler(lastDelta, content, isFirstDelta, isLastDelta); err != nil {\n\t\t\t\t\t\t\treturn nil, fmt.Errorf(\"handing stream: %w\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tisFirstDelta = false\n\t\t\t\t\t}\n\n\t\t\t\t\tif isLastDelta {\n\t\t\t\t\t\tif len(accumulatedStreamedFunctions) == 0 {\n\t\t\t\t\t\t\treturn agency.NewTextMessage(\n\t\t\t\t\t\t\t\tagency.AssistantRole,\n\t\t\t\t\t\t\t\tcontent,\n\t\t\t\t\t\t\t), nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tif len(recv.Choices) < 1 {\n\t\t\t\t\t\treturn nil, errors.New(\"no choice\")\n\t\t\t\t\t}\n\n\t\t\t\t\tfirstChoice := recv.Choices[0]\n\n\t\t\t\t\tif len(firstChoice.Delta.Content) > 0 {\n\t\t\t\t\t\tlastDelta = firstChoice.Delta.Content\n\t\t\t\t\t\tcontent += lastDelta\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlastDelta = \"\"\n\t\t\t\t\t}\n\n\t\t\t\t\tfor index, toolCall := range firstChoice.Delta.ToolCalls {\n\t\t\t\t\t\tif len(accumulatedStreamedFunctions) < index+1 {\n\t\t\t\t\t\t\taccumulatedStreamedFunctions = append(accumulatedStreamedFunctions, openai.ToolCall{\n\t\t\t\t\t\t\t\tIndex: toolCall.Index,\n\t\t\t\t\t\t\t\tID:    toolCall.ID,\n\t\t\t\t\t\t\t\tType:  toolCall.Type,\n\t\t\t\t\t\t\t\tFunction: openai.FunctionCall{\n\t\t\t\t\t\t\t\t\tName:      toolCall.Function.Name,\n\t\t\t\t\t\t\t\t\tArguments: toolCall.Function.Arguments,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t\taccumulatedStreamedFunctions[index].Function.Arguments += toolCall.Function.Arguments\n\t\t\t\t\t}\n\n\t\t\t\t\tif firstChoice.FinishReason != openai.FinishReasonToolCalls {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\t// Saving tool call to history\n\t\t\t\t\topenAIMessages = append(openAIMessages, openai.ChatCompletionMessage{\n\t\t\t\t\t\tRole:      openai.ChatMessageRoleAssistant,\n\t\t\t\t\t\tToolCalls: accumulatedStreamedFunctions,\n\t\t\t\t\t})\n\n\t\t\t\t\tfor _, call := range accumulatedStreamedFunctions {\n\t\t\t\t\t\ttoolResponse, err := callTool(ctx, call, params.FuncDefs)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn nil, fmt.Errorf(\"text to text call tool: %w\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif toolResponse.Role() != agency.ToolRole {\n\t\t\t\t\t\t\treturn toolResponse, nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\topenAIMessages = append(openAIMessages, toolMessageToOpenAI(toolResponse, call.ID))\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\topenAIResponse.Close()\n\t\t\t}\n\t\t},\n\t)\n}\n\nfunc messageToOpenAI(message agency.Message) openai.ChatCompletionMessage {\n\twrappedMessage := openai.ChatCompletionMessage{\n\t\tRole: string(message.Role()),\n\t}\n\n\tswitch message.Kind() {\n\n\tcase agency.ImageKind:\n\t\twrappedMessage.MultiContent = append(\n\t\t\twrappedMessage.MultiContent,\n\t\t\topenAIBase64ImageMessage(message.Content()),\n\t\t)\n\tdefault:\n\t\twrappedMessage.Content = string(message.Content())\n\t}\n\n\treturn wrappedMessage\n}\n\nfunc toolMessageToOpenAI(message agency.Message, toolID string) openai.ChatCompletionMessage {\n\twrappedMessage := messageToOpenAI(message)\n\twrappedMessage.ToolCallID = toolID\n\n\treturn wrappedMessage\n}\n"
  },
  {
    "path": "providers/openai/text_to_text.go",
    "content": "package openai\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/sashabaranov/go-openai\"\n\n\t\"github.com/neurocult/agency\"\n)\n\n// TextToTextParams represents parameters that are specific for this operation.\ntype TextToTextParams struct {\n\tModel               string\n\tTemperature         NullableFloat32\n\tMaxTokens           int\n\tFuncDefs            []FuncDef\n\tSeed                *int\n\tIsToolsCallRequired bool\n\tFormat              *openai.ChatCompletionResponseFormat\n}\n\nfunc (p TextToTextParams) ToolCallRequired() *string {\n\tvar toolChoice *string\n\tif p.IsToolsCallRequired {\n\t\tv := \"required\"\n\t\ttoolChoice = &v\n\t}\n\n\treturn toolChoice\n}\n\n// TextToText is an operation builder that creates operation than can convert text to text.\n// It can also call provided functions if needed, as many times as needed until the final answer is generated.\nfunc (p Provider) TextToText(params TextToTextParams) *agency.Operation {\n\topenAITools := castFuncDefsToOpenAITools(params.FuncDefs)\n\n\treturn agency.NewOperation(\n\t\tfunc(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) {\n\t\t\topenAIMessages, err := agencyToOpenaiMessages(cfg, msg)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"text to stream: %w\", err)\n\t\t\t}\n\n\t\t\tfor {\n\t\t\t\topenAIResponse, err := p.client.CreateChatCompletion(\n\t\t\t\t\tctx,\n\t\t\t\t\topenai.ChatCompletionRequest{\n\t\t\t\t\t\tModel:          params.Model,\n\t\t\t\t\t\tTemperature:    nullableToFloat32(params.Temperature),\n\t\t\t\t\t\tMaxTokens:      params.MaxTokens,\n\t\t\t\t\t\tMessages:       openAIMessages,\n\t\t\t\t\t\tTools:          openAITools,\n\t\t\t\t\t\tSeed:           params.Seed,\n\t\t\t\t\t\tToolChoice:     params.ToolCallRequired(),\n\t\t\t\t\t\tResponseFormat: params.Format,\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tif len(openAIResponse.Choices) == 0 {\n\t\t\t\t\treturn nil, errors.New(\"get text to text response: no choice\")\n\t\t\t\t}\n\n\t\t\t\tresponseMessage := openAIResponse.Choices[0].Message\n\n\t\t\t\tif len(responseMessage.ToolCalls) == 0 {\n\t\t\t\t\treturn OpenaiToAgencyMessage(responseMessage), nil\n\t\t\t\t}\n\n\t\t\t\topenAIMessages = append(openAIMessages, responseMessage)\n\t\t\t\tfor _, call := range responseMessage.ToolCalls {\n\t\t\t\t\ttoolResponse, err := callTool(ctx, call, params.FuncDefs)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"text to text call tool: %w\", err)\n\t\t\t\t\t}\n\n\t\t\t\t\tif toolResponse.Role() != agency.ToolRole {\n\t\t\t\t\t\treturn toolResponse, nil\n\t\t\t\t\t}\n\n\t\t\t\t\topenAIMessages = append(openAIMessages, toolMessageToOpenAI(toolResponse, call.ID))\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t)\n}\n\n// === Helpers ===\n\nfunc castFuncDefsToOpenAITools(funcDefs []FuncDef) []openai.Tool {\n\ttools := make([]openai.Tool, 0, len(funcDefs))\n\tfor _, f := range funcDefs {\n\t\ttool := openai.Tool{\n\t\t\tType: openai.ToolTypeFunction,\n\t\t\tFunction: &openai.FunctionDefinition{\n\t\t\t\tName:        f.Name,\n\t\t\t\tDescription: f.Description,\n\t\t\t},\n\t\t}\n\t\tif f.Parameters != nil {\n\t\t\ttool.Function.Parameters = f.Parameters\n\t\t}\n\t\ttools = append(tools, tool)\n\t}\n\treturn tools\n}\n\nfunc agencyToOpenaiMessages(cfg *agency.OperationConfig, msg agency.Message) ([]openai.ChatCompletionMessage, error) {\n\topenAIMessages := make([]openai.ChatCompletionMessage, 0, len(cfg.Messages)+2)\n\n\topenAIMessages = append(openAIMessages, openai.ChatCompletionMessage{\n\t\tRole:    openai.ChatMessageRoleSystem,\n\t\tContent: cfg.Prompt,\n\t})\n\n\tfor _, cfgMsg := range cfg.Messages {\n\t\topenAIMessages = append(openAIMessages, messageToOpenAI(cfgMsg))\n\t}\n\n\topenaiMsg := openai.ChatCompletionMessage{\n\t\tRole: openai.ChatMessageRoleUser,\n\t}\n\n\tswitch msg.Kind() {\n\tcase agency.TextKind:\n\t\topenaiMsg.Content = string(msg.Content())\n\tcase agency.ImageKind:\n\t\topenaiMsg.MultiContent = append(\n\t\t\topenaiMsg.MultiContent,\n\t\t\topenAIBase64ImageMessage(msg.Content()),\n\t\t)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"operator doesn't support %s kind\", msg.Kind())\n\t}\n\n\topenAIMessages = append(openAIMessages, openaiMsg)\n\n\treturn openAIMessages, nil\n}\n\nfunc callTool(\n\tctx context.Context,\n\tcall openai.ToolCall,\n\tdefs FuncDefs,\n) (agency.Message, error) {\n\tfuncToCall := defs.getFuncDefByName(call.Function.Name)\n\tif funcToCall == nil {\n\t\treturn nil, errors.New(\"function not found\")\n\t}\n\n\tfuncResult, err := funcToCall.Body(ctx, []byte(call.Function.Arguments))\n\tif err != nil {\n\t\treturn funcResult, fmt.Errorf(\"call function %s: %w\", funcToCall.Name, err)\n\t}\n\n\treturn funcResult, nil\n}\n\nfunc OpenaiToAgencyMessage(msg openai.ChatCompletionMessage) agency.Message {\n\treturn agency.NewTextMessage(\n\t\tagency.Role(msg.Role),\n\t\tmsg.Content,\n\t)\n}\n"
  },
  {
    "path": "providers/openai/tools.go",
    "content": "package openai\n\nimport (\n\t\"context\"\n\n\t\"github.com/neurocult/agency\"\n\t\"github.com/sashabaranov/go-openai/jsonschema\"\n)\n\ntype ToolResultMessage struct {\n\tagency.Message\n\n\tToolID   string\n\tToolName string\n}\n\n// FuncDef represents a function definition that can be called during the conversation.\ntype FuncDef struct {\n\tName        string\n\tDescription string\n\t// Parameters is an optional structure that defines the schema of the parameters that the function accepts.\n\tParameters *jsonschema.Definition\n\t// Body is the actual function that get's called.\n\t// Parameters passed are bytes that can be unmarshalled to type that implements provided json schema.\n\t// Returned result must be anything that can be marshalled, including primitive values.\n\tBody func(ctx context.Context, params []byte) (agency.Message, error)\n}\n\ntype FuncDefs []FuncDef\n\nfunc (ds FuncDefs) getFuncDefByName(name string) *FuncDef {\n\tfor _, f := range ds {\n\t\tif f.Name == name {\n\t\t\treturn &f\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  }
]