diff --git a/README.md b/README.md index 00f3f56..579b8f4 100644 --- a/README.md +++ b/README.md @@ -9,19 +9,14 @@ This repository contains a generic HTTP client which can be adapted to provide: * Debugging capabilities to see the request and response data * Streaming text and JSON events -API Documentation: https://pkg.go.dev/github.com/mutablelogic/go-client +API Documentation: There are also some example clients which use this library: -* [Anthropic API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/anthropic) for Claude LLM * [Bitwarden API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/bitwarden) -* [Elevenlabs API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/elevenlabs) for Text-to-Speech * [Home Assistant API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/homeassistant) * [IPify Client](https://github.com/mutablelogic/go-client/tree/main/pkg/ipify) -* [Mistral API Client](https://github.com/mutablelogic/go-client/tree/main/pkg/mistral) for Mistral LLM * [NewsAPI client](https://github.com/mutablelogic/go-client/tree/main/pkg/newsapi) -* [Ollama API client](https://github.com/mutablelogic/go-client/tree/main/pkg/ollama) for locally-hosted LLM's -* [OpenAI API client](https://github.com/mutablelogic/go-client/tree/main/pkg/openai) for OpenAI LLM's * [WeatherAPI client](https://github.com/mutablelogic/go-client/tree/main/pkg/weatherapi) Aiming to have compatibility with go version 1.21 and above. @@ -81,7 +76,7 @@ You can create a payload using the following methods: a JSON payload which defaults to POST. * `client.NewMultipartRequest(payload any, accept string)` returns a new request with a Multipart Form data payload which defaults to POST. -* `client.NewFormRequest(payload any, accept string)` returns a new request with a +* `client.NewFormRequest(payload any, accept string)` returns a new request with a Form data payload which defaults to POST. For example, @@ -147,7 +142,7 @@ type Client interface { ``` If you pass a context to the `DoWithContext` method, then the request can be -cancelled using the context in addition to the timeout. Various options can be passed to +cancelled using the context in addition to the timeout. Various options can be passed to modify each individual request when using the `Do` method: * `OptReqEndpoint(value string)` sets the endpoint for the request @@ -273,7 +268,7 @@ func Callback(event client.TextStreamEvent) error { } ``` -The `TextStreamEvent` object has the following +The `TextStreamEvent` object has the following If you return an error of type `io.EOF` from the callback, then the stream will be closed. Similarly, if you return any other error the stream will be closed and the error returned. diff --git a/cmd/agent/chat.go b/cmd/agent/chat.go deleted file mode 100644 index e9e2d87..0000000 --- a/cmd/agent/chat.go +++ /dev/null @@ -1,169 +0,0 @@ -package main - -import ( - "context" - "fmt" - - // Packages - markdown "github.com/MichaelMure/go-term-markdown" - agent "github.com/mutablelogic/go-client/pkg/agent" -) - -///////////////////////////////////////////////////////////////////// -// TYPES - -type ChatCmd struct { - Prompt string `arg:"" optional:"" help:"The prompt to generate a response for"` - Agent string `flag:"agent" help:"The agent to use"` - Model string `flag:"model" help:"The model to use"` - Stream bool `flag:"stream" help:"Stream the response"` -} - -///////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -func (cmd *ChatCmd) Run(globals *Globals) error { - // Get the agent and the model - model_agent, model := globals.getModel(globals.ctx, cmd.Agent, cmd.Model) - if model_agent == nil || model == nil { - return fmt.Errorf("model %q not found, or not set on command line", globals.state.Model) - } - - // Generate the options - opts := make([]agent.Opt, 0) - if cmd.Stream { - opts = append(opts, agent.OptStream(func(r agent.Response) { - fmt.Println(r) - })) - } - - // Add tools - if tools := globals.getTools(); len(tools) > 0 { - opts = append(opts, agent.OptTools(tools...)) - } - - // If the prompt is empty, then we're in interative mode - context := []agent.Context{} - if cmd.Prompt == "" { - if globals.term == nil { - return fmt.Errorf("prompt is empty and not in interactive mode") - } - } else { - context = append(context, model_agent.UserPrompt(cmd.Prompt)) - } - -FOR_LOOP: - for { - // When there is no context, create some - if len(context) == 0 { - if prompt, err := globals.term.ReadLine(model.Name() + "> "); err != nil { - return err - } else if prompt == "" { - break FOR_LOOP - } else { - context = append(context, model_agent.UserPrompt(prompt)) - } - } - - // Generate a chat completion - response, err := model_agent.Generate(globals.ctx, model, context, opts...) - if err != nil { - return err - } - - // If the response is a tool call, then run the tool - if response.ToolCall != nil { - result, err := globals.runTool(globals.ctx, response.ToolCall) - if err != nil { - return err - } - response.Context = append(response.Context, result) - } else { - if globals.term != nil { - w, _ := globals.term.Size() - fmt.Println(string(markdown.Render(response.Text, w, 0))) - } else { - fmt.Println(response.Text) - } - - // Make empty context - response.Context = []agent.Context{} - } - - // Context comes from the response - context = response.Context - } - - // Return success - return nil -} - -///////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -// Get the model, either from state or from the command-line flags. -// If the model is not found, or there is another error, return nil -func (globals *Globals) getModel(ctx context.Context, agent, model string) (agent.Agent, agent.Model) { - state := globals.state - if agent != "" { - state.Agent = agent - } - if model != "" { - state.Model = model - } - - // Cycle through the agents and models to find the one we want - for _, agent := range globals.agents { - // Filter by agent - if state.Agent != "" && agent.Name() != state.Agent { - continue - } - - // Retrieve the models for this agent - models, err := agent.Models(ctx) - if err != nil { - continue - } - - // Filter by model - for _, model := range models { - if state.Model != "" && model.Name() != state.Model { - continue - } - - // This is the model we're using.... - state.Agent = agent.Name() - state.Model = model.Name() - return agent, model - } - } - - // No model found - return nil, nil -} - -// Get the tools -func (globals *Globals) getTools() []agent.Tool { - return globals.tools -} - -// Return a tool by name. If the tool is not found, return nil -func (globals *Globals) getTool(name string) agent.Tool { - for _, tool := range globals.tools { - if tool.Name() == name { - return tool - } - } - return nil -} - -// Run a tool from a tool call, and return the result -func (globals *Globals) runTool(ctx context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - tool := globals.getTool(call.Name) - if tool == nil { - return nil, fmt.Errorf("tool %q not found", call.Name) - } - - // Run the tool - return tool.Run(ctx, call) -} diff --git a/cmd/agent/list_agents.go b/cmd/agent/list_agents.go deleted file mode 100644 index c07da17..0000000 --- a/cmd/agent/list_agents.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" -) - -///////////////////////////////////////////////////////////////////// -// TYPES - -type ListAgentsCmd struct { -} - -///////////////////////////////////////////////////////////////////// -// METHODS - -func (cmd *ListAgentsCmd) Run(ctx *Globals) error { - result := make([]string, 0) - for _, agent := range ctx.agents { - result = append(result, agent.Name()) - } - - data, err := json.MarshalIndent(result, "", " ") - if err != nil { - return err - } - - fmt.Println(string(data)) - - return nil -} diff --git a/cmd/agent/list_models.go b/cmd/agent/list_models.go deleted file mode 100644 index bdceef0..0000000 --- a/cmd/agent/list_models.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" -) - -///////////////////////////////////////////////////////////////////// -// TYPES - -type ListModelsCmd struct { -} - -type modeljson struct { - Agent string `json:"agent"` - Model string `json:"model"` -} - -///////////////////////////////////////////////////////////////////// -// METHODS - -func (cmd *ListModelsCmd) Run(ctx *Globals) error { - result := make([]modeljson, 0) - for _, agent := range ctx.agents { - models, err := agent.Models(ctx.ctx) - if err != nil { - return err - } - for _, model := range models { - result = append(result, modeljson{Agent: agent.Name(), Model: model.Name()}) - } - } - - data, err := json.MarshalIndent(result, "", " ") - if err != nil { - return err - } - - fmt.Println(string(data)) - - return nil -} diff --git a/cmd/agent/list_tools.go b/cmd/agent/list_tools.go deleted file mode 100644 index 16cc62a..0000000 --- a/cmd/agent/list_tools.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" -) - -///////////////////////////////////////////////////////////////////// -// TYPES - -type ListToolsCmd struct { -} - -type tooljson struct { - Provider string `json:"provider"` - Name string `json:"name"` - Description string `json:"description"` -} - -///////////////////////////////////////////////////////////////////// -// METHODS - -func (cmd *ListToolsCmd) Run(ctx *Globals) error { - result := make([]tooljson, 0) - for _, tool := range ctx.tools { - result = append(result, tooljson{Provider: tool.Provider(), Name: tool.Name(), Description: tool.Description()}) - } - - data, err := json.MarshalIndent(result, "", " ") - if err != nil { - return err - } - - fmt.Println(string(data)) - - return nil -} diff --git a/cmd/agent/main.go b/cmd/agent/main.go deleted file mode 100644 index bac6cbd..0000000 --- a/cmd/agent/main.go +++ /dev/null @@ -1,168 +0,0 @@ -package main - -import ( - "context" - "os" - "os/signal" - "path/filepath" - "syscall" - - // Packages - kong "github.com/alecthomas/kong" - client "github.com/mutablelogic/go-client" - agent "github.com/mutablelogic/go-client/pkg/agent" - "github.com/mutablelogic/go-client/pkg/homeassistant" - "github.com/mutablelogic/go-client/pkg/ipify" - "github.com/mutablelogic/go-client/pkg/newsapi" - ollama "github.com/mutablelogic/go-client/pkg/ollama" - openai "github.com/mutablelogic/go-client/pkg/openai" - "github.com/mutablelogic/go-client/pkg/weatherapi" -) - -//////////////////////////////////////////////////////////////////////////////// -// TYPES - -type Globals struct { - OllamaUrl string `name:"ollama-url" help:"URL of Ollama service (can be set from OLLAMA_URL env)" default:"${OLLAMA_URL}"` - OpenAIKey string `name:"openai-key" help:"API key for OpenAI service (can be set from OPENAI_API_KEY env)" default:"${OPENAI_API_KEY}"` - WeatherKey string `name:"weather-key" help:"API key for WeatherAPI service (can be set from WEATHERAPI_KEY env)" default:"${WEATHERAPI_KEY}"` - NewsKey string `name:"news-key" help:"API key for NewsAPI service (can be set from NEWSAPI_KEY env)" default:"${NEWSAPI_KEY}"` - HomeAssistantUrl string `name:"homeassistant-url" help:"URL of HomeAssistant service (can be set from HA_ENDPOINT env)" default:"${HA_ENDPOINT}"` - HomeAssistantKey string `name:"homeassistant-key" help:"API key for HomeAssistant service (can be set from HA_TOKEN env)" default:"${HA_TOKEN}"` - - // Debugging - Debug bool `name:"debug" help:"Enable debug output"` - Verbose bool `name:"verbose" help:"Enable verbose output"` - - ctx context.Context - agents []agent.Agent - tools []agent.Tool - state *State - - // Terminal interaction - term *Term -} - -type CLI struct { - Globals - - // Agents, Models and Tools - Agents ListAgentsCmd `cmd:"" help:"Return a list of agents"` - Models ListModelsCmd `cmd:"" help:"Return a list of models"` - Tools ListToolsCmd `cmd:"" help:"Return a list of tools"` - - // Generate Responses - Chat ChatCmd `cmd:"" help:"Generate a response from a chat message"` -} - -//////////////////////////////////////////////////////////////////////////////// -// MAIN - -func main() { - // The name of the executable - name, err := os.Executable() - if err != nil { - panic(err) - } else { - name = filepath.Base(name) - } - - // Create a cli parser - cli := CLI{} - cmd := kong.Parse(&cli, - kong.Name(name), - kong.Description("Agent command line interface"), - kong.UsageOnError(), - kong.ConfigureHelp(kong.HelpOptions{Compact: true}), - kong.Vars{ - "OLLAMA_URL": envOrDefault("OLLAMA_URL", ""), - "OPENAI_API_KEY": envOrDefault("OPENAI_API_KEY", ""), - "WEATHERAPI_KEY": envOrDefault("WEATHERAPI_KEY", ""), - "NEWSAPI_KEY": envOrDefault("NEWSAPI_KEY", ""), - "HA_TOKEN": envOrDefault("HA_TOKEN", ""), - "HA_ENDPOINT": envOrDefault("HA_ENDPOINT", ""), - }, - ) - - if cli.OllamaUrl != "" { - ollama, err := ollama.New(cli.OllamaUrl, clientOpts(&cli)...) - cmd.FatalIfErrorf(err) - cli.Globals.agents = append(cli.Globals.agents, ollama) - } - if cli.OpenAIKey != "" { - openai, err := openai.New(cli.OpenAIKey, clientOpts(&cli)...) - cmd.FatalIfErrorf(err) - cli.Globals.agents = append(cli.Globals.agents, openai) - } - if cli.WeatherKey != "" { - weather, err := weatherapi.New(cli.WeatherKey, clientOpts(&cli)...) - cmd.FatalIfErrorf(err) - cli.Globals.tools = append(cli.Globals.tools, weather.Tools()...) - } - if cli.NewsKey != "" { - news, err := newsapi.New(cli.NewsKey, clientOpts(&cli)...) - cmd.FatalIfErrorf(err) - cli.Globals.tools = append(cli.Globals.tools, news.Tools()...) - } - if cli.HomeAssistantKey != "" && cli.HomeAssistantUrl != "" { - ha, err := homeassistant.New(cli.HomeAssistantUrl, cli.HomeAssistantKey, clientOpts(&cli)...) - cmd.FatalIfErrorf(err) - cli.Globals.tools = append(cli.Globals.tools, ha.Tools()...) - } - - // Add ipify - ipify, err := ipify.New(clientOpts(&cli)...) - cmd.FatalIfErrorf(err) - cli.Globals.tools = append(cli.Globals.tools, ipify.Tools()...) - - // Create a context - ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer cancel() - cli.Globals.ctx = ctx - - // Create a state - if state, err := NewState(name); err != nil { - cmd.FatalIfErrorf(err) - return - } else { - cli.Globals.state = state - } - - // Terminal from stdin - if term, err := NewTerm(os.Stdin); err != nil { - cmd.FatalIfErrorf(err) - } else { - cli.Globals.term = term - } - - // Run the command - if err := cmd.Run(&cli.Globals); err != nil { - cmd.FatalIfErrorf(err) - return - } - - // Save state - if err := cli.Globals.state.Close(); err != nil { - cmd.FatalIfErrorf(err) - return - } -} - -//////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func envOrDefault(name, def string) string { - if value := os.Getenv(name); value != "" { - return value - } else { - return def - } -} - -func clientOpts(cli *CLI) []client.ClientOpt { - result := []client.ClientOpt{} - if cli.Debug { - result = append(result, client.OptTrace(os.Stderr, cli.Verbose)) - } - return result -} diff --git a/cmd/agent/state.go b/cmd/agent/state.go deleted file mode 100644 index 1553404..0000000 --- a/cmd/agent/state.go +++ /dev/null @@ -1,97 +0,0 @@ -package main - -import ( - "encoding/json" - "os" - "path/filepath" -) - -////////////////////////////////////////////////////////////////// -// TYPES - -type State struct { - Agent string `json:"agent"` - Model string `json:"model"` - - // Path of the state file - path string -} - -////////////////////////////////////////////////////////////////// -// GLOBALS - -const ( - // The name of the state file - stateFile = "state.json" -) - -////////////////////////////////////////////////////////////////// -// LIFECYCLE - -// Create a new state object with the given name -func NewState(name string) (*State, error) { - // Load the state from the file, or return a new empty state - path, err := os.UserConfigDir() - if err != nil { - return nil, err - } - - // Append the name of the application to the path - if name != "" { - path = filepath.Join(path, name) - } - - // Create the directory if it doesn't exist - if err := os.MkdirAll(path, 0700); err != nil { - return nil, err - } - - // The state to return - var state State - state.path = filepath.Join(path, stateFile) - - // Load the state from the file, ignore any errors - _ = state.Load() - - // Return success - return &state, nil -} - -// Release resources -func (s *State) Close() error { - return s.Save() -} - -////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -// Load state as JSON -func (s *State) Load() error { - // Open the file - file, err := os.Open(s.path) - if err != nil { - return nil - } - defer file.Close() - - // Decode the JSON - if err := json.NewDecoder(file).Decode(s); err != nil { - return err - } - - // Return success - return nil -} - -// Save state as JSON -func (s *State) Save() error { - // Open the file - file, err := os.Create(s.path) - if err != nil { - return err - } - defer file.Close() - - // Encode the JSON - return json.NewEncoder(file).Encode(s) -} diff --git a/cmd/agent/term.go b/cmd/agent/term.go deleted file mode 100644 index 6c3827c..0000000 --- a/cmd/agent/term.go +++ /dev/null @@ -1,65 +0,0 @@ -package main - -import ( - "io" - "os" - - "golang.org/x/term" -) - -type Term struct { - r io.Reader - fd int - *term.Terminal -} - -func NewTerm(r io.Reader) (*Term, error) { - t := new(Term) - t.r = r - - // Set file descriptor - if osf, ok := r.(*os.File); ok { - t.fd = int(osf.Fd()) - if term.IsTerminal(t.fd) { - t.Terminal = term.NewTerminal(osf, "") - } - } - - // Return success - return t, nil -} - -// Returns the width and height of the terminal, or (0,0) -func (t *Term) Size() (int, int) { - if t.Terminal != nil { - if w, h, err := term.GetSize(t.fd); err == nil { - return w, h - } - } - // Unable to get the size - return 0, 0 -} - -func (t *Term) ReadLine(prompt string) (string, error) { - // Set terminal raw mode - if t.Terminal != nil { - state, err := term.MakeRaw(t.fd) - if err != nil { - return "", err - } - defer term.Restore(t.fd, state) - } - - // Set the prompt - if t.Terminal != nil { - t.Terminal.SetPrompt(prompt) - } - - // Read the line - if t.Terminal != nil { - return t.Terminal.ReadLine() - } else { - // Don't support non-terminal input yet - return "", io.EOF - } -} diff --git a/cmd/api/anthropic.go b/cmd/api/anthropic.go deleted file mode 100644 index dcf7faa..0000000 --- a/cmd/api/anthropic.go +++ /dev/null @@ -1,125 +0,0 @@ -package main - -import ( - "context" - "fmt" - - // Packages - "github.com/djthorpe/go-tablewriter" - "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-client/pkg/anthropic" - "github.com/mutablelogic/go-client/pkg/openai/schema" -) - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -var ( - anthropicName = "claude" - anthropicClient *anthropic.Client - anthropicModel string - anthropicTemperature *float64 - anthropicMaxTokens *uint64 - anthropicStream bool -) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -func anthropicRegister(flags *Flags) { - // Register flags required - flags.String(anthropicName, "anthropic-api-key", "${ANTHROPIC_API_KEY}", "API Key") - - flags.Register(Cmd{ - Name: anthropicName, - Description: "Interact with Claude, from https://www.anthropic.com/api", - Parse: anthropicParse, - Fn: []Fn{ - {Name: "chat", Call: anthropicChat, Description: "Chat with Claude", MinArgs: 1}, - }, - }) -} - -func anthropicParse(flags *Flags, opts ...client.ClientOpt) error { - apiKey := flags.GetString("anthropic-api-key") - if apiKey == "" { - return fmt.Errorf("missing -anthropic-api-key flag") - } else if client, err := anthropic.New(apiKey, opts...); err != nil { - return err - } else { - anthropicClient = client - } - - // Get the command-line parameters - anthropicModel = flags.GetString("model") - if temp, err := flags.GetValue("temperature"); err == nil { - t := temp.(float64) - anthropicTemperature = &t - } - if maxtokens, err := flags.GetValue("max-tokens"); err == nil { - t := maxtokens.(uint64) - anthropicMaxTokens = &t - } - if stream, err := flags.GetValue("stream"); err == nil { - t := stream.(bool) - anthropicStream = t - } - - // Return success - return nil -} - -/////////////////////////////////////////////////////////////////////////////// -// METHODS - -func anthropicChat(ctx context.Context, w *tablewriter.Writer, args []string) error { - - // Set options - opts := []anthropic.Opt{} - if anthropicModel != "" { - opts = append(opts, anthropic.OptModel(anthropicModel)) - } - if anthropicTemperature != nil { - opts = append(opts, anthropic.OptTemperature(float32(*anthropicTemperature))) - } - if anthropicMaxTokens != nil { - opts = append(opts, anthropic.OptMaxTokens(int(*anthropicMaxTokens))) - } - if anthropicStream { - opts = append(opts, anthropic.OptStream(func(choice schema.MessageChoice) { - w := w.Output() - if choice.Delta != nil { - if choice.Delta.Role != "" { - fmt.Fprintf(w, "\n%v: ", choice.Delta.Role) - } - if choice.Delta.Content != "" { - fmt.Fprintf(w, "%v", choice.Delta.Content) - } - } - if choice.FinishReason != "" { - fmt.Printf("\nfinish_reason: %q\n", choice.FinishReason) - } - })) - } - - // Append user message - message := schema.NewMessage("user") - for _, arg := range args { - message.Add(schema.Text(arg)) - } - - // Request -> Response - responses, err := anthropicClient.Messages(ctx, []*schema.Message{ - message, - }, opts...) - if err != nil { - return err - } - - // Write table (if not streaming) - if !anthropicStream { - return w.Write(responses) - } else { - return nil - } -} diff --git a/cmd/api/auth.go b/cmd/api/auth.go deleted file mode 100644 index 6f3e41d..0000000 --- a/cmd/api/auth.go +++ /dev/null @@ -1,90 +0,0 @@ -package main - -import ( - "context" - "time" - - // Packages - tablewriter "github.com/djthorpe/go-tablewriter" - client "github.com/mutablelogic/go-client" - auth "github.com/mutablelogic/go-server/pkg/handler/auth/client" -) - -var ( - authClient *auth.Client - authName = "tokenauth" - authDuration time.Duration -) - -func authRegister(flags *Flags) { - // Register flags - flags.String(authName, "tokenauth-endpoint", "${TOKENAUTH_ENDPOINT}", "tokenauth endpoint (ie, http://host/api/auth/)") - flags.String(authName, "tokenauth-token", "${TOKENAUTH_TOKEN}", "tokenauth token") - flags.Duration(authName, "expiry", 0, "token expiry duration") - - // Register commands - flags.Register(Cmd{ - Name: authName, - Description: "Manage token authentication", - Parse: authParse, - Fn: []Fn{ - // Default caller - {Call: authList, Description: "List authentication tokens"}, - {Name: "list", Call: authList, Description: "List authentication tokens"}, - {Name: "create", Call: authCreate, Description: "Create a token", MinArgs: 1}, - {Name: "delete", Call: authDelete, Description: "Delete a token", MinArgs: 1, MaxArgs: 1}, - }, - }) -} - -func authParse(flags *Flags, opts ...client.ClientOpt) error { - endpoint := flags.GetString("tokenauth-endpoint") - if token := flags.GetString("tokenauth-token"); token != "" { - opts = append(opts, client.OptReqToken(client.Token{ - Scheme: "Bearer", - Value: token, - })) - } - - if duration := flags.GetString("expiry"); duration != "" { - if d, err := time.ParseDuration(duration); err != nil { - return err - } else { - authDuration = d - } - } - - if client, err := auth.New(endpoint, opts...); err != nil { - return err - } else { - authClient = client - } - return nil -} - -func authList(_ context.Context, w *tablewriter.Writer, _ []string) error { - tokens, err := authClient.List() - if err != nil { - return err - } - return w.Write(tokens) -} - -func authCreate(_ context.Context, w *tablewriter.Writer, params []string) error { - name := params[0] - scopes := params[1:] - token, err := authClient.Create(name, authDuration, scopes...) - if err != nil { - return err - } - return w.Write(token) -} - -func authDelete(ctx context.Context, w *tablewriter.Writer, params []string) error { - name := params[0] - err := authClient.Delete(name) - if err != nil { - return err - } - return authList(ctx, w, nil) -} diff --git a/cmd/api/elevenlabs.go b/cmd/api/elevenlabs.go deleted file mode 100644 index 917443a..0000000 --- a/cmd/api/elevenlabs.go +++ /dev/null @@ -1,346 +0,0 @@ -package main - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "regexp" - "strings" - - // Packages - tablewriter "github.com/djthorpe/go-tablewriter" - audio "github.com/go-audio/audio" - wav "github.com/go-audio/wav" - client "github.com/mutablelogic/go-client" - elevenlabs "github.com/mutablelogic/go-client/pkg/elevenlabs" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -var ( - elName = "elevenlabs" - elClient *elevenlabs.Client - elExt = "mp3" - elBitrate = uint64(32) // in kbps - elSamplerate = uint64(44100) // in Hz - elSimilarityBoost = float64(0.0) - elStability = float64(0.0) - elUseSpeakerBoost = false - elWriteSettings = false - reVoiceId = regexp.MustCompile("^[a-z0-9-]{20}$") -) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -func elRegister(flags *Flags) { - // Register flags required - flags.String(elName, "elevenlabs-api-key", "${ELEVENLABS_API_KEY}", "API Key") - flags.Float(elName, "similarity-boost", 0, "Similarity boost") - flags.Float(elName, "stability", 0, "Voice stability") - flags.Bool(elName, "use-speaker-boost", false, "Use speaker boost") - flags.Unsigned(elName, "bitrate", 0, "Bit rate (kbps)") - flags.Unsigned(elName, "samplerate", 0, "Sample rate (kHz)") - - // Register command set - flags.Register(Cmd{ - Name: elName, - Description: "Elevenlabs API", - Parse: elParse, - Fn: []Fn{ - {Name: "models", Call: elModels, Description: "Gets a list of available models"}, - {Name: "voices", Call: elVoices, Description: "Return registered voices"}, - {Name: "voice", Call: elVoice, Description: "Return one voice", MinArgs: 1, MaxArgs: 1, Syntax: ""}, - {Name: "settings", Call: elVoiceSettings, Description: "Return voice settings, or default settings. Set voice settings from -stability, -similarity-boost and -use-speaker-boost flags", MaxArgs: 1, Syntax: "()"}, - {Name: "say", Call: elTextToSpeech, Description: "Text to speech", MinArgs: 2, Syntax: " ..."}, - }, - }) -} - -func elParse(flags *Flags, opts ...client.ClientOpt) error { - // Set defaults - if typ := flags.GetOutExt(); typ != "" { - elExt = strings.ToLower(flags.GetOutExt()) - } - - // Create the client - apiKey := flags.GetString("elevenlabs-api-key") - if apiKey == "" { - return fmt.Errorf("missing -elevenlabs-api-key flag") - } else if client, err := elevenlabs.New(apiKey, opts...); err != nil { - return err - } else { - elClient = client - } - - // Get the bit rate and sample rate - if bitrate, err := flags.GetValue("bitrate"); err == nil { - if bitrate_, ok := bitrate.(uint64); ok && bitrate_ > 0 { - elBitrate = bitrate_ - } - } - if samplerate, err := flags.GetValue("samplerate"); err == nil { - if samplerate_, ok := samplerate.(uint64); ok && samplerate_ > 0 { - elSamplerate = samplerate_ - } - } - - // Similarity boost - if value, err := flags.GetValue("similarity-boost"); err == nil { - elSimilarityBoost = value.(float64) - elWriteSettings = true - } else if !errors.Is(err, ErrNotFound) { - return err - } - - // Stability - if value, err := flags.GetValue("stability"); err == nil { - elStability = value.(float64) - elWriteSettings = true - } else if !errors.Is(err, ErrNotFound) { - return err - } - - // Use speaker boost - if value, err := flags.GetValue("use-speaker-boost"); err == nil { - elUseSpeakerBoost = value.(bool) - elWriteSettings = true - } else if !errors.Is(err, ErrNotFound) { - return err - } - - // Return success - return nil -} - -///////////////////////////////////////////////////////////////////// -// API CALL FUNCTIONS - -func elModels(ctx context.Context, w *tablewriter.Writer, args []string) error { - models, err := elClient.Models() - if err != nil { - return err - } - return w.Write(models) -} - -func elVoices(ctx context.Context, w *tablewriter.Writer, args []string) error { - voices, err := elClient.Voices() - if err != nil { - return err - } - return w.Write(voices) -} - -func elVoice(ctx context.Context, w *tablewriter.Writer, args []string) error { - if voice, err := elVoiceId(args[0]); err != nil { - return err - } else if voice, err := elClient.Voice(voice); err != nil { - return err - } else { - return w.Write(voice) - } -} - -func elVoiceSettings(ctx context.Context, w *tablewriter.Writer, args []string) error { - var voice string - if len(args) > 0 { - if v, err := elVoiceId(args[0]); err != nil { - return err - } else { - voice = v - } - } - - // Get voice settings - settings, err := elClient.VoiceSettings(voice) - if err != nil { - return err - } - - // Modify settings - if elWriteSettings { - // We need a voice in order to write the settings - if voice == "" { - return ErrBadParameter.With("Missing voice-id") - } - - // Change parameters - if elStability != 0.0 { - settings.Stability = float32(elStability) - } - if elSimilarityBoost != 0.0 { - settings.SimilarityBoost = float32(elSimilarityBoost) - } - if elUseSpeakerBoost != settings.UseSpeakerBoost { - settings.UseSpeakerBoost = elUseSpeakerBoost - } - - // Set voice settings - if err := elClient.SetVoiceSettings(voice, settings); err != nil { - return err - } - } - - return w.Write(settings) -} - -func elTextToSpeech(ctx context.Context, w *tablewriter.Writer, args []string) error { - // The voice to use - voice, err := elVoiceId(args[0]) - if err != nil { - return err - } - - // Output format - opts := []elevenlabs.Opt{} - if format := elOutputFormat(); format != nil { - opts = append(opts, format) - } else { - return ErrBadParameter.Withf("invalid output format %q", elExt) - } - - // The text to speak - text := strings.Join(args[1:], " ") - - // If wav, then wrap in a header - if elExt == "wav" { - // Create the writer - writer := NewAudioWriter(w.Output().(io.WriteSeeker), int(elSamplerate), 1) - defer writer.Close() - - // Read the data - if n, err := elClient.TextToSpeech(writer, voice, text, opts...); err != nil { - return err - } else { - elClient.Debugf("elTextToSpeech: generated %v bytes of PCM data", n) - } - } else if _, err := elClient.TextToSpeech(w.Output(), voice, text, opts...); err != nil { - return err - } - - // Return success - return nil -} - -///////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func elVoiceId(q string) (string, error) { - if reVoiceId.MatchString(q) { - return q, nil - } else if voices, err := elClient.Voices(); err != nil { - return "", err - } else { - for _, v := range voices { - if strings.EqualFold(v.Name, q) || v.Id == q { - return v.Id, nil - } - } - } - return "", ErrNotFound.Withf("%q", q) -} - -func elOutputFormat() elevenlabs.Opt { - switch elExt { - case "mp3": - return elevenlabs.OptFormatMP3(uint(elBitrate), uint(elSamplerate)) - case "wav": - return elevenlabs.OptFormatPCM(uint(elSamplerate)) - case "ulaw": - return elevenlabs.OptFormatULAW() - } - return nil -} - -///////////////////////////////////////////////////////////////////// -// AUDIO WRITER - -type wavWriter struct { - enc *wav.Encoder - buf *bytes.Buffer - pcm *audio.IntBuffer -} - -func NewAudioWriter(w io.WriteSeeker, sampleRate, channels int) *wavWriter { - this := new(wavWriter) - - // Create a WAV encoder - this.enc = wav.NewEncoder(w, sampleRate, 16, channels, 1) - if this.enc == nil { - return nil - } - - // Create a buffer for the incoming byte data - this.buf = bytes.NewBuffer(nil) - - // Make a PCM buffer with a capacity of 4096 samples - this.pcm = &audio.IntBuffer{ - Format: &audio.Format{ - SampleRate: this.enc.SampleRate, - NumChannels: this.enc.NumChans, - }, - SourceBitDepth: this.enc.BitDepth, - Data: make([]int, 0, 4096), - } - - // Return the writer - return this -} - -func (a *wavWriter) Write(data []byte) (int, error) { - // Write the data to the buffer - if n, err := a.buf.Write(data); err != nil { - return 0, err - } else if err := a.Flush(); err != nil { - return 0, err - } else { - return n, nil - } -} - -func (a *wavWriter) Flush() error { - var n int - var sample [2]byte - - // Read data until we have a full PCM buffer - for { - if a.buf.Len() < len(sample) { - break - } else if n, err := a.buf.Read(sample[:]); err != nil { - return err - } else if n != len(sample) { - return ErrInternalAppError.With("short read") - } - - // Append the sample data - Little Endian - a.pcm.Data = append(a.pcm.Data, int(int16(sample[0])|int16(sample[1])<<8)) - n += 2 - } - - // Write the PCM data - if n > 0 { - if err := a.enc.Write(a.pcm); err != nil { - return err - } - } - - // Reset the PCM data - a.pcm.Data = a.pcm.Data[:0] - - // Return success - return nil -} - -func (a *wavWriter) Close() error { - if err := a.Flush(); err != nil { - return err - } - return a.enc.Close() -} diff --git a/cmd/api/main.go b/cmd/api/main.go index 1c85e61..ce9a25a 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -6,29 +6,23 @@ import ( "fmt" "io" "os" + "os/signal" "path" "strings" "syscall" // Packages tablewriter "github.com/djthorpe/go-tablewriter" - mycontext "github.com/mutablelogic/go-client/pkg/context" ) func main() { flags := NewFlags(path.Base(os.Args[0])) // Register commands - anthropicRegister(flags) - authRegister(flags) bwRegister(flags) - elRegister(flags) haRegister(flags) ipifyRegister(flags) - mistralRegister(flags) newsapiRegister(flags) - openaiRegister(flags) - samRegister(flags) weatherapiRegister(flags) // Parse command line and return function to run @@ -48,7 +42,8 @@ func main() { } // Create a context - ctx := mycontext.ContextForSignal(os.Interrupt, syscall.SIGQUIT) + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGQUIT) + defer cancel() // Create a tablewriter, optionally close the stream, then run the // function diff --git a/cmd/api/mistral.go b/cmd/api/mistral.go deleted file mode 100644 index a074c2d..0000000 --- a/cmd/api/mistral.go +++ /dev/null @@ -1,185 +0,0 @@ -package main - -import ( - "context" - "fmt" - - // Packages - - "github.com/djthorpe/go-tablewriter" - "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-client/pkg/mistral" - "github.com/mutablelogic/go-client/pkg/openai/schema" -) - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -var ( - mistralName = "mistral" - mistralClient *mistral.Client - mistralModel string - mistralEncodingFormat string - mistralTemperature *float64 - mistralMaxTokens *uint64 - mistralStream *bool - mistralSafePrompt bool - mistralSeed *uint64 - mistralSystemPrompt string -) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -func mistralRegister(flags *Flags) { - // Register flags required - flags.String(mistralName, "mistral-api-key", "${MISTRAL_API_KEY}", "API Key") - flags.String(mistralName, "model", "", "Model to use") - flags.String(mistralName, "encoding-format", "", "The format of the output data") - flags.String(mistralName, "system", "", "Provide a system prompt to the model") - flags.Float(mistralName, "temperature", 0, "Sampling temperature to use, between 0.0 and 1.0") - flags.Unsigned(mistralName, "max-tokens", 0, "Maximum number of tokens to generate") - flags.Bool(mistralName, "stream", false, "Stream output") - flags.Bool(mistralName, "safe-prompt", false, "Inject a safety prompt before all conversations.") - flags.Unsigned(mistralName, "seed", 0, "Set random seed") - - flags.Register(Cmd{ - Name: mistralName, - Description: "Interact with Mistral models, from https://docs.mistral.ai/api/", - Parse: mistralParse, - Fn: []Fn{ - {Name: "models", Call: mistralModels, Description: "Gets a list of available models"}, - {Name: "embeddings", Call: mistralEmbeddings, Description: "Create embeddings from text", MinArgs: 1, Syntax: "..."}, - {Name: "chat", Call: mistralChat, Description: "Create a chat completion", MinArgs: 1, Syntax: "..."}, - }, - }) -} - -func mistralParse(flags *Flags, opts ...client.ClientOpt) error { - apiKey := flags.GetString("mistral-api-key") - if apiKey == "" { - return fmt.Errorf("missing -mistral-api-key flag") - } else if client, err := mistral.New(apiKey, opts...); err != nil { - return err - } else { - mistralClient = client - } - - // Get the command-line parameters - mistralModel = flags.GetString("model") - mistralEncodingFormat = flags.GetString("encoding-format") - mistralSafePrompt = flags.GetBool("safe-prompt") - mistralSystemPrompt = flags.GetString("system") - if temp, err := flags.GetValue("temperature"); err == nil { - t := temp.(float64) - mistralTemperature = &t - } - if maxtokens, err := flags.GetValue("max-tokens"); err == nil { - t := maxtokens.(uint64) - mistralMaxTokens = &t - } - if stream, err := flags.GetValue("stream"); err == nil { - t := stream.(bool) - mistralStream = &t - } - if seed, err := flags.GetValue("seed"); err == nil { - t := seed.(uint64) - mistralSeed = &t - } - - // Return success - return nil -} - -/////////////////////////////////////////////////////////////////////////////// -// METHODS - -func mistralModels(ctx context.Context, writer *tablewriter.Writer, args []string) error { - // Get models - models, err := mistralClient.ListModels() - if err != nil { - return err - } - - return writer.Write(models) -} - -func mistralEmbeddings(ctx context.Context, writer *tablewriter.Writer, args []string) error { - // Set options - opts := []mistral.Opt{} - if mistralModel != "" { - opts = append(opts, mistral.OptModel(mistralModel)) - } - if mistralEncodingFormat != "" { - opts = append(opts, mistral.OptEncodingFormat(mistralEncodingFormat)) - } - - // Get embeddings - embeddings, err := mistralClient.CreateEmbedding(args, opts...) - if err != nil { - return err - } - return writer.Write(embeddings) -} - -func mistralChat(ctx context.Context, w *tablewriter.Writer, args []string) error { - var messages []*schema.Message - - // Set options - opts := []mistral.Opt{} - if mistralModel != "" { - opts = append(opts, mistral.OptModel(mistralModel)) - } - if mistralTemperature != nil { - opts = append(opts, mistral.OptTemperature(float32(*mistralTemperature))) - } - if mistralMaxTokens != nil { - opts = append(opts, mistral.OptMaxTokens(int(*mistralMaxTokens))) - } - if mistralStream != nil { - opts = append(opts, mistral.OptStream(func(choice schema.MessageChoice) { - w := w.Output() - if choice.Delta == nil { - return - } - if choice.Delta.Role != "" { - fmt.Fprintf(w, "\n%v: ", choice.Delta.Role) - } - if choice.Delta.Content != "" { - fmt.Fprintf(w, "%v", choice.Delta.Content) - } - if choice.FinishReason != "" { - fmt.Printf("\nfinish_reason: %q\n", choice.FinishReason) - } - })) - } - if mistralSafePrompt { - opts = append(opts, mistral.OptSafePrompt()) - } - if mistralSeed != nil { - opts = append(opts, mistral.OptSeed(int(*mistralSeed))) - } - if mistralSystemPrompt != "" { - messages = append(messages, schema.NewMessage("system").Add(schema.Text(mistralSystemPrompt))) - } - - // Append user message - message := schema.NewMessage("user") - for _, arg := range args { - message.Add(schema.Text(arg)) - } - messages = append(messages, message) - - // Request -> Response - responses, err := mistralClient.Chat(ctx, messages, opts...) - if err != nil { - return err - } - - // Write table (if not streaming) - if mistralStream == nil || !*mistralStream { - return w.Write(responses) - } else { - return nil - } -} diff --git a/cmd/api/nginx.go_old b/cmd/api/nginx.go_old deleted file mode 100644 index 70d0525..0000000 --- a/cmd/api/nginx.go_old +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "context" - - // Packages - tablewriter "github.com/djthorpe/go-tablewriter" - client "github.com/mutablelogic/go-client" - nginx "github.com/mutablelogic/go-server/pkg/handler/nginx/client" -) - -var ( - nginxClient *nginx.Client - nginxName = "nginx" - nginxEndpoint string -) - -func nginxRegister(flags *Flags) { - flags.Register(Cmd{ - Name: nginxName, - Description: "Manage nginx instances", - Parse: nginxParse, - Fn: []Fn{ - // Default caller - {Call: nginxGetVersion, Description: "Get the nginx version that is running"}, - }, - }) -} - -func nginxParse(flags *Flags, opts ...client.ClientOpt) error { - // Register flags - flags.String(nginxName, "nginx-endpoint", "${NGINX_ENDPOINT}", "nginx endpoint") - - if client, err := nginx.New(nginxEndpoint, opts...); err != nil { - return err - } else { - nginxClient = client - } - return nil -} - -func nginxGetVersion(_ context.Context, w *tablewriter.Writer, _ []string) error { - version, _, err := nginxClient.Health() - if err != nil { - return err - } - return w.Write(version) -} diff --git a/cmd/api/openai.go b/cmd/api/openai.go deleted file mode 100644 index 8578052..0000000 --- a/cmd/api/openai.go +++ /dev/null @@ -1,380 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "os" - "strings" - - // Packages - tablewriter "github.com/djthorpe/go-tablewriter" - client "github.com/mutablelogic/go-client" - openai "github.com/mutablelogic/go-client/pkg/openai" - "github.com/mutablelogic/go-client/pkg/openai/schema" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -var ( - openaiName = "openai" - openaiClient *openai.Client - openaiModel string - openaiQuality bool - openaiResponseFormat string - openaiStyle string - openaiFrequencyPenalty *float64 - openaiPresencePenalty *float64 - openaiMaxTokens uint64 - openaiCount *uint64 - openaiStream bool - openaiTemperature *float64 - openaiUser string - openaiSystemPrompt string - openaiPrompt string - openaiLanguage string - openaiExt string - openaiSpeed float64 -) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -func openaiRegister(flags *Flags) { - // Register flags - flags.String(openaiName, "openai-api-key", "${OPENAI_API_KEY}", "OpenAI API key") - // TODO flags.String(openaiName, "model", "", "The model to use") - // TODO flags.Unsigned(openaiName, "max-tokens", 0, "The maximum number of tokens that can be generated in the chat completion") - flags.Bool(openaiName, "hd", false, "Create images with finer details and greater consistency across the image") - flags.String(openaiName, "response-format", "", "The format in which the generated images are returned") - flags.String(openaiName, "style", "", "The style of the generated images. Must be one of vivid or natural") - flags.String(openaiName, "user", "", "A unique identifier representing your end-user") - flags.Float(openaiName, "frequency-penalty", 0, "The model's likelihood to repeat the same line verbatim") - flags.Float(openaiName, "presence-penalty", 0, "The model's likelihood to talk about new topics") - flags.Unsigned(openaiName, "n", 0, "How many chat completion choices to generate for each input message") - // TODO flags.String(openaiName, "system", "", "The system prompt") - // TODO flags.Bool(openaiName, "stream", false, "If set, partial message deltas will be sent, like in ChatGPT") - // TODO flags.Float(openaiName, "temperature", 0, "Sampling temperature to use, between 0.0 and 2.0") - flags.String(openaiName, "prompt", "", "An optional text to guide the model's style or continue a previous audio segment") - //flags.String(openaiName, "language", "", "The language of the input audio in ISO-639-1 format") - flags.Float(openaiName, "speed", 0, "The speed of the generated audio") - - // Register commands - flags.Register(Cmd{ - Name: openaiName, - Description: "Interact with OpenAI, from https://platform.openai.com/docs/api-reference", - Parse: openaiParse, - Fn: []Fn{ - {Name: "models", Call: openaiListModels, Description: "Gets a list of available models"}, - {Name: "model", Call: openaiGetModel, Description: "Return model information", MinArgs: 1, MaxArgs: 1, Syntax: ""}, - {Name: "image", Call: openaiImage, Description: "Create image from a prompt", MinArgs: 1, Syntax: ""}, - {Name: "chat", Call: openaiChat, Description: "Create a chat completion", MinArgs: 1, Syntax: "..."}, - {Name: "transcribe", Call: openaiTranscribe, Description: "Transcribes audio into the input language", MinArgs: 1, MaxArgs: 1, Syntax: ""}, - {Name: "translate", Call: openaiTranslate, Description: "Translates audio into English", MinArgs: 1, MaxArgs: 1, Syntax: ""}, - {Name: "say", Call: openaiTextToSpeech, Description: "Text to speech", MinArgs: 2, Syntax: " ..."}, - {Name: "moderations", Call: openaiModerations, Description: "Classifies text across several categories", MinArgs: 1, Syntax: "..."}, - }, - }) -} - -func openaiParse(flags *Flags, opts ...client.ClientOpt) error { - apiKey := flags.GetString("openai-api-key") - if apiKey == "" { - return fmt.Errorf("missing -openai-api-key flag") - } else if client, err := openai.New(apiKey, opts...); err != nil { - return err - } else { - openaiClient = client - } - - // Set arguments - openaiModel = flags.GetString("model") - openaiQuality = flags.GetBool("hd") - openaiResponseFormat = flags.GetString("response-format") - openaiStyle = flags.GetString("style") - openaiStream = flags.GetBool("stream") - openaiUser = flags.GetString("user") - openaiSystemPrompt = flags.GetString("system") - openaiPrompt = flags.GetString("prompt") - openaiLanguage = flags.GetString("language") - openaiExt = flags.GetOutExt() - - if temp, err := flags.GetValue("temperature"); err == nil { - t := temp.(float64) - openaiTemperature = &t - } - if value, err := flags.GetValue("frequency-penalty"); err == nil { - v := value.(float64) - openaiFrequencyPenalty = &v - } - if value, err := flags.GetValue("presence-penalty"); err == nil { - v := value.(float64) - openaiPresencePenalty = &v - } - if maxtokens, err := flags.GetValue("max-tokens"); err == nil { - t := maxtokens.(uint64) - openaiMaxTokens = t - } - if count, err := flags.GetValue("n"); err == nil { - v := count.(uint64) - openaiCount = &v - } - if speed, err := flags.GetValue("speed"); err == nil { - openaiSpeed = speed.(float64) - } - - // Return success - return nil -} - -/////////////////////////////////////////////////////////////////////////////// -// METHODS - -func openaiListModels(ctx context.Context, w *tablewriter.Writer, args []string) error { - models, err := openaiClient.ListModels() - if err != nil { - return err - } - return w.Write(models) -} - -func openaiGetModel(ctx context.Context, w *tablewriter.Writer, args []string) error { - model, err := openaiClient.GetModel(args[0]) - if err != nil { - return err - } - return w.Write(model) -} - -func openaiImage(ctx context.Context, w *tablewriter.Writer, args []string) error { - opts := []openai.Opt{} - prompt := strings.Join(args, " ") - - // Process options - if openaiModel != "" { - opts = append(opts, openai.OptModel(openaiModel)) - } - if openaiQuality { - opts = append(opts, openai.OptQuality("hd")) - } - if openaiResponseFormat != "" { - opts = append(opts, openai.OptResponseFormat(openaiResponseFormat)) - } - if openaiStyle != "" { - opts = append(opts, openai.OptStyle(openaiStyle)) - } - if openaiUser != "" { - opts = append(opts, openai.OptUser(openaiUser)) - } - - // Request->Response - response, err := openaiClient.CreateImages(ctx, prompt, opts...) - if err != nil { - return err - } else if len(response) == 0 { - return ErrUnexpectedResponse.With("no images returned") - } - - // Write each image - var result error - for _, image := range response { - if n, err := openaiClient.WriteImage(w.Output(), image); err != nil { - result = errors.Join(result, err) - } else { - openaiClient.Debugf("openaiImage: wrote %v bytes", n) - } - } - - // Return success - return nil -} - -func openaiChat(ctx context.Context, w *tablewriter.Writer, args []string) error { - var messages []*schema.Message - - // Set options - opts := []openai.Opt{} - if openaiModel != "" { - opts = append(opts, openai.OptModel(openaiModel)) - } - if openaiFrequencyPenalty != nil { - opts = append(opts, openai.OptFrequencyPenalty(float32(*openaiFrequencyPenalty))) - } - if openaiPresencePenalty != nil { - opts = append(opts, openai.OptPresencePenalty(float32(*openaiPresencePenalty))) - } - if openaiTemperature != nil { - opts = append(opts, openai.OptTemperature(float32(*openaiTemperature))) - } - if openaiMaxTokens != 0 { - opts = append(opts, openai.OptMaxTokens(int(openaiMaxTokens))) - } - if openaiCount != nil && *openaiCount > 1 { - opts = append(opts, openai.OptCount(int(*openaiCount))) - } - if openaiResponseFormat != "" { - // TODO: Should be an object, not a string - opts = append(opts, openai.OptResponseFormat(openaiResponseFormat)) - } - if openaiStream { - opts = append(opts, openai.OptStream(func(choice schema.MessageChoice) { - w := w.Output() - if choice.Delta == nil { - return - } - if choice.Delta.Role != "" { - fmt.Fprintf(w, "\nrole: %q\n", choice.Delta.Role) - } - if choice.Delta.Content != "" { - fmt.Fprintf(w, "%v", choice.Delta.Content) - } - if choice.FinishReason != "" { - fmt.Printf("\nfinish_reason: %q\n", choice.FinishReason) - } - })) - } - if openaiUser != "" { - opts = append(opts, openai.OptUser(openaiUser)) - } - if openaiSystemPrompt != "" { - messages = append(messages, schema.NewMessage("system").Add(schema.Text(openaiSystemPrompt))) - } - - // Append user message - message := schema.NewMessage("user") - for _, arg := range args { - message.Add(schema.Text(arg)) - } - messages = append(messages, message) - - // Request->Response - responses, err := openaiClient.Chat(ctx, messages, opts...) - if err != nil { - return err - } - - // Write table (if not streaming) - if !openaiStream { - return w.Write(responses) - } else { - return nil - } -} - -func openaiTranscribe(ctx context.Context, w *tablewriter.Writer, args []string) error { - opts := []openai.Opt{} - if openaiModel != "" { - opts = append(opts, openai.OptModel(openaiModel)) - } - if openaiPrompt != "" { - opts = append(opts, openai.OptPrompt(openaiPrompt)) - } - if openaiLanguage != "" { - opts = append(opts, openai.OptLanguage(openaiLanguage)) - } - if openaiResponseFormat != "" { - opts = append(opts, openai.OptResponseFormat(openaiResponseFormat)) - } - if openaiTemperature != nil { - opts = append(opts, openai.OptTemperature(float32(*openaiTemperature))) - } - - // Open audio file for reading - r, err := os.Open(args[0]) - if err != nil { - return err - } - defer r.Close() - - // Perform transcription - transcription, err := openaiClient.Transcribe(ctx, r, opts...) - if err != nil { - return err - } - - // Write output - return w.Write(transcription) -} - -func openaiTranslate(ctx context.Context, w *tablewriter.Writer, args []string) error { - opts := []openai.Opt{} - - if openaiModel != "" { - opts = append(opts, openai.OptModel(openaiModel)) - } - if openaiPrompt != "" { - opts = append(opts, openai.OptPrompt(openaiPrompt)) - } - if openaiResponseFormat != "" { - opts = append(opts, openai.OptResponseFormat(openaiResponseFormat)) - } - if openaiTemperature != nil { - opts = append(opts, openai.OptTemperature(float32(*openaiTemperature))) - } - - // Open audio file for reading - r, err := os.Open(args[0]) - if err != nil { - return err - } - defer r.Close() - - // Perform translation - transcription, err := openaiClient.Translate(ctx, r, opts...) - if err != nil { - return err - } - - // Write output - return w.Write(transcription) -} - -func openaiTextToSpeech(ctx context.Context, w *tablewriter.Writer, args []string) error { - opts := []openai.Opt{} - - // Set response format - if openaiResponseFormat != "" { - opts = append(opts, openai.OptResponseFormat(openaiResponseFormat)) - } else if openaiExt != "" { - opts = append(opts, openai.OptResponseFormat(openaiExt)) - } - - // Set other options - if openaiSpeed > 0 { - opts = append(opts, openai.OptSpeed(float32(openaiSpeed))) - } - - // The text to speak - voice := args[0] - text := strings.Join(args[1:], " ") - - // Request -> Response - if n, err := openaiClient.TextToSpeech(ctx, w.Output(), voice, text, opts...); err != nil { - return err - } else { - openaiClient.Debugf("wrote %v bytes", n) - } - - // Return success - return nil -} - -func openaiModerations(ctx context.Context, w *tablewriter.Writer, args []string) error { - opts := []openai.Opt{} - if openaiModel != "" { - opts = append(opts, openai.OptModel(openaiModel)) - } - - // Request -> Response - response, err := openaiClient.Moderations(ctx, args, opts...) - if err != nil { - return err - } - - // Write output - return w.Write(response) -} diff --git a/cmd/api/samantha.go b/cmd/api/samantha.go deleted file mode 100644 index bf9d896..0000000 --- a/cmd/api/samantha.go +++ /dev/null @@ -1,287 +0,0 @@ -package main - -import ( - "bufio" - "context" - "encoding/json" - "fmt" - "os" - "reflect" - "strings" - - // Packages - "github.com/djthorpe/go-tablewriter" - "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-client/pkg/anthropic" - "github.com/mutablelogic/go-client/pkg/newsapi" - "github.com/mutablelogic/go-client/pkg/openai/schema" -) - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -var ( - samName = "sam" - samWeatherTool = schema.NewTool("get_current_weather", "Get the current weather conditions for a location") - samNewsHeadlinesTool = schema.NewTool("get_news_headlines", "Get the news headlines") - samNewsSearchTool = schema.NewTool("search_news", "Search news articles") - samHomeAssistantTool = schema.NewTool("get_home_devices", "Return information about home devices by type, including their state and entity_id") - samHomeAssistantSearch = schema.NewTool("search_home_devices", "Return information about home devices by name, including their state and entity_id") - samHomeAssistantTurnOn = schema.NewTool("turn_on_device", "Turn on a device") - samHomeAssistantTurnOff = schema.NewTool("turn_off_device", "Turn off a device") - samSystemPrompt = `Your name is Samantha, you are a personal assistant modelled on the personality of Samantha from the movie "Her". Your responses should be short and friendly.` -) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -func samRegister(flags *Flags) { - flags.Register(Cmd{ - Name: samName, - Description: "Interact with Samantha, a friendly AI assistant, to query news and weather", - Parse: samParse, - Fn: []Fn{ - {Name: "chat", Call: samChat, Description: "Chat with Sam"}, - }, - }) -} - -func samParse(flags *Flags, opts ...client.ClientOpt) error { - // Initialize weather - if err := weatherapiParse(flags, opts...); err != nil { - return err - } - // Initialize news - if err := newsapiParse(flags, opts...); err != nil { - return err - } - // Initialize home assistant - if err := haParse(flags, opts...); err != nil { - return err - } - // Initialize anthropic - opts = append(opts, client.OptHeader("Anthropic-Beta", "tools-2024-04-04")) - if err := anthropicParse(flags, opts...); err != nil { - return err - } - - // Add tool parameters - if err := samWeatherTool.Add("location", `City to get the weather for. If a country, use the capital city. To get weather for the current location, use "auto:ip"`, true, reflect.TypeOf("")); err != nil { - return err - } - if err := samNewsHeadlinesTool.Add("category", "The cateogry of news, which should be one of business, entertainment, general, health, science, sports or technology", true, reflect.TypeOf("")); err != nil { - return err - } - if err := samNewsHeadlinesTool.Add("country", "Headlines from agencies in a specific country. Optional. Use ISO 3166 country code.", false, reflect.TypeOf("")); err != nil { - return err - } - if err := samNewsSearchTool.Add("query", "The query with which to search news", true, reflect.TypeOf("")); err != nil { - return err - } - if err := samHomeAssistantTool.Add("type", "Query for a device type, which could one or more of door,lock,occupancy,motion,climate,light,switch,sensor,speaker,media_player,temperature,humidity,battery,tv,remote,light,vacuum separated by spaces", true, reflect.TypeOf("")); err != nil { - return err - } - if err := samHomeAssistantSearch.Add("name", "Search for device state by name", true, reflect.TypeOf("")); err != nil { - return err - } - if err := samHomeAssistantTurnOn.Add("entity_id", "The device entity_id to turn on", true, reflect.TypeOf("")); err != nil { - return err - } - if err := samHomeAssistantTurnOff.Add("entity_id", "The device entity_id to turn off", true, reflect.TypeOf("")); err != nil { - return err - } - - // Return success - return nil -} - -/////////////////////////////////////////////////////////////////////////////// -// METHODS - -func samChat(ctx context.Context, w *tablewriter.Writer, _ []string) error { - var toolResult bool - - messages := []*schema.Message{} - for { - if ctx.Err() != nil { - return nil - } - - // Read if there hasn't been any tool results yet - if !toolResult { - reader := bufio.NewReader(os.Stdin) - fmt.Print("prompt: ") - text, err := reader.ReadString('\n') - if err != nil { - return err - } - if text := strings.TrimSpace(text); text == "" { - continue - } else if text == "exit" { - return nil - } else { - messages = append(messages, schema.NewMessage("user", schema.Text(strings.TrimSpace(text)))) - } - } - - // Curtail requests to the last N history - if len(messages) > 10 { - messages = messages[len(messages)-10:] - - // First message must have role 'user' and not be a tool_result - for { - if len(messages) == 0 { - break - } - if messages[0].Role == "user" { - if content, ok := messages[0].Content.([]schema.Content); ok { - if len(content) > 0 && content[0].Type != "tool_result" { - break - } - } else { - break - } - } - messages = messages[1:] - } - } - - // Request -> Response - responses, err := anthropicClient.Messages(ctx, messages, - anthropic.OptSystem(samSystemPrompt), - anthropic.OptMaxTokens(1000), - anthropic.OptTool(samWeatherTool), - anthropic.OptTool(samNewsHeadlinesTool), - anthropic.OptTool(samNewsSearchTool), - anthropic.OptTool(samHomeAssistantTool), - anthropic.OptTool(samHomeAssistantSearch), - anthropic.OptTool(samHomeAssistantTurnOn), - anthropic.OptTool(samHomeAssistantTurnOff), - ) - toolResult = false - if err != nil { - messages = samAppend(messages, schema.NewMessage("assistant", schema.Text(fmt.Sprint("An error occurred: ", err)))) - fmt.Println(err) - fmt.Println("") - } else { - for _, response := range responses { - switch response.Type { - case "text": - messages = samAppend(messages, schema.NewMessage("assistant", schema.Text(response.Text))) - fmt.Println(response.Text) - fmt.Println("") - case "tool_use": - messages = samAppend(messages, schema.NewMessage("assistant", response)) - result := samCall(ctx, response) - messages = samAppend(messages, schema.NewMessage("user", result)) - toolResult = true - } - } - } - } -} - -func samCall(_ context.Context, content *schema.Content) *schema.Content { - anthropicClient.Debugf("%v: %v: %v", content.Type, content.Name, content.Input) - if content.Type != "tool_use" { - return schema.ToolResult(content.Id, fmt.Sprint("unexpected content type:", content.Type)) - } - switch content.Name { - case samWeatherTool.Name: - var location string - if v, exists := content.GetString(content.Name, "location"); exists { - location = v - } else { - location = "auto:ip" - } - if weather, err := weatherapiClient.Current(location); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to get the current weather, the error is ", err)) - } else if data, err := json.MarshalIndent(weather, "", " "); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to marshal the weather data, the error is ", err)) - } else { - return schema.ToolResult(content.Id, string(data)) - } - case samNewsHeadlinesTool.Name: - var category string - if v, exists := content.GetString(content.Name, "category"); exists { - category = v - } else { - category = "general" - } - country, _ := content.GetString(content.Name, "country") - if headlines, err := newsapiClient.Headlines(newsapi.OptCategory(category), newsapi.OptCountry(country)); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to get the news headlines, the error is ", err)) - } else if data, err := json.MarshalIndent(headlines, "", " "); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to marshal the headlines data, the error is ", err)) - } else { - return schema.ToolResult(content.Id, string(data)) - } - case samNewsSearchTool.Name: - var query string - if v, exists := content.GetString(content.Name, "query"); exists { - query = v - } else { - return schema.ToolResult(content.Id, "Unable to search news due to missing query") - } - if articles, err := newsapiClient.Articles(newsapi.OptQuery(query), newsapi.OptLimit(5)); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to search news, the error is ", err)) - } else if data, err := json.MarshalIndent(articles, "", " "); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to marshal the articles data, the error is ", err)) - } else { - return schema.ToolResult(content.Id, string(data)) - } - case samHomeAssistantTool.Name: - classes, exists := content.GetString(content.Name, "type") - if !exists || classes == "" { - return schema.ToolResult(content.Id, "Unable to get home devices due to missing type") - } - if states, err := haGetStates("", strings.Fields(classes)); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to get home devices, the error is ", err)) - } else if data, err := json.MarshalIndent(states, "", " "); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to marshal the states data, the error is ", err)) - } else { - return schema.ToolResult(content.Id, string(data)) - } - case samHomeAssistantSearch.Name: - name, exists := content.GetString(content.Name, "name") - if !exists || name == "" { - return schema.ToolResult(content.Id, "Unable to search home devices due to missing name") - } - if states, err := haGetStates(name, nil); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to get home devices, the error is ", err)) - } else if data, err := json.MarshalIndent(states, "", " "); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to marshal the states data, the error is ", err)) - } else { - return schema.ToolResult(content.Id, string(data)) - } - case samHomeAssistantTurnOn.Name: - entity, _ := content.GetString(content.Name, "entity_id") - if _, err := haClient.Call("turn_on", entity); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to turn on device, the error is ", err)) - } else if state, err := haClient.State(entity); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to get device state, the error is ", err)) - } else { - return schema.ToolResult(content.Id, fmt.Sprint("The updated state is: ", state)) - } - case samHomeAssistantTurnOff.Name: - entity, _ := content.GetString(content.Name, "entity_id") - if _, err := haClient.Call("turn_off", entity); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to turn off device, the error is ", err)) - } else if state, err := haClient.State(entity); err != nil { - return schema.ToolResult(content.Id, fmt.Sprint("Unable to get device state, the error is ", err)) - } else { - return schema.ToolResult(content.Id, fmt.Sprint("The updated state is: ", state)) - } - } - return schema.ToolResult(content.Id, fmt.Sprint("unable to call:", content.Name)) -} - -func samAppend(messages []*schema.Message, message *schema.Message) []*schema.Message { - // if the previous message was of the same role, then append the new message to the previous one - if len(messages) > 0 && messages[len(messages)-1].Role == message.Role { - messages[len(messages)-1].Add(message.Content) - return messages - } else { - return append(messages, message) - } -} diff --git a/go.mod b/go.mod index c361f97..c8ddea8 100644 --- a/go.mod +++ b/go.mod @@ -1,58 +1,29 @@ module github.com/mutablelogic/go-client -go 1.23.5 - -toolchain go1.24.2 +go 1.24.0 require ( - github.com/alecthomas/kong v1.10.0 github.com/andreburgaud/crypt2go v1.8.0 github.com/djthorpe/go-errors v1.0.3 github.com/djthorpe/go-tablewriter v0.0.11 - github.com/go-audio/audio v1.0.0 - github.com/go-audio/wav v1.1.0 - github.com/mutablelogic/go-server v1.5.9 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/xdg-go/pbkdf2 v1.0.0 - golang.org/x/crypto v0.37.0 - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 - golang.org/x/term v0.31.0 + golang.org/x/crypto v0.41.0 + golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b + golang.org/x/term v0.34.0 ) require ( - github.com/MichaelMure/go-term-text v0.3.1 // indirect - github.com/alecthomas/chroma v0.10.0 // indirect - github.com/disintegration/imaging v1.6.2 // indirect - github.com/djthorpe/go-pg v1.0.5 // indirect - github.com/dlclark/regexp2 v1.11.5 // indirect - github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.3 // indirect - github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/kr/pretty v0.3.1 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/kyokomi/emoji/v2 v2.2.13 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - golang.org/x/image v0.26.0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/text v0.24.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) require ( - github.com/MichaelMure/go-term-markdown v0.1.4 github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-audio/riff v1.0.0 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - golang.org/x/sys v0.32.0 // indirect + golang.org/x/sys v0.35.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d21d6d8..b1f9e64 100644 --- a/go.sum +++ b/go.sum @@ -1,129 +1,44 @@ -github.com/MichaelMure/go-term-markdown v0.1.4 h1:Ir3kBXDUtOX7dEv0EaQV8CNPpH+T7AfTh0eniMOtNcs= -github.com/MichaelMure/go-term-markdown v0.1.4/go.mod h1:EhcA3+pKYnlUsxYKBJ5Sn1cTQmmBMjeNlpV8nRb+JxA= -github.com/MichaelMure/go-term-text v0.3.1 h1:Kw9kZanyZWiCHOYu9v/8pWEgDQ6UVN9/ix2Vd2zzWf0= -github.com/MichaelMure/go-term-text v0.3.1/go.mod h1:QgVjAEDUnRMlzpS6ky5CGblux7ebeiLnuy9dAaFZu8o= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= -github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= -github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma v0.7.1/go.mod h1:gHw09mkX1Qp80JlYbmN9L3+4R5o6DJJ3GRShh+AICNc= -github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= -github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= -github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= -github.com/alecthomas/kong v1.10.0 h1:8K4rGDpT7Iu+jEXCIJUeKqvpwZHbsFRoebLbnzlmrpw= -github.com/alecthomas/kong v1.10.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= -github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/andreburgaud/crypt2go v1.8.0 h1:J73vGTb1P6XL69SSuumbKs0DWn3ulbl9L92ZXBjw6pc= github.com/andreburgaud/crypt2go v1.8.0/go.mod h1:L5nfShQ91W78hOWhUH2tlGRPO+POAPJAF5fKOLB9SXg= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= -github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/djthorpe/go-errors v1.0.3 h1:GZeMPkC1mx2vteXLI/gvxZS0Ee9zxzwD1mcYyKU5jD0= github.com/djthorpe/go-errors v1.0.3/go.mod h1:HtfrZnMd6HsX75Mtbv9Qcnn0BqOrrFArvCaj3RMnZhY= github.com/djthorpe/go-tablewriter v0.0.11 h1:CimrEsiAG/KN2C8bTDC85RsZTsP2s5a7m7dqhaGFTv0= github.com/djthorpe/go-tablewriter v0.0.11/go.mod h1:ednj4tB4GHpenQL6NtDrbQW9VXyDdbIVTSH2693B+lI= -github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= -github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75 h1:vbix8DDQ/rfatfFr/8cf/sJfIL69i4BcZfjrVOxsMqk= -github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75/go.mod h1:0gZuvTO1ikSA5LtTI6E13LEOdWQNjIo5MTQOvrV0eFg= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4= -github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= -github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= -github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= -github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g= -github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE= -github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU= -github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk= -github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kyokomi/emoji/v2 v2.2.8/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= -github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U= -github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= -github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mutablelogic/go-server v1.4.7 h1:NpzG30f/D50Xbwr96dA6uiapyr4QHBziSanc/q/LR7k= -github.com/mutablelogic/go-server v1.4.7/go.mod h1:wrrDg863hlv5/DUpSG/Pb4k9LiSYO7VxRgLPiMhrE6M= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= -golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= -golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go deleted file mode 100644 index bc6381f..0000000 --- a/pkg/agent/agent.go +++ /dev/null @@ -1,18 +0,0 @@ -package agent - -import "context" - -// An LLM Agent is a client for the LLM service -type Agent interface { - // Return the name of the agent - Name() string - - // Return the models - Models(context.Context) ([]Model, error) - - // Generate a response from a prompt - Generate(context.Context, Model, []Context, ...Opt) (*Response, error) - - // Create user message context - UserPrompt(string) Context -} diff --git a/pkg/agent/context.go b/pkg/agent/context.go deleted file mode 100644 index 2dce2a6..0000000 --- a/pkg/agent/context.go +++ /dev/null @@ -1,10 +0,0 @@ -package agent - -////////////////////////////////////////////////////////////////// -// TYPES - -// Context is fed to the agent to generate a response. Role can be -// assistant, user, tool, tool_result, ... -type Context interface { - Role() string -} diff --git a/pkg/agent/model.go b/pkg/agent/model.go deleted file mode 100644 index 3329bf4..0000000 --- a/pkg/agent/model.go +++ /dev/null @@ -1,7 +0,0 @@ -package agent - -// An LLM Agent is a client for the LLM service -type Model interface { - // Return the name of the model - Name() string -} diff --git a/pkg/agent/opt.go b/pkg/agent/opt.go deleted file mode 100644 index 235f0a4..0000000 --- a/pkg/agent/opt.go +++ /dev/null @@ -1,41 +0,0 @@ -package agent - -import "fmt" - -////////////////////////////////////////////////////////////////// -// TYPES - -type Opts struct { - Tools []Tool - StreamFn func(Response) -} - -type Opt func(*Opts) error - -////////////////////////////////////////////////////////////////// -// METHODS - -// OptStream sets the stream function, which is called during the -// response generation process -func OptStream(fn func(Response)) Opt { - return func(o *Opts) error { - o.StreamFn = fn - return nil - } -} - -// OptTools sets the tools for the chat request -func OptTools(t ...Tool) Opt { - return func(o *Opts) error { - if len(t) == 0 { - return fmt.Errorf("no tools specified") - } - for _, tool := range t { - if tool == nil { - return fmt.Errorf("nil tool specified") - } - o.Tools = append(o.Tools, tool) - } - return nil - } -} diff --git a/pkg/agent/response.go b/pkg/agent/response.go deleted file mode 100644 index 6b30671..0000000 --- a/pkg/agent/response.go +++ /dev/null @@ -1,30 +0,0 @@ -package agent - -import ( - "encoding/json" - "time" -) - -////////////////////////////////////////////////////////////////// -// TYPES - -type Response struct { - Agent string `json:"agent,omitempty"` // The agent name - Model string `json:"model,omitempty"` // The model name - Context []Context `json:"context,omitempty"` // The context for the response - Text string `json:"text,omitempty"` // The response text - *ToolCall `json:"tool,omitempty"` // The tool call, if not nil - Tokens uint `json:"tokens,omitempty"` // The number of tokens - Duration time.Duration `json:"duration,omitempty"` // The response duration -} - -////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (r Response) String() string { - data, err := json.MarshalIndent(r, "", " ") - if err != nil { - return err.Error() - } - return string(data) -} diff --git a/pkg/agent/tool.go b/pkg/agent/tool.go deleted file mode 100644 index c71cd81..0000000 --- a/pkg/agent/tool.go +++ /dev/null @@ -1,99 +0,0 @@ -package agent - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -//////////////////////////////////////////////////////////////////////////////// -// TYPES - -// A tool can be called from an LLM -type Tool interface { - // Return the provider of the tool - Provider() string - - // Return the name of the tool - Name() string - - // Return the description of the tool - Description() string - - // Tool parameters - Params() []ToolParameter - - // Execute the tool with a specific tool - Run(context.Context, *ToolCall) (*ToolResult, error) -} - -// A tool parameter -type ToolParameter struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - Required bool `json:"required,omitempty"` -} - -// A call to a tool -type ToolCall struct { - Id string `json:"id"` - Name string `json:"name"` - Args map[string]any `json:"args"` -} - -// The result of a tool call -type ToolResult struct { - Id string `json:"id"` - Result map[string]any `json:"result,omitempty"` -} - -//////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// Return the arguments for the call as a JSON -func (t *ToolCall) JSON() string { - data, err := json.MarshalIndent(t.Args, "", " ") - if err != nil { - return err.Error() - } else { - return string(data) - } -} - -// Return role for the tool result -func (t *ToolResult) Role() string { - return "tool" -} - -// Return parameter as a string -func (t *ToolCall) String(name string) (string, error) { - v, ok := t.Args[name] - if !ok { - return "", ErrNotFound.Withf("%q not found", name) - } - return fmt.Sprint(v), nil -} - -// Return parameter as an integer -func (t *ToolCall) Int(name string) (int, error) { - v, ok := t.Args[name] - if !ok { - return 0, ErrNotFound.Withf("%q not found", name) - } - switch v := v.(type) { - case int: - return v, nil - case string: - if v_, err := strconv.ParseInt(v, 10, 32); err != nil { - return 0, ErrBadParameter.Withf("%q: %v", name, err) - } else { - return int(v_), nil - } - default: - return 0, ErrBadParameter.Withf("%q: Expected integer, got %T", name, v) - } -} diff --git a/pkg/anthropic/README.md b/pkg/anthropic/README.md deleted file mode 100644 index e8e8838..0000000 --- a/pkg/anthropic/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Anthropic API Client - -This package provides a client for the Anthropic API, which is used to interact with the Claude LLM. - -References: -- API https://docs.anthropic.com/en/api/getting-started -- Package https://pkg.go.dev/github.com/mutablelogic/go-client/pkg/anthropic - diff --git a/pkg/anthropic/client.go b/pkg/anthropic/client.go deleted file mode 100644 index ece824b..0000000 --- a/pkg/anthropic/client.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -anthropic implements an API client for anthropic (https://docs.anthropic.com/en/api/getting-started) -*/ -package anthropic - -import ( - // Packages - "github.com/mutablelogic/go-client" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type Client struct { - *client.Client -} - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -const ( - endPoint = "https://api.anthropic.com/v1" - defaultVersion = "2023-06-01" - defaultMessageModel = "claude-3-haiku-20240307" - defaultMaxTokens = 1024 -) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -// Create a new client -func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { - // Create client - opts = append(opts, client.OptEndpoint(endPoint)) - opts = append(opts, client.OptHeader("x-api-key", ApiKey), client.OptHeader("anthropic-version", defaultVersion)) - client, err := client.New(opts...) - if err != nil { - return nil, err - } - - // Return the client - return &Client{client}, nil -} diff --git a/pkg/anthropic/client_test.go b/pkg/anthropic/client_test.go deleted file mode 100644 index ad0c0c1..0000000 --- a/pkg/anthropic/client_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package anthropic_test - -import ( - "os" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - anthropic "github.com/mutablelogic/go-client/pkg/anthropic" - assert "github.com/stretchr/testify/assert" -) - -func Test_client_001(t *testing.T) { - assert := assert.New(t) - client, err := anthropic.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - t.Log(client) -} - -/////////////////////////////////////////////////////////////////////////////// -// ENVIRONMENT - -func GetApiKey(t *testing.T) string { - key := os.Getenv("ANTHROPIC_API_KEY") - if key == "" { - t.Skip("ANTHROPIC_API_KEY not set") - t.SkipNow() - } - return key -} diff --git a/pkg/anthropic/messages.go b/pkg/anthropic/messages.go deleted file mode 100644 index 48feb50..0000000 --- a/pkg/anthropic/messages.go +++ /dev/null @@ -1,250 +0,0 @@ -package anthropic - -import ( - "context" - "encoding/json" - "io" - - // Packages - client "github.com/mutablelogic/go-client" - schema "github.com/mutablelogic/go-client/pkg/openai/schema" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -// A request for a message -type reqMessage struct { - options - Messages []*schema.Message `json:"messages,omitempty"` -} - -// A response to a message generation request -type respMessage struct { - Id string `json:"id"` - Model string `json:"model"` - Type string `json:"type,omitempty"` - Role string `json:"role"` - Content []*schema.Content `json:"content"` - TokenUsage Usage `json:"usage,omitempty"` - respStopReason -} - -type respStopReason struct { - StopReason string `json:"stop_reason,omitempty"` - StopSequence string `json:"stop_sequence,omitempty"` -} - -// Token usage for messages -type Usage struct { - InputTokens int `json:"input_tokens"` - OutputTokens int `json:"output_tokens"` -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// Send a structured list of input messages with text and/or image content, -// and the model will generate the next message in the conversation. Use -// a context to cancel the request, instead of the client-related timeout. -func (c *Client) Messages(ctx context.Context, messages []*schema.Message, opts ...Opt) ([]*schema.Content, error) { - var request reqMessage - var response respMessage - - // Set request options - request.Model = defaultMessageModel - request.Messages = messages - request.MaxTokens = defaultMaxTokens - for _, opt := range opts { - if err := opt(&request.options); err != nil { - return nil, err - } - } - - // Switch parameters -> input_schema - for _, tool := range request.options.Tools { - tool.InputSchema, tool.Parameters = tool.Parameters, nil - } - - // Set up the request - reqopts := []client.RequestOpt{ - client.OptPath("messages"), - } - if request.Stream { - reqopts = append(reqopts, client.OptTextStreamCallback(func(event client.TextStreamEvent) error { - return response.streamCallback(event, request.options.StreamCallback) - })) - } - - // Request -> Response - if payload, err := client.NewJSONRequest(request); err != nil { - return nil, err - } else if err := c.DoWithContext(ctx, payload, &response, reqopts...); err != nil { - return nil, err - } else if len(response.Content) == 0 { - return nil, ErrInternalAppError.With("No content returned") - } - - // Return success - return response.Content, nil -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func (response *respMessage) streamCallback(v client.TextStreamEvent, fn Callback) error { - switch v.Event { - case "ping": - // No-op - return nil - case "message_start": - // Populate the response - var message struct { - Type string `json:"type"` - Message *respMessage `json:"message"` - } - message.Message = response - if err := v.Json(&message); err != nil { - return err - } - - // Text callback - if fn != nil { - fn(schema.MessageChoice{ - Delta: &schema.MessageDelta{ - Role: message.Message.Role, - }, - }) - } - case "content_block_start": - // Create a new content block - var content struct { - Type string `json:"type"` - Index int `json:"index"` - Content *schema.Content `json:"content_block"` - } - if err := v.Json(&content); err != nil { - return err - } - // Sanity check - if len(response.Content) != content.Index { - return ErrUnexpectedResponse.With("content block index out of range") - } - // Append content block - response.Content = append(response.Content, content.Content) - case "content_block_delta": - // Append to an existing content block - var content struct { - Type string `json:"type"` - Index int `json:"index"` - Content *schema.Content `json:"delta"` - } - if err := v.Json(&content); err != nil { - return err - } - - // Sanity check - if content.Index >= len(response.Content) { - return ErrUnexpectedResponse.With("content block index out of range") - } - - // Append either text or tool_use - contentBlock := response.Content[content.Index] - switch content.Content.Type { - case "text_delta": - if contentBlock.Type != "text" { - return ErrUnexpectedResponse.With("content block delta is not text") - } else { - contentBlock.Text += content.Content.Text - } - - // Text callback - if fn != nil { - fn(schema.MessageChoice{ - Index: content.Index, - Delta: &schema.MessageDelta{ - Content: content.Content.Text, - }, - }) - } - - case "input_json_delta": - if contentBlock.Type != "tool_use" { - return ErrUnexpectedResponse.With("content block delta is not tool_use") - } else if content.Content.Json != "" { - contentBlock.Json += content.Content.Json - } - default: - return ErrUnexpectedResponse.With(content.Content.Type) - } - - case "content_block_stop": - // Append to an existing content block - var content struct { - Type string `json:"type"` - Index int `json:"index"` - } - if err := v.Json(&content); err != nil { - return err - } - - // Sanity check - if content.Index >= len(response.Content) { - return ErrInternalAppError.With("content block index out of range") - } - - // Decode the partial_json into the input - contentBlock := response.Content[content.Index] - if contentBlock.Type == "tool_use" { - if partialJson := []byte(contentBlock.Json); len(partialJson) > 0 { - if err := json.Unmarshal(partialJson, &contentBlock.Input); err != nil { - return err - } - } - } - - // Remove the partial_json - contentBlock.Json = "" - case "message_delta": - // Populate the response - var message struct { - Type string `json:"type"` - Message *respMessage `json:"delta"` - Usage *Usage `json:"usage"` - } - message.Message = response - if err := v.Json(&message); err != nil { - return err - } - - // Increment the token usage - if message.Usage != nil { - response.TokenUsage.InputTokens += message.Usage.InputTokens - response.TokenUsage.OutputTokens += message.Usage.OutputTokens - } - - // Text callback - stop reason - if fn != nil { - if message.Message.StopReason != "" { - fn(schema.MessageChoice{ - FinishReason: message.Message.StopReason, - }) - } - } - case "message_stop": - // Text callback - end of message - if fn != nil { - fn(schema.MessageChoice{}) - } - // End the stream - return io.EOF - default: - return ErrUnexpectedResponse.Withf("%q", v.Event) - } - - // Comntinue processing stream - return nil -} diff --git a/pkg/anthropic/messages_test.go b/pkg/anthropic/messages_test.go deleted file mode 100644 index 015a38a..0000000 --- a/pkg/anthropic/messages_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package anthropic_test - -import ( - "context" - "os" - "reflect" - "testing" - - opts "github.com/mutablelogic/go-client" - anthropic "github.com/mutablelogic/go-client/pkg/anthropic" - schema "github.com/mutablelogic/go-client/pkg/openai/schema" - assert "github.com/stretchr/testify/assert" -) - -func Test_message_001(t *testing.T) { - assert := assert.New(t) - client, err := anthropic.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - msg := schema.NewMessage("user", "What is the weather today") - content, err := client.Messages(context.Background(), []*schema.Message{msg}) - assert.NoError(err) - t.Log(content) -} - -func Test_message_002(t *testing.T) { - assert := assert.New(t) - client, err := anthropic.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - msg := schema.NewMessage("user", "What is the weather today") - content, err := client.Messages(context.Background(), []*schema.Message{msg}, anthropic.OptStream(func(evt schema.MessageChoice) { - t.Log(evt) - })) - assert.NoError(err) - t.Log(content) -} - -func Test_message_003(t *testing.T) { - assert := assert.New(t) - client, err := anthropic.New(GetApiKey(t), opts.OptTrace(os.Stderr, true), opts.OptHeader("Anthropic-Beta", "tools-2024-04-04")) - assert.NoError(err) - assert.NotNil(client) - msg := schema.NewMessage("user", "What is the weather today in Berlin, Germany") - - // Create the weather tool - weather := schema.NewTool("weather", "Get the weather for a location") - assert.NoError(weather.Add("location", "The location to get the weather for", true, reflect.TypeOf(""))) - - // Request -> Response - content, err := client.Messages(context.Background(), []*schema.Message{msg}, anthropic.OptTool(weather)) - assert.NoError(err) - t.Log(content) -} - -func Test_message_004(t *testing.T) { - assert := assert.New(t) - client, err := anthropic.New(GetApiKey(t), opts.OptTrace(os.Stderr, true), opts.OptHeader("Anthropic-Beta", "tools-2024-04-04")) - assert.NoError(err) - assert.NotNil(client) - msg := schema.NewMessage("user", "What is the weather today in Berlin, Germany") - - // Create the weather tool - weather := schema.NewTool("weather", "Get the weather for a location") - assert.NoError(weather.Add("location", "The location to get the weather for", true, reflect.TypeOf(""))) - - // Request -> Response - content, err := client.Messages(context.Background(), []*schema.Message{msg}, anthropic.OptTool(weather), anthropic.OptStream(func(evt schema.MessageChoice) { - t.Log(evt) - })) - assert.NoError(err) - t.Log(content) -} - -func Test_message_005(t *testing.T) { - assert := assert.New(t) - client, err := anthropic.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - msg := schema.NewMessage("user", "Provide me with a caption for this image") - content, err := schema.ImageData("../../etc/test/IMG_20130413_095348.JPG") - if !assert.NoError(err) { - t.SkipNow() - } - msg.Add(content) - - // Request -> Response - response, err := client.Messages(context.Background(), []*schema.Message{msg, schema.NewMessage("assistant", "The caption is:")}) - assert.NoError(err) - t.Log(response) -} - -func Test_message_006(t *testing.T) { - assert := assert.New(t) - client, err := anthropic.New(GetApiKey(t), opts.OptTrace(os.Stderr, true), opts.OptHeader("Anthropic-Beta", "tools-2024-04-04")) - assert.NoError(err) - assert.NotNil(client) - msg := schema.NewMessage("user", "What is Berlin most famous for?") - - // Request -> Response - content, err := client.Messages(context.Background(), []*schema.Message{msg}, anthropic.OptMaxTokens(40), anthropic.OptStream(func(evt schema.MessageChoice) { - t.Log(evt) - })) - assert.NoError(err) - t.Log(content) -} diff --git a/pkg/anthropic/opts.go b/pkg/anthropic/opts.go deleted file mode 100644 index 1d11d52..0000000 --- a/pkg/anthropic/opts.go +++ /dev/null @@ -1,119 +0,0 @@ -package anthropic - -import ( - // Package imports - schema "github.com/mutablelogic/go-client/pkg/openai/schema" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type options struct { - // Common options - Model string `json:"model"` - MaxTokens int `json:"max_tokens,omitempty"` - Temperature float32 `json:"temperature,omitempty"` - Metadata *metadataoptions `json:"metadata,omitempty"` - - // Options for messages - Stop []string `json:"stop_sequences,omitempty"` - Stream bool `json:"stream,omitempty"` - StreamCallback Callback `json:"-"` - System string `json:"system,omitempty"` - Tools []*schema.Tool `json:"tools,omitempty"` -} - -type metadataoptions struct { - User string `json:"user_id,omitempty"` -} - -// Opt is a function which can be used to set options on a request -type Opt func(*options) error - -// Stream response, which is called with each delta in the conversation -// or nil if the conversation is complete -type Callback func(schema.MessageChoice) - -/////////////////////////////////////////////////////////////////////////////// -// OPTIONS - -// Set the model -func OptModel(v string) Opt { - return func(o *options) error { - o.Model = v - return nil - } -} - -// Maximum number of tokens to generate in the reply -func OptMaxTokens(v int) Opt { - return func(o *options) error { - o.MaxTokens = v - return nil - } -} - -// Set streaming response -func OptStream(fn Callback) Opt { - return func(o *options) error { - o.Stream = true - o.StreamCallback = fn - return nil - } -} - -// Set system prompt -func OptSystem(prompt string) Opt { - return func(o *options) error { - o.System = prompt - return nil - } -} - -// An external identifier for the user who is associated with the request. -func OptUser(v string) Opt { - return func(o *options) error { - o.Metadata = &metadataoptions{User: v} - return nil - } -} - -// Custom text sequences that will cause the model to stop generating. -func OptStop(value ...string) Opt { - return func(o *options) error { - o.Stop = value - return nil - } -} - -// Amount of randomness injected into the response. -func OptTemperature(v float32) Opt { - return func(o *options) error { - if v < 0.0 || v > 1.0 { - return ErrBadParameter.With("OptTemperature") - } - o.Temperature = v - return nil - } -} - -// Add a tool -func OptTool(value ...*schema.Tool) Opt { - return func(o *options) error { - // Make a copy of each tool - for _, tool := range value { - if tool == nil { - return ErrBadParameter.With("OptTool") - } else { - tool := *tool - o.Tools = append(o.Tools, &tool) - } - } - - // Return success - return nil - } -} diff --git a/pkg/context/signal.go b/pkg/context/signal.go deleted file mode 100644 index 8868baf..0000000 --- a/pkg/context/signal.go +++ /dev/null @@ -1,33 +0,0 @@ -package context - -import ( - "context" - "os" - "os/signal" -) - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// ContextForSignal returns a context object which is cancelled when a signal -// is received. It returns nil if no signal parameter is provided -func ContextForSignal(signals ...os.Signal) context.Context { - if len(signals) == 0 { - return nil - } - - ch := make(chan os.Signal, 1) - ctx, cancel := context.WithCancel(context.Background()) - - // Send message on channel when signal received - signal.Notify(ch, signals...) - - // When any signal received, call cancel - go func() { - <-ch - cancel() - }() - - // Return success - return ctx -} diff --git a/pkg/elevenlabs/README.md b/pkg/elevenlabs/README.md deleted file mode 100644 index c315370..0000000 --- a/pkg/elevenlabs/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Elevenlabs API Client - -This package provides a client for the Elevenlabs API, which is used to interact with the Elevenlabs Text-to-Speech service. - -References: - -- API https://elevenlabs.io/docs/api-reference/text-to-speech -- Package https://pkg.go.dev/github.com/mutablelogic/go-client/pkg/elevenlabs - diff --git a/pkg/elevenlabs/client.go b/pkg/elevenlabs/client.go deleted file mode 100644 index 03156eb..0000000 --- a/pkg/elevenlabs/client.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -elevenlabs implements an API client for elevenlabs (https://elevenlabs.io/docs/api-reference/text-to-speech) -*/ -package elevenlabs - -import ( - // Packages - "github.com/mutablelogic/go-client" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type Client struct { - *client.Client -} - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -const ( - endPoint = "https://api.elevenlabs.io/v1" -) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { - // Create client - client, err := client.New(append(opts, client.OptEndpoint(endPoint), client.OptHeader("xi-api-key", ApiKey))...) - if err != nil { - return nil, err - } - - // Return the client - return &Client{client}, nil -} diff --git a/pkg/elevenlabs/client_test.go b/pkg/elevenlabs/client_test.go deleted file mode 100644 index 6fb757b..0000000 --- a/pkg/elevenlabs/client_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package elevenlabs_test - -import ( - "os" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - elevenlabs "github.com/mutablelogic/go-client/pkg/elevenlabs" - assert "github.com/stretchr/testify/assert" -) - -func Test_client_001(t *testing.T) { - assert := assert.New(t) - client, err := elevenlabs.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - t.Log(client) -} - -/////////////////////////////////////////////////////////////////////////////// -// ENVIRONMENT - -func GetApiKey(t *testing.T) string { - key := os.Getenv("ELEVENLABS_API_KEY") - if key == "" { - t.Skip("ELEVENLABS_API_KEY not set") - t.SkipNow() - } - return key -} diff --git a/pkg/elevenlabs/model.go b/pkg/elevenlabs/model.go deleted file mode 100644 index 4e184c8..0000000 --- a/pkg/elevenlabs/model.go +++ /dev/null @@ -1,39 +0,0 @@ -package elevenlabs - -import ( - // Packages - client "github.com/mutablelogic/go-client" -) - -/////////////////////////////////////////////////////////////////////////////// -// SCHEMA - -type Model struct { - Id string `json:"model_id" writer:",width:30"` - Name string `json:"name" writer:",width:30,wrap"` - Description string `json:"description,omitempty" writer:",wrap"` - CanBeFineTuned bool `json:"can_be_fine_tuned" writer:",width:5"` - CanDoTextToSpeech bool `json:"can_do_text_to_speech" writer:",width:5"` - CanDoVoiceConversion bool `json:"can_do_voice_conversion" writer:",width:5"` - CanUseStyle bool `json:"can_use_style" writer:",width:5"` - CanUseSpeakerBoost bool `json:"can_use_speaker_boost" writer:",width:5"` - ServesProVoices bool `json:"serves_pro_voices" writer:",width:5"` - TokenCostFactor float32 `json:"token_cost_factor" writer:",width:5,right"` - RequiresAlphaAccess bool `json:"requires_alpha_access,omitempty" writer:",width:5"` - Languages []struct { - Id string `json:"language_id"` - Name string `json:"name"` - } `json:"languages,omitempty" writer:",wrap"` -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// Return models -func (c *Client) Models() ([]Model, error) { - var response []Model - if err := c.Do(nil, &response, client.OptPath("models")); err != nil { - return nil, err - } - return response, nil -} diff --git a/pkg/elevenlabs/opts.go b/pkg/elevenlabs/opts.go deleted file mode 100644 index 457ef2e..0000000 --- a/pkg/elevenlabs/opts.go +++ /dev/null @@ -1,86 +0,0 @@ -package elevenlabs - -import ( - "fmt" - "net/url" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type opts struct { - url.Values - Model string `json:"model_id,omitempty"` - Seed uint `json:"seed,omitempty"` -} - -// Opt is a function which can be used to set options on a request -type Opt func(*opts) error - -/////////////////////////////////////////////////////////////////////////////// -// OPTIONS - -// Set the voice model -func OptModel(v string) Opt { - return func(o *opts) error { - o.Model = v - return nil - } -} - -// Set the deterministic seed -func OptSeed(v uint) Opt { - return func(o *opts) error { - o.Seed = v - return nil - } -} - -// Set the output format -func OptFormat(v string) Opt { - return func(o *opts) error { - if o.Values == nil { - o.Values = make(url.Values) - } - if v == "" { - o.Del("output_format") - } else { - o.Set("output_format", v) - } - return nil - } -} - -// Set the output format to MP3 given bitrate and samplerate -func OptFormatMP3(bitrate, samplerate uint) Opt { - return func(o *opts) error { - switch samplerate { - case 22050, 44100: - return OptFormat(fmt.Sprintf("mp3_%v_%v", samplerate, bitrate))(o) - default: - return ErrBadParameter.With("OptFormatMP3: invalid sample rate: ", samplerate) - } - } -} - -// Set the output format to PCM -func OptFormatPCM(samplerate uint) Opt { - return func(o *opts) error { - switch samplerate { - case 16000, 22050, 24000, 44100: - return OptFormat(fmt.Sprintf("pcm_%v", samplerate))(o) - default: - return ErrBadParameter.With("OptFormatPCM: invalid sample rate: ", samplerate) - } - } -} - -// Set the output format to ULAW -func OptFormatULAW() Opt { - return func(o *opts) error { - return OptFormat("ulaw_8000")(o) - } -} diff --git a/pkg/elevenlabs/opts.go_old b/pkg/elevenlabs/opts.go_old deleted file mode 100644 index 4e0f648..0000000 --- a/pkg/elevenlabs/opts.go_old +++ /dev/null @@ -1,62 +0,0 @@ -package elevenlabs - -import ( - "fmt" - - "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type TextToSpeechOpt func(*textToSpeechRequest) error -type TextToSpeechFormat string - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -const ( - MP3_44100_64 TextToSpeechFormat = "mp3_44100_64" // mp3 with 44.1kHz sample rate at 64kbps - MP3_44100_96 TextToSpeechFormat = "mp3_44100_96" // mp3 with 44.1kHz sample rate at 96kbps - MP3_44100_128 TextToSpeechFormat = "mp3_44100_128" // default output format, mp3 with 44.1kHz sample rate at 128kbps - MP3_44100_192 TextToSpeechFormat = "mp3_44100_192" // mp3 with 44.1kHz sample rate at 192kbps - PCM_16000 TextToSpeechFormat = "pcm_16000" // PCM format (S16LE) with 16kHz sample rate - PCM_22050 TextToSpeechFormat = "pcm_22050" // PCM format (S16LE) with 22.05kHz sample rate - PCM_24000 TextToSpeechFormat = "pcm_24000" // PCM format (S16LE) with 24kHz sample rate - PCM_44100 TextToSpeechFormat = "pcm_44100" // PCM format (S16LE) with 44.1kHz sample rate - ULAW_8000 TextToSpeechFormat = "ulaw_8000" // μ-law format (sometimes written mu-law, often approximated as u-law) with 8kHz sample rate -) - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -func OptOutput(format TextToSpeechFormat) TextToSpeechOpt { - return func(req *textToSpeechRequest) error { - req.Query.Set("output_format", string(format)) - return nil - } -} - -func OptOptimizeStreamingLatency(value uint8) TextToSpeechOpt { - return func(req *textToSpeechRequest) error { - req.Query.Set("optimize_streaming_latency", fmt.Sprint(value)) - return nil - } -} - -func OptModel(id string) TextToSpeechOpt { - return func(req *textToSpeechRequest) error { - if id == "" { - return errors.ErrBadParameter.With("OptModel: id") - } - req.ModelId = id - return nil - } -} - -func OptVoiceSettings(settings VoiceSettings) TextToSpeechOpt { - return func(req *textToSpeechRequest) error { - req.Settings = settings - return nil - } -} diff --git a/pkg/elevenlabs/text_to_speech.go b/pkg/elevenlabs/text_to_speech.go deleted file mode 100644 index a5828f4..0000000 --- a/pkg/elevenlabs/text_to_speech.go +++ /dev/null @@ -1,72 +0,0 @@ -package elevenlabs - -import ( - "io" - - // Packages - "github.com/mutablelogic/go-client" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type reqTextToSpeech struct { - opts - Text string `json:"text"` -} - -type respBinary struct { - mimetype string - bytes int64 - w io.Writer -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// Converts text into speech, returning the number of bytes written to the writer -func (c *Client) TextToSpeech(w io.Writer, voice, text string, opts ...Opt) (int64, error) { - var request reqTextToSpeech - var response respBinary - - // Create the request and response - request.Text = text - response.w = w - - // Set opts - for _, opt := range opts { - if err := opt(&request.opts); err != nil { - return 0, err - } - } - - // Make a response object, write the data - if payload, err := client.NewJSONRequest(request); err != nil { - return 0, err - } else if err := c.Do(payload, &response, client.OptQuery(request.opts.Values), client.OptPath("text-to-speech", voice)); err != nil { - return 0, err - } - - // Return success - return response.bytes, nil -} - -/////////////////////////////////////////////////////////////////////////////// -// UNMARSHAL METHODS - -func (resp *respBinary) Unmarshal(mimetype string, r io.Reader) error { - // Set mimetype - resp.mimetype = mimetype - - // Copy the data - if resp.w != nil { - if n, err := io.Copy(resp.w, r); err != nil { - return err - } else { - resp.bytes = n - } - } - - // Return success - return nil -} diff --git a/pkg/elevenlabs/voice.go b/pkg/elevenlabs/voice.go deleted file mode 100644 index 8a4119b..0000000 --- a/pkg/elevenlabs/voice.go +++ /dev/null @@ -1,160 +0,0 @@ -package elevenlabs - -import ( - // Packages - - "github.com/djthorpe/go-errors" - "github.com/mutablelogic/go-client" -) - -/////////////////////////////////////////////////////////////////////////////// -// SCHEMA - -type Voice struct { - Id string `json:"voice_id" writer:",width:20"` - Name string `json:"name"` - Description string `json:"description,omitempty" writer:",wrap"` - PreviewUrl string `json:"preview_url,omitempty" writer:",width:40,wrap"` - Category string `json:"category,omitempty" writer:",width:10"` - Samples []struct { - Id string `json:"sample_id"` - Filename string `json:"file_name"` - MimeType string `json:"mime_type"` - Size int64 `json:"size_bytes"` - Hash string `json:"hash"` - } `json:"samples,omitempty" writer:"samples,wrap"` - Settings VoiceSettings `json:"settings" writer:"settings,wrap,width:20"` -} - -type VoiceSettings struct { - SimilarityBoost float32 `json:"similarity_boost"` - Stability float32 `json:"stability"` - Style float32 `json:"style,omitempty"` - UseSpeakerBoost bool `json:"use_speaker_boost"` -} - -/////////////////////////////////////////////////////////////////////////////// -// PAYLOADS - -type voiceDeleteRequest struct { - client.Payload `json:"-"` -} - -type voiceAddRequest struct { - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` -} - -type voicesResponse struct { - Voices []Voice `json:"voices"` -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// Return current set of voices -func (c *Client) Voices() ([]Voice, error) { - var request client.Payload - var response voicesResponse - if err := c.Do(request, &response, client.OptPath("voices")); err != nil { - return nil, err - } - return response.Voices, nil -} - -// Return a single voice -func (c *Client) Voice(Id string) (Voice, error) { - var request client.Payload - var response Voice - if Id == "" { - return response, errors.ErrBadParameter.With("Id") - } - if err := c.Do(request, &response, client.OptPath("voices", Id)); err != nil { - return response, err - } - return response, nil -} - -// Return voice settings. If Id is empty, then return the default voice settings -func (c *Client) VoiceSettings(Id string) (VoiceSettings, error) { - var request client.Payload - var response VoiceSettings - var path client.RequestOpt - if Id == "" { - path = client.OptPath("voices", "settings", "default") - } else { - path = client.OptPath("voices", Id, "settings") - } - if err := c.Do(request, &response, path); err != nil { - return response, err - } - return response, nil -} - -// Set voice settings for a voice -func (c *Client) SetVoiceSettings(Id string, v VoiceSettings) error { - request, err := client.NewJSONRequest(v) - if err != nil { - return err - } - return c.Do(request, nil, client.OptPath("voices", Id, "settings", "edit")) -} - -/* - -// Delete a voice -func (c *Client) DeleteVoice(Id string) error { - var request voiceDeleteRequest - if Id == "" { - return errors.ErrBadParameter.With("Id") - } - if err := c.Do(request, nil, client.OptPath("voices", Id)); err != nil { - return err - } - return nil -} - - -/////////////////////////////////////////////////////////////////////////////// -// PAYLOAD METHODS - -func (voiceDeleteRequest) Method() string { - return http.MethodDelete -} - -func (voiceDeleteRequest) Type() string { - return "" -} - -func (voiceDeleteRequest) Accept() string { - return client.ContentTypeJson -} - -func (voiceAddRequest) Method() string { - return http.MethodPost -} - -func (voiceAddRequest) Type() string { - return client.ContentTypeBinary -} - -func (voiceAddRequest) Accept() string { - return client.ContentTypeForm -} - -/////////////////////////////////////////////////////////////////////////////// -// MARSHAL - -func (v VoiceSettings) Marshal() ([]byte, error) { - data := new(bytes.Buffer) - data.Write([]byte(fmt.Sprintf("similarity_boost=%v\n", v.SimilarityBoost))) - data.Write([]byte(fmt.Sprintf("stability=%v\n", v.Stability))) - if v.Style != 0 { - data.Write([]byte(fmt.Sprintf("style=%v\n", v.Style))) - } - if v.UseSpeakerBoost { - data.Write([]byte(fmt.Sprintf("use_speaker_boost=%v\n", v.UseSpeakerBoost))) - } - return data.Bytes(), nil -} -*/ diff --git a/pkg/elevenlabs/voice_test.go b/pkg/elevenlabs/voice_test.go deleted file mode 100644 index 8de884d..0000000 --- a/pkg/elevenlabs/voice_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package elevenlabs_test - -import ( - "encoding/json" - "os" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - elevenlabs "github.com/mutablelogic/go-client/pkg/elevenlabs" - assert "github.com/stretchr/testify/assert" -) - -func Test_voice_001(t *testing.T) { - assert := assert.New(t) - client, err := elevenlabs.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - response, err := client.Voices() - assert.NoError(err) - assert.NotEmpty(response) - data, err := json.MarshalIndent(response, "", " ") - assert.NoError(err) - t.Log(string(data)) -} - -func Test_voice_002(t *testing.T) { - assert := assert.New(t) - client, err := elevenlabs.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - response, err := client.Voices() - assert.NoError(err) - assert.NotEmpty(response) - - for _, voice := range response { - voice, err := client.Voice(voice.Id) - assert.NoError(err) - assert.NotEmpty(voice) - data, err := json.MarshalIndent(voice, "", " ") - assert.NoError(err) - t.Log(string(data)) - } - -} - -/* -func Test_voice_003(t *testing.T) { - assert := assert.New(t) - client, err := elevenlabs.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - response, err := client.VoiceSettings("") - assert.NoError(err) - assert.NotEmpty(response) - data, err := json.MarshalIndent(response, "", " ") - assert.NoError(err) - t.Log(string(data)) -} - -func Test_voice_004(t *testing.T) { - assert := assert.New(t) - client, err := elevenlabs.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - response, err := client.Voices() - assert.NoError(err) - assert.NotEmpty(response) - - for _, voice := range response { - settings, err := client.VoiceSettings(voice.Id) - assert.NoError(err) - assert.NotEmpty(settings) - data, err := json.MarshalIndent(settings, "", " ") - assert.NoError(err) - t.Log(voice.Name, "=>", string(data)) - } - -} - -func Test_voice_005(t *testing.T) { - assert := assert.New(t) - client, err := elevenlabs.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - response, err := client.Voices() - assert.NoError(err) - assert.NotEmpty(response) - - err = client.DeleteVoice("test") - assert.NotNil(err) -} -*/ diff --git a/pkg/homeassistant/agent.go b/pkg/homeassistant/agent.go deleted file mode 100644 index bb1825d..0000000 --- a/pkg/homeassistant/agent.go +++ /dev/null @@ -1,179 +0,0 @@ -package homeassistant - -import ( - "context" - "errors" - "slices" - "strings" - - // Packages - agent "github.com/mutablelogic/go-client/pkg/agent" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type tool struct { - name string - description string - params []agent.ToolParameter - run func(context.Context, *agent.ToolCall) (*agent.ToolResult, error) -} - -// Ensure tool satisfies the agent.Tool interface -var _ agent.Tool = (*tool)(nil) - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// Return all the agent tools for the weatherapi -func (c *Client) Tools() []agent.Tool { - return []agent.Tool{ - &tool{ - name: "devices", - description: "Lookup all device id's in the home, or search for a device ny name", - run: c.agentGetDeviceIds, - params: []agent.ToolParameter{ - { - Name: "name", - Description: "Name to filter devices", - }, - }, - }, &tool{ - name: "get_device_state", - description: "Return the current state of a device, given the device id", - run: c.agentGetDeviceState, - params: []agent.ToolParameter{ - { - Name: "device", - Description: "The device id", - Required: true, - }, - }, - }, - } -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - TOOL - -func (*tool) Provider() string { - return "homeassistant" -} - -func (t *tool) Name() string { - return t.name -} - -func (t *tool) Description() string { - return t.description -} - -func (t *tool) Params() []agent.ToolParameter { - return t.params -} - -func (t *tool) Run(ctx context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - return t.run(ctx, call) -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - TOOL - -var ( - allowedClasses = []string{ - "temperature", - "humidity", - "battery", - "select", - "number", - "switch", - "enum", - "light", - "sensor", - "binary_sensor", - "remote", - "climate", - "occupancy", - "motion", - "button", - "door", - "lock", - "tv", - "vacuum", - } -) - -// Return the current devices and their id's -func (c *Client) agentGetDeviceIds(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - name, err := call.String("name") - if errors.Is(err, ErrNotFound) { - name = "" - } else if err != nil { - return nil, err - } - - // Query all devices - devices, err := c.States() - if err != nil { - return nil, err - } - - // Make the device id's - type DeviceId struct { - Id string `json:"id"` - Name string `json:"name"` - } - var result []DeviceId - for _, device := range devices { - if !slices.Contains(allowedClasses, device.Class()) { - continue - } - var found bool - if name != "" { - if strings.Contains(strings.ToLower(device.Name()), strings.ToLower(name)) { - found = true - } else if strings.Contains(strings.ToLower(device.Class()), strings.ToLower(name)) { - found = true - } - if !found { - continue - } - } - result = append(result, DeviceId{ - Id: device.Entity, - Name: device.Name(), - }) - } - return &agent.ToolResult{ - Id: call.Id, - Result: map[string]any{ - "type": "text", - "devices": result, - }, - }, nil -} - -// Return a device state -func (c *Client) agentGetDeviceState(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - device, err := call.String("device") - if err != nil { - return nil, err - } - - state, err := c.State(device) - if err != nil { - return nil, err - } - - return &agent.ToolResult{ - Id: call.Id, - Result: map[string]any{ - "type": "text", - "device": state, - }, - }, nil -} diff --git a/pkg/homeassistant/services.go b/pkg/homeassistant/services.go index 0076ecd..eb22cc4 100644 --- a/pkg/homeassistant/services.go +++ b/pkg/homeassistant/services.go @@ -34,11 +34,11 @@ type Field struct { } type Selector struct { - Text string `json:"text,omitempty"` - Mode string `json:"mode,omitempty"` - Min int `json:"min,omitempty"` - Max int `json:"max,omitempty"` - UnitOfMeasurement string `json:"unit_of_measurement,omitempty"` + Text string `json:"text,omitempty"` + Mode string `json:"mode,omitempty"` + Min float32 `json:"min,omitempty"` + Max float32 `json:"max,omitempty"` + UnitOfMeasurement string `json:"unit_of_measurement,omitempty"` } type reqCall struct { diff --git a/pkg/ipify/agent.go b/pkg/ipify/agent.go deleted file mode 100644 index 56c91a9..0000000 --- a/pkg/ipify/agent.go +++ /dev/null @@ -1,76 +0,0 @@ -package ipify - -import ( - "context" - - // Packages - agent "github.com/mutablelogic/go-client/pkg/agent" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type tool struct { - name string - description string - params []agent.ToolParameter - run func(context.Context, *agent.ToolCall) (*agent.ToolResult, error) -} - -// Ensure tool satisfies the agent.Tool interface -var _ agent.Tool = (*tool)(nil) - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// Return all the agent tools for the weatherapi -func (c *Client) Tools() []agent.Tool { - return []agent.Tool{ - &tool{ - name: "get_ip_address", - description: "Return your IP address", - run: c.agentGetAddress, - }, - } -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - TOOL - -func (*tool) Provider() string { - return "ipify" -} - -func (t *tool) Name() string { - return t.name -} - -func (t *tool) Description() string { - return t.description -} - -func (t *tool) Params() []agent.ToolParameter { - return t.params -} - -func (t *tool) Run(ctx context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - return t.run(ctx, call) -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - TOOL - -// Return the current general headlines -func (c *Client) agentGetAddress(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - response, err := c.Get() - if err != nil { - return nil, err - } - return &agent.ToolResult{ - Id: call.Id, - Result: map[string]any{ - "type": "text", - "ip_address": response, - }, - }, nil -} diff --git a/pkg/mistral/README.md b/pkg/mistral/README.md deleted file mode 100644 index fc64c94..0000000 --- a/pkg/mistral/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Mistral API Client - -This package provides a client for Mistral API, which is used to interact with the Mistral LLM models. - -References: - -- API https://docs.mistral.ai/api/ -- Package https://pkg.go.dev/github.com/mutablelogic/go-client/pkg/mistral diff --git a/pkg/mistral/chat.go b/pkg/mistral/chat.go deleted file mode 100644 index 85c96bf..0000000 --- a/pkg/mistral/chat.go +++ /dev/null @@ -1,202 +0,0 @@ -package mistral - -import ( - "context" - "encoding/json" - "io" - "reflect" - - // Packages - client "github.com/mutablelogic/go-client" - schema "github.com/mutablelogic/go-client/pkg/openai/schema" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -// A request for a chat completion -type reqChat struct { - options - Tools []reqChatTools `json:"tools,omitempty"` - Messages []*schema.Message `json:"messages,omitempty"` -} - -type reqChatTools struct { - Type string `json:"type"` - Function *schema.Tool `json:"function"` -} - -// A chat completion object -type respChat struct { - Id string `json:"id"` - Created int64 `json:"created"` - Model string `json:"model"` - Choices []*schema.MessageChoice `json:"choices,omitempty"` - TokenUsage schema.TokenUsage `json:"usage,omitempty"` -} - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -const ( - defaultChatCompletionModel = "mistral-small-latest" - endOfStreamToken = "[DONE]" -) - -/////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (v respChat) String() string { - if data, err := json.MarshalIndent(v, "", " "); err != nil { - return err.Error() - } else { - return string(data) - } -} - -/////////////////////////////////////////////////////////////////////////////// -// API CALLS - -// Chat creates a model response for the given chat conversation. -func (c *Client) Chat(ctx context.Context, messages []*schema.Message, opts ...Opt) ([]*schema.Content, error) { - var request reqChat - var response respChat - - // Process options - request.Model = defaultChatCompletionModel - request.Messages = messages - for _, opt := range opts { - if err := opt(&request.options); err != nil { - return nil, err - } - } - - // Append tools - for _, tool := range request.options.Tools { - request.Tools = append(request.Tools, reqChatTools{ - Type: "function", - Function: tool, - }) - } - - // Set up the request - reqopts := []client.RequestOpt{ - client.OptPath("chat/completions"), - } - if request.Stream { - reqopts = append(reqopts, client.OptTextStreamCallback(func(event client.TextStreamEvent) error { - return response.streamCallback(event, request.StreamCallback) - })) - } - - // Request->Response - if payload, err := client.NewJSONRequest(request); err != nil { - return nil, err - } else if err := c.DoWithContext(ctx, payload, &response, reqopts...); err != nil { - return nil, err - } else if len(response.Choices) == 0 { - return nil, ErrUnexpectedResponse.With("no choices returned") - } - - // Return all choices - var result []*schema.Content - for _, choice := range response.Choices { - if choice.Message == nil || choice.Message.Content == nil { - continue - } - for _, tool := range choice.Message.ToolCalls { - result = append(result, schema.ToolUse(tool)) - } - switch v := choice.Message.Content.(type) { - case []string: - for _, v := range v { - result = append(result, schema.Text(v)) - } - case string: - result = append(result, schema.Text(v)) - default: - return nil, ErrUnexpectedResponse.With("unexpected content type ", reflect.TypeOf(choice.Message.Content)) - } - } - - // Return success - return result, nil -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func (response *respChat) streamCallback(v client.TextStreamEvent, fn Callback) error { - var delta schema.MessageChunk - - // [DONE] indicates the end of the stream, return io.EOF - // or decode the data into a MessageChunk - if v.Data == endOfStreamToken { - return io.EOF - } else if err := v.Json(&delta); err != nil { - return err - } - - // Set the response fields - if delta.Id != "" { - response.Id = delta.Id - } - if delta.Model != "" { - response.Model = delta.Model - } - if delta.Created != 0 { - response.Created = delta.Created - } - if delta.TokenUsage != nil { - response.TokenUsage = *delta.TokenUsage - } - - // With no choices, return success - if len(delta.Choices) == 0 { - return nil - } - - // Append choices - for _, choice := range delta.Choices { - // Sanity check the choice index - if choice.Index < 0 || choice.Index >= 6 { - continue - } - // Ensure message has the choice - for { - if choice.Index < len(response.Choices) { - break - } - response.Choices = append(response.Choices, new(schema.MessageChoice)) - } - // Append the choice data onto the messahe - if response.Choices[choice.Index].Message == nil { - response.Choices[choice.Index].Message = new(schema.Message) - } - if choice.Index != 0 { - response.Choices[choice.Index].Index = choice.Index - } - if choice.FinishReason != "" { - response.Choices[choice.Index].FinishReason = choice.FinishReason - } - if choice.Delta != nil { - if choice.Delta.Role != "" { - response.Choices[choice.Index].Message.Role = choice.Delta.Role - } - if choice.Delta.Content != "" { - response.Choices[choice.Index].Message.Add(choice.Delta.Content) - } - } - - // Callback to the client - if fn != nil { - fn(choice) - } - } - - // Return success - return nil -} diff --git a/pkg/mistral/chat_test.go b/pkg/mistral/chat_test.go deleted file mode 100644 index 612d410..0000000 --- a/pkg/mistral/chat_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package mistral_test - -import ( - "context" - "os" - "reflect" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - mistral "github.com/mutablelogic/go-client/pkg/mistral" - schema "github.com/mutablelogic/go-client/pkg/openai/schema" - assert "github.com/stretchr/testify/assert" -) - -func Test_chat_001(t *testing.T) { - assert := assert.New(t) - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - _, err = client.Chat(context.Background(), []*schema.Message{ - {Role: "user", Content: "What is the weather"}, - }) - assert.NoError(err) -} - -func Test_chat_002(t *testing.T) { - assert := assert.New(t) - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - _, err = client.Chat(context.Background(), []*schema.Message{ - {Role: "user", Content: "What is the weather"}, - }, mistral.OptStream(func(message schema.MessageChoice) { - t.Log(message) - })) - assert.NoError(err) -} - -func Test_chat_003(t *testing.T) { - assert := assert.New(t) - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - - tool := schema.NewTool("weather", "get weather in a specific city") - tool.Add("city", "name of the city, if known", false, reflect.TypeOf("")) - - _, err = client.Chat(context.Background(), []*schema.Message{ - {Role: "user", Content: "What is the weather in Berlin"}, - }, mistral.OptTool(tool)) - assert.NoError(err) -} diff --git a/pkg/mistral/client.go b/pkg/mistral/client.go deleted file mode 100644 index 9dacda1..0000000 --- a/pkg/mistral/client.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -mistral implements an API client for mistral (https://docs.mistral.ai/api/) -*/ -package mistral - -import ( - // Packages - "github.com/mutablelogic/go-client" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type Client struct { - *client.Client -} - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -const ( - endPoint = "https://api.mistral.ai/v1" -) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -// Create a new client -func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { - // Create client - opts = append(opts, client.OptEndpoint(endPoint)) - opts = append(opts, client.OptReqToken(client.Token{ - Scheme: client.Bearer, - Value: ApiKey, - })) - client, err := client.New(opts...) - if err != nil { - return nil, err - } - - // Return the client - return &Client{client}, nil -} diff --git a/pkg/mistral/client_test.go b/pkg/mistral/client_test.go deleted file mode 100644 index 89c9392..0000000 --- a/pkg/mistral/client_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package mistral_test - -import ( - "os" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - mistral "github.com/mutablelogic/go-client/pkg/mistral" - assert "github.com/stretchr/testify/assert" -) - -func Test_client_001(t *testing.T) { - assert := assert.New(t) - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - t.Log(client) -} - -/////////////////////////////////////////////////////////////////////////////// -// ENVIRONMENT - -func GetApiKey(t *testing.T) string { - key := os.Getenv("MISTRAL_API_KEY") - if key == "" { - t.Skip("MISTRAL_API_KEY not set") - t.SkipNow() - } - return key -} diff --git a/pkg/mistral/embedding.go b/pkg/mistral/embedding.go deleted file mode 100644 index 2118621..0000000 --- a/pkg/mistral/embedding.go +++ /dev/null @@ -1,63 +0,0 @@ -package mistral - -import ( - // Packages - client "github.com/mutablelogic/go-client" - schema "github.com/mutablelogic/go-client/pkg/openai/schema" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -// A request to create embeddings -type reqCreateEmbedding struct { - Input []string `json:"input"` - options -} - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -const ( - defaultEmbeddingModel = "mistral-embed" -) - -/////////////////////////////////////////////////////////////////////////////// -// API CALLS - -// CreateEmbedding creates an embedding from a string or array of strings -func (c *Client) CreateEmbedding(content any, opts ...Opt) (schema.Embeddings, error) { - var request reqCreateEmbedding - var response schema.Embeddings - - // Set options - request.Model = defaultEmbeddingModel - for _, opt := range opts { - if err := opt(&request.options); err != nil { - return response, err - } - } - - // Set the input, which is either a string or array of strings - switch v := content.(type) { - case string: - request.Input = []string{v} - case []string: - request.Input = v - default: - return response, ErrBadParameter.With("CreateEmbedding") - } - - // Request->Response - if payload, err := client.NewJSONRequest(request); err != nil { - return response, err - } else if err := c.Do(payload, &response, client.OptPath("embeddings")); err != nil { - return response, err - } - - // Return success - return response, nil -} diff --git a/pkg/mistral/embedding_test.go b/pkg/mistral/embedding_test.go deleted file mode 100644 index 41b67ed..0000000 --- a/pkg/mistral/embedding_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package mistral_test - -import ( - "encoding/json" - "os" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - mistral "github.com/mutablelogic/go-client/pkg/mistral" - assert "github.com/stretchr/testify/assert" -) - -func Test_embedding_001(t *testing.T) { - assert := assert.New(t) - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - response, err := client.CreateEmbedding("test") - assert.NoError(err) - data, _ := json.MarshalIndent(response, "", " ") - t.Log(string(data)) -} diff --git a/pkg/mistral/model.go b/pkg/mistral/model.go deleted file mode 100644 index ce3d04f..0000000 --- a/pkg/mistral/model.go +++ /dev/null @@ -1,33 +0,0 @@ -package mistral - -import ( - // Packages - "github.com/mutablelogic/go-client" - - // Namespace imports - . "github.com/mutablelogic/go-client/pkg/openai/schema" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type responseListModels struct { - Data []Model `json:"data"` -} - -/////////////////////////////////////////////////////////////////////////////// -// API CALLS - -// ListModels returns all the models -func (c *Client) ListModels() ([]Model, error) { - var response responseListModels - - // Request the models, populate the response - payload := client.NewRequest() - if err := c.Do(payload, &response, client.OptPath("models")); err != nil { - return nil, err - } - - // Return success - return response.Data, nil -} diff --git a/pkg/mistral/model_test.go b/pkg/mistral/model_test.go deleted file mode 100644 index b2481bf..0000000 --- a/pkg/mistral/model_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package mistral_test - -import ( - "encoding/json" - "os" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - mistral "github.com/mutablelogic/go-client/pkg/mistral" - assert "github.com/stretchr/testify/assert" -) - -func Test_models_001(t *testing.T) { - assert := assert.New(t) - client, err := mistral.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - response, err := client.ListModels() - assert.NoError(err) - assert.NotEmpty(response) - data, err := json.MarshalIndent(response, "", " ") - assert.NoError(err) - t.Log(string(data)) -} diff --git a/pkg/mistral/opts.go b/pkg/mistral/opts.go deleted file mode 100644 index ccb9c65..0000000 --- a/pkg/mistral/opts.go +++ /dev/null @@ -1,114 +0,0 @@ -package mistral - -import ( - // Packages - schema "github.com/mutablelogic/go-client/pkg/openai/schema" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type options struct { - // Common options - Model string `json:"model,omitempty"` - EncodingFormat string `json:"encoding_format,omitempty"` - Temperature *float32 `json:"temperature,omitempty"` - MaxTokens int `json:"max_tokens,omitempty"` - SafePrompt bool `json:"safe_prompt,omitempty"` - Seed int `json:"random_seed,omitempty"` - - // Options for chat - Stream bool `json:"stream,omitempty"` - StreamCallback Callback `json:"-"` - Tools []*schema.Tool `json:"-"` -} - -// Opt is a function which can be used to set options on a request -type Opt func(*options) error - -// Callback when new stream data is received -type Callback func(schema.MessageChoice) - -/////////////////////////////////////////////////////////////////////////////// -// OPTIONS - -// Set the model -func OptModel(v string) Opt { - return func(o *options) error { - o.Model = v - return nil - } -} - -// Set the embedding encoding format -func OptEncodingFormat(v string) Opt { - return func(o *options) error { - o.EncodingFormat = v - return nil - } -} - -// Set the maximum number of tokens -func OptMaxTokens(v int) Opt { - return func(o *options) error { - o.MaxTokens = v - return nil - } -} - -// Set streaming response -func OptStream(fn Callback) Opt { - return func(o *options) error { - o.Stream = true - o.StreamCallback = fn - return nil - } -} - -// Inject a safety prompt before all conversations. -func OptSafePrompt() Opt { - return func(o *options) error { - o.SafePrompt = true - return nil - } -} - -// The seed to use for random sampling. If set, different calls will generate deterministic results. -func OptSeed(v int) Opt { - return func(o *options) error { - o.Seed = v - return nil - } -} - -// Amount of randomness injected into the response. -func OptTemperature(v float32) Opt { - return func(o *options) error { - if v < 0.0 || v > 1.0 { - return ErrBadParameter.With("OptTemperature") - } - o.Temperature = &v - return nil - } -} - -// A list of tools the model may call. -func OptTool(value ...*schema.Tool) Opt { - return func(o *options) error { - // Check tools - for _, tool := range value { - if tool == nil { - return ErrBadParameter.With("OptTool") - } - } - - // Append tools - o.Tools = append(o.Tools, value...) - - // Return success - return nil - } -} diff --git a/pkg/multipart/multipart.go b/pkg/multipart/multipart.go index 55d064d..2369e35 100644 --- a/pkg/multipart/multipart.go +++ b/pkg/multipart/multipart.go @@ -129,12 +129,18 @@ func (enc *Encoder) Encode(v any) error { } // Write field - value := rv.FieldByIndex(field.Index).Interface() - if field.Type == fileType { - if _, err := enc.writeFileField(name, value.(File)); err != nil { + value := rv.FieldByIndex(field.Index) + if value.Kind() == reflect.Ptr { + if value.IsNil() { + continue + } + value = value.Elem() + } + if value.Type() == fileType { + if _, err := enc.writeFileField(name, value.Interface().(File)); err != nil { result = errors.Join(result, err) } - } else if err := enc.writeField(name, value); err != nil { + } else if err := enc.writeField(name, value.Interface()); err != nil { result = errors.Join(result, err) } } diff --git a/pkg/newsapi/agent.go b/pkg/newsapi/agent.go deleted file mode 100644 index 0819299..0000000 --- a/pkg/newsapi/agent.go +++ /dev/null @@ -1,172 +0,0 @@ -package newsapi - -import ( - "context" - "strings" - - // Packages - agent "github.com/mutablelogic/go-client/pkg/agent" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type tool struct { - name string - description string - params []agent.ToolParameter - run func(context.Context, *agent.ToolCall) (*agent.ToolResult, error) -} - -// Ensure tool satisfies the agent.Tool interface -var _ agent.Tool = (*tool)(nil) - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// Return all the agent tools for the weatherapi -func (c *Client) Tools() []agent.Tool { - return []agent.Tool{ - &tool{ - name: "current_headlines", - description: "Return the current news headlines", - run: c.agentCurrentHeadlines, - }, &tool{ - name: "current_headlines_country", - description: "Return the current news headlines for a country", - run: c.agentCountryHeadlines, - params: []agent.ToolParameter{ - { - Name: "countrycode", - Description: "The two-letter country code to return headlines for", - Required: true, - }, - }, - }, &tool{ - name: "current_headlines_category", - description: "Return the current news headlines for a business, entertainment, health, science, sports or technology", - run: c.agentCategoryHeadlines, - params: []agent.ToolParameter{ - { - Name: "category", - Description: "business, entertainment, health, science, sports, technology", - Required: true, - }, - }, - }, &tool{ - name: "search_news", - description: "Return the news headlines with a search query", - run: c.agentSearchNews, - params: []agent.ToolParameter{ - { - Name: "query", - Description: "A phrase used to search for news headlines", - Required: true, - }, - }, - }, - } -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - TOOL - -func (*tool) Provider() string { - return "newsapi" -} - -func (t *tool) Name() string { - return t.name -} - -func (t *tool) Description() string { - return t.description -} - -func (t *tool) Params() []agent.ToolParameter { - return t.params -} - -func (t *tool) Run(ctx context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - return t.run(ctx, call) -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - TOOL - -// Return the current general headlines -func (c *Client) agentCurrentHeadlines(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - response, err := c.Headlines(OptCategory("general"), OptLimit(5)) - if err != nil { - return nil, err - } - return &agent.ToolResult{ - Id: call.Id, - Result: map[string]any{ - "type": "text", - "headlines": response, - }, - }, nil -} - -// Return the headlines for a specific country -func (c *Client) agentCountryHeadlines(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - country, err := call.String("countrycode") - if err != nil { - return nil, err - } - country = strings.ToLower(country) - response, err := c.Headlines(OptCountry(country), OptLimit(5)) - if err != nil { - return nil, err - } - return &agent.ToolResult{ - Id: call.Id, - Result: map[string]any{ - "type": "text", - "country": country, - "headlines": response, - }, - }, nil -} - -// Return the headlines for a specific category -func (c *Client) agentCategoryHeadlines(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - category, err := call.String("category") - if err != nil { - return nil, err - } - category = strings.ToLower(category) - response, err := c.Headlines(OptCategory(category), OptLimit(5)) - if err != nil { - return nil, err - } - return &agent.ToolResult{ - Id: call.Id, - Result: map[string]any{ - "type": "text", - "category": category, - "headlines": response, - }, - }, nil -} - -// Return the headlines for a specific query -func (c *Client) agentSearchNews(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - query, err := call.String("query") - if err != nil { - return nil, err - } - response, err := c.Articles(OptQuery(query), OptLimit(5)) - if err != nil { - return nil, err - } - return &agent.ToolResult{ - Id: call.Id, - Result: map[string]any{ - "type": "text", - "query": query, - "headlines": response, - }, - }, nil -} diff --git a/pkg/ollama/agent.go b/pkg/ollama/agent.go deleted file mode 100644 index a794d58..0000000 --- a/pkg/ollama/agent.go +++ /dev/null @@ -1,128 +0,0 @@ -package ollama - -import ( - "context" - "fmt" - "time" - - // Packages - "github.com/mutablelogic/go-client/pkg/agent" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type model struct { - *Model -} - -type userPrompt string - -// Ensure Ollama client satisfies the agent.Agent interface -var _ agent.Agent = (*Client)(nil) - -// Ensure model satisfies the agent.Model interface -var _ agent.Model = (*model)(nil) - -// Ensure userPrompt satisfies the agent.Context interface -var _ agent.Context = userPrompt("") - -///////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// Return the agent name -func (*Client) Name() string { - return "ollama" -} - -// Return the model name -func (m *model) Name() string { - return m.Model.Name -} - -// Return all the models and their capabilities -func (o *Client) Models(context.Context) ([]agent.Model, error) { - models, err := o.ListModels() - if err != nil { - return nil, err - } - - // Append models - result := make([]agent.Model, len(models)) - for i, m := range models { - result[i] = &model{Model: &m} - } - - // Return success - return result, nil -} - -// Return the role -func (userPrompt) Role() string { - return "user" -} - -// Create a user prompt -func (o *Client) UserPrompt(v string) agent.Context { - return userPrompt(v) -} - -// Generate a response from a text message -func (o *Client) Generate(ctx context.Context, model agent.Model, context []agent.Context, opts ...agent.Opt) (*agent.Response, error) { - // Get options - chatopts, err := newOpts(opts...) - if err != nil { - return nil, err - } - - if len(context) != 1 { - return nil, fmt.Errorf("context must contain exactly one element") - } - - prompt, ok := context[0].(userPrompt) - if !ok { - return nil, fmt.Errorf("context must contain a user prompt") - } - - // Generate a response - status, err := o.ChatGenerate(ctx, model.Name(), string(prompt), chatopts...) - if err != nil { - return nil, err - } - - // Create a response - response := agent.Response{ - Agent: o.Name(), - Model: model.Name(), - Text: status.Response, - Tokens: uint(status.ResponseTokens), - Duration: time.Nanosecond * time.Duration(status.TotalDurationNs), - } - - // Return success - return &response, nil -} - -///////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func newOpts(opts ...agent.Opt) ([]ChatOpt, error) { - // Apply the options - var o agent.Opts - for _, opt := range opts { - if err := opt(&o); err != nil { - return nil, err - } - } - - // Create local options - result := make([]ChatOpt, 0, len(opts)) - if o.StreamFn != nil { - result = append(result, OptStream(func(text string) { - fmt.Println(text) - })) - } - - // Return success - return result, nil -} diff --git a/pkg/ollama/chat.go b/pkg/ollama/chat.go deleted file mode 100644 index fc5d20e..0000000 --- a/pkg/ollama/chat.go +++ /dev/null @@ -1,134 +0,0 @@ -package ollama - -import ( - "context" - "encoding/base64" - "encoding/json" - "io" - "time" - - // Packages - "github.com/mutablelogic/go-client" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type reqChatCompletion struct { - Model string `json:"model"` - Prompt string `json:"prompt"` - Stream bool `json:"stream"` - System string `json:"system,omitempty"` - Template string `json:"template,omitempty"` - Images []string `json:"images,omitempty"` - Format string `json:"format,omitempty"` - Options map[string]any `json:"options,omitempty"` - callback func(string) -} - -type respChatCompletion struct { - Model string `json:"model"` - CreatedAt time.Time `json:"created_at,omitempty"` - Done bool `json:"done,omitempty"` - ChatStatus -} - -type ChatDuration time.Duration - -type ChatStatus struct { - Response string `json:"response,omitempty"` - Context []int `json:"context,omitempty"` - PromptTokens int `json:"prompt_eval_count,omitempty"` - ResponseTokens int `json:"total_eval_count,omitempty"` - LoadDurationNs int64 `json:"load_duration,omitempty"` - PromptDurationNs int64 `json:"prompt_eval_duration,omitempty"` - ResponseDurationNs int64 `json:"response_eval_duration,omitempty"` - TotalDurationNs int64 `json:"total_duration,omitempty"` -} - -type ChatOpt func(*reqChatCompletion) error - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// Generate a response, given a model and a prompt -func (c *Client) ChatGenerate(ctx context.Context, model, prompt string, opts ...ChatOpt) (ChatStatus, error) { - var request reqChatCompletion - var response respChatCompletion - - // Make the request - request.Model = model - request.Prompt = prompt - request.Options = make(map[string]any) - for _, opt := range opts { - if err := opt(&request); err != nil { - return response.ChatStatus, err - } - } - - // Create a new request - if req, err := client.NewJSONRequest(request); err != nil { - return response.ChatStatus, err - } else if err := c.DoWithContext(ctx, req, &response, client.OptPath("generate"), client.OptNoTimeout()); err != nil { - return response.ChatStatus, err - } - - // Return success - return response.ChatStatus, nil -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - OPTIONS - -// OptStream sets the callback to use for chat completion in streaming mode -func OptStream(callback func(string)) ChatOpt { - return func(req *reqChatCompletion) error { - req.Stream = true - req.callback = callback - return nil - } -} - -// OptFormatJSON sets the output to be JSON. it's also important to instruct the model to use JSON in the prompt. -func OptFormatJSON() ChatOpt { - return func(req *reqChatCompletion) error { - req.Format = "json" - return nil - } -} - -// OptImage adds an image to the chat completion request -func OptImage(r io.Reader) ChatOpt { - return func(req *reqChatCompletion) error { - if data, err := io.ReadAll(r); err != nil { - return err - } else { - req.Images = append(req.Images, base64.StdEncoding.EncodeToString(data)) - } - return nil - } -} - -// OptSeed sets the seed for the chat completion request -func OptSeed(v int) ChatOpt { - return func(req *reqChatCompletion) error { - req.Options["seed"] = v - return nil - } -} - -// OptTemperature sets deterministic output -func OptTemperature(v int) ChatOpt { - return func(req *reqChatCompletion) error { - req.Options["temperature"] = v - return nil - } -} - -/////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (m ChatStatus) String() string { - data, _ := json.MarshalIndent(m, "", " ") - return string(data) -} diff --git a/pkg/ollama/chat_test.go b/pkg/ollama/chat_test.go deleted file mode 100644 index 50811c7..0000000 --- a/pkg/ollama/chat_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package ollama_test - -import ( - "context" - "os" - "testing" - "time" - - // Packages - opts "github.com/mutablelogic/go-client" - ollama "github.com/mutablelogic/go-client/pkg/ollama" - assert "github.com/stretchr/testify/assert" -) - -func Test_chat_001(t *testing.T) { - assert := assert.New(t) - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - status, err := client.ChatGenerate(ctx, "gemma:2b", `What is the word which is the opposite of "yes"? Strictly keep your response to one word.`, ollama.OptStream(func(value string) { - t.Logf("Response: %q", value) - })) - assert.NoError(err) - t.Log(status) -} - -func Test_chat_002(t *testing.T) { - assert := assert.New(t) - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - status, err := client.ChatGenerate(ctx, "gemma:2b", `What is the word which is the opposite of "yes"? Respond using JSON with no whitespace.`, ollama.OptFormatJSON(), ollama.OptStream(func(value string) { - t.Logf("Response: %q", value) - })) - assert.NoError(err) - t.Log(status) -} - -func Test_chat_003(t *testing.T) { - assert := assert.New(t) - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - // Pull the llava model - err = client.PullModel(ctx, "llava") - assert.NoError(err) - - // Read the image - r, err := os.Open("../../etc/test/IMG_20130413_095348.JPG") - assert.NoError(err) - defer r.Close() - - // Generate a response - ctx2, cancel2 := context.WithTimeout(context.Background(), time.Minute*10) - defer cancel2() - status, err := client.ChatGenerate(ctx2, "llava", `What is in this image`, ollama.OptImage(r), ollama.OptStream(func(value string) { - t.Logf("Response: %q", value) - })) - assert.NoError(err) - t.Log(status) -} diff --git a/pkg/ollama/client.go b/pkg/ollama/client.go deleted file mode 100644 index 93f2c55..0000000 --- a/pkg/ollama/client.go +++ /dev/null @@ -1,35 +0,0 @@ -/* -ollama implements an API client for ollama -https://github.com/ollama/ollama/blob/main/docs/api.md -*/ -package ollama - -import ( - // Packages - "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-client/pkg/agent" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type Client struct { - *client.Client -} - -// Ensure it satisfies the agent.Agent interface -var _ agent.Agent = (*Client)(nil) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -func New(endPoint string, opts ...client.ClientOpt) (*Client, error) { - // Create client - client, err := client.New(append(opts, client.OptEndpoint(endPoint))...) - if err != nil { - return nil, err - } - - // Return the client - return &Client{client}, nil -} diff --git a/pkg/ollama/client_test.go b/pkg/ollama/client_test.go deleted file mode 100644 index 94136c9..0000000 --- a/pkg/ollama/client_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package ollama_test - -import ( - "os" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - ollama "github.com/mutablelogic/go-client/pkg/ollama" - assert "github.com/stretchr/testify/assert" -) - -func Test_client_001(t *testing.T) { - assert := assert.New(t) - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - t.Log(client) -} - -/////////////////////////////////////////////////////////////////////////////// -// ENVIRONMENT - -func GetEndpoint(t *testing.T) string { - key := os.Getenv("OLLAMA_URL") - if key == "" { - t.Skip("OLLAMA_URL not set") - t.SkipNow() - } - return key -} diff --git a/pkg/ollama/model.go b/pkg/ollama/model.go deleted file mode 100644 index f9f96a6..0000000 --- a/pkg/ollama/model.go +++ /dev/null @@ -1,190 +0,0 @@ -package ollama - -import ( - "context" - "encoding/json" - "net/http" - "time" - - // Packages - "github.com/mutablelogic/go-client" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -// Model is a docker image of a ollama model -type Model struct { - Name string `json:"name"` - Model string `json:"model"` - ModifiedAt time.Time `json:"modified_at"` - Size int64 `json:"size"` - Digest string `json:"digest"` - Details ModelDetails `json:"details"` -} - -// ModelShow provides details of the docker image -type ModelShow struct { - File string `json:"modelfile"` - Parameters string `json:"parameters"` - Template string `json:"template"` - Details ModelDetails `json:"details"` -} - -// ModelDetails are the details of the model -type ModelDetails struct { - ParentModel string `json:"parent_model,omitempty"` - Format string `json:"format"` - Family string `json:"family"` - Families []string `json:"families"` - ParameterSize string `json:"parameter_size"` - QuantizationLevel string `json:"quantization_level"` -} - -type reqModel struct { - Name string `json:"name"` -} - -type reqCreateModel struct { - Name string `json:"name"` - File string `json:"modelfile"` -} - -type respListModel struct { - Models []Model `json:"models"` -} - -type reqPullModel struct { - Name string `json:"name"` - Insecure bool `json:"insecure,omitempty"` - Stream bool `json:"stream"` -} - -type reqCopyModel struct { - Source string `json:"source"` - Destination string `json:"destination"` -} - -type respPullModel struct { - Status string `json:"status"` - DigestName string `json:"digest,omitempty"` - TotalBytes int64 `json:"total,omitempty"` - CompletedBytes int64 `json:"completed,omitempty"` -} - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -// List local models -func (c *Client) ListModels() ([]Model, error) { - // Send the request - var response respListModel - if err := c.Do(nil, &response, client.OptPath("tags")); err != nil { - return nil, err - } - return response.Models, nil -} - -// Show model details -func (c *Client) ShowModel(name string) (ModelShow, error) { - var response ModelShow - - // Make request - req, err := client.NewJSONRequest(reqModel{ - Name: name, - }) - if err != nil { - return response, err - } - - // Request -> Response - if err := c.Do(req, &response, client.OptPath("show")); err != nil { - return response, err - } - - // Return success - return response, nil -} - -// Delete a local model by name -func (c *Client) DeleteModel(name string) error { - // Create a new DELETE request - req, err := client.NewJSONRequestEx(http.MethodDelete, reqModel{ - Name: name, - }, client.ContentTypeAny) - if err != nil { - return err - } - - // Send the request - return c.Do(req, nil, client.OptPath("delete")) -} - -// Copy a local model by name -func (c *Client) CopyModel(source, destination string) error { - req, err := client.NewJSONRequest(reqCopyModel{ - Source: source, - Destination: destination, - }) - if err != nil { - return err - } - - // Send the request - return c.Do(req, nil, client.OptPath("copy")) -} - -// Pull a remote model locally -func (c *Client) PullModel(ctx context.Context, name string) error { - // Create a new POST request - req, err := client.NewJSONRequest(reqPullModel{ - Name: name, - Stream: true, - }) - if err != nil { - return err - } - - // Send the request - var response respPullModel - return c.DoWithContext(ctx, req, &response, client.OptPath("pull"), client.OptNoTimeout()) -} - -// Create a new model with a name and contents of the Modelfile -func (c *Client) CreateModel(ctx context.Context, name, modelfile string) error { - // Create a new POST request - req, err := client.NewJSONRequest(reqCreateModel{ - Name: name, - File: modelfile, - }) - if err != nil { - return err - } - - // Send the request - var response respPullModel - return c.DoWithContext(ctx, req, &response, client.OptPath("create")) -} - -/////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (m Model) String() string { - data, _ := json.MarshalIndent(m, "", " ") - return string(data) -} - -func (m ModelDetails) String() string { - data, _ := json.MarshalIndent(m, "", " ") - return string(data) -} - -func (m ModelShow) String() string { - data, _ := json.MarshalIndent(m, "", " ") - return string(data) -} - -func (m respPullModel) String() string { - data, _ := json.MarshalIndent(m, "", " ") - return string(data) -} diff --git a/pkg/ollama/model_test.go b/pkg/ollama/model_test.go deleted file mode 100644 index 8b506fc..0000000 --- a/pkg/ollama/model_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package ollama_test - -import ( - "context" - "os" - "testing" - "time" - - // Packages - opts "github.com/mutablelogic/go-client" - ollama "github.com/mutablelogic/go-client/pkg/ollama" - assert "github.com/stretchr/testify/assert" -) - -func Test_model_001(t *testing.T) { - assert := assert.New(t) - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - err = client.PullModel(ctx, "gemma:2b") - assert.NoError(err) -} - -func Test_model_002(t *testing.T) { - assert := assert.New(t) - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - - err = client.CopyModel("gemma:2b", "mymodel") - assert.NoError(err) -} - -func Test_model_003(t *testing.T) { - assert := assert.New(t) - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - - models, err := client.ListModels() - assert.NoError(err) - - for _, model := range models { - t.Logf("Model: %v", model) - } -} - -func Test_model_004(t *testing.T) { - assert := assert.New(t) - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - - details, err := client.ShowModel("mymodel") - assert.NoError(err) - - t.Log(details) -} - -func Test_model_005(t *testing.T) { - assert := assert.New(t) - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - - err = client.DeleteModel("mymodel") - assert.NoError(err) -} - -func Test_model_006(t *testing.T) { - assert := assert.New(t) - client, err := ollama.New(GetEndpoint(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - err = client.CreateModel(ctx, "mymodel2", "FROM gemma:2b\nSYSTEM You are mario from Super Mario Bros.") - assert.NoError(err) -} diff --git a/pkg/openai/agent.go b/pkg/openai/agent.go deleted file mode 100644 index d3594e8..0000000 --- a/pkg/openai/agent.go +++ /dev/null @@ -1,189 +0,0 @@ -package openai - -import ( - "context" - "reflect" - "time" - - // Package imports - agent "github.com/mutablelogic/go-client/pkg/agent" - schema "github.com/mutablelogic/go-client/pkg/openai/schema" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type model struct { - *schema.Model -} - -type message struct { - *schema.Message -} - -// Ensure Ollama client satisfies the agent.Agent interface -var _ agent.Agent = (*Client)(nil) - -// Ensure model satisfies the agent.Model interface -var _ agent.Model = (*model)(nil) - -// Ensure context satisfies the agent.Context interface -var _ agent.Context = (*message)(nil) - -///////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// Return the agent name -func (*Client) Name() string { - return "openai" -} - -// Return the model name -func (m *model) Name() string { - return m.Model.Id -} - -// Return the context role -func (m *message) Role() string { - return m.Message.Role -} - -// Return all the models and their capabilities -func (o *Client) Models(context.Context) ([]agent.Model, error) { - models, err := o.ListModels() - if err != nil { - return nil, err - } - - // Append models - result := make([]agent.Model, len(models)) - for i, m := range models { - result[i] = &model{Model: &m} - } - - // Return success - return result, nil -} - -// Create a user prompt -func (o *Client) UserPrompt(v string) agent.Context { - return &message{schema.NewMessage("user", v)} -} - -// Generate a response from a text message -func (o *Client) Generate(ctx context.Context, model agent.Model, content []agent.Context, opts ...agent.Opt) (*agent.Response, error) { - // Get options - chatopts, err := newOpts(opts...) - if err != nil { - return nil, err - } - - // Add model - chatopts = append(chatopts, OptModel(model.Name())) - - // Add usage option - now := time.Now() - response := agent.Response{ - Agent: o.Name(), - Model: model.Name(), - } - chatopts = append(chatopts, OptUsage(func(u schema.TokenUsage) { - response.Tokens = uint(u.TotalTokens) - response.Duration = time.Since(now) - })) - - // Create messages - messages := make([]*schema.Message, 0, len(content)) - for _, c := range content { - if message, ok := c.(*message); ok { - messages = append(messages, message.Message) - } else if toolresult, ok := c.(*agent.ToolResult); ok { - messages = append(messages, schema.NewToolResult(toolresult.Id, toolresult.Result)) - } else { - return nil, ErrBadParameter.Withf("context must contain a message (not %T)", c) - } - } - - // Append messages to the response - for _, m := range messages { - response.Context = append(response.Context, &message{m}) - } - - // Generate a response - response_content, err := o.Chat(ctx, messages, chatopts...) - if err != nil { - return nil, err - } - - // Combine content into a single response, and add to the context - for _, c := range response_content { - if c.Text != "" { - response.Text += c.Text - } else if c.Type == "function" { - response.ToolCall = &agent.ToolCall{Id: c.Id, Name: c.Name, Args: c.Input} - } - } - - // Append the response to the context - if response.ToolCall != nil { - m := schema.NewMessage("assistant", "") - m.ToolCalls = []schema.ToolCall{ - { - Id: response.ToolCall.Id, - Type: "function", - Function: schema.ToolFunction{ - Name: response.ToolCall.Name, - Arguments: response.ToolCall.JSON(), - }, - }, - } - response.Context = append(response.Context, &message{m}) - } else { - response.Context = append(response.Context, &message{schema.NewMessage("assistant", response.Text)}) - } - - // Return success - return &response, nil -} - -///////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func newOpts(opts ...agent.Opt) ([]Opt, error) { - // Apply the options - var o agent.Opts - for _, opt := range opts { - if err := opt(&o); err != nil { - return nil, err - } - } - - // Create local options - result := make([]Opt, 0, len(opts)) - - // Stream - if o.StreamFn != nil { - result = append(result, OptStream(func(text schema.MessageChoice) { - if text.Delta != nil && text.Delta.Content != "" { - o.StreamFn(agent.Response{ - Text: text.Delta.Content, - }) - } - })) - } - - // Create tools - for _, tool := range o.Tools { - otool := schema.NewTool(tool.Name(), tool.Description()) - for _, param := range tool.Params() { - otool.Add(param.Name, param.Description, param.Required, reflect.TypeOf("")) - } - result = append(result, OptTool(otool)) - } - - // Return success - return result, nil -} diff --git a/pkg/openai/audio.go b/pkg/openai/audio.go deleted file mode 100644 index 1b3b2d2..0000000 --- a/pkg/openai/audio.go +++ /dev/null @@ -1,222 +0,0 @@ -package openai - -import ( - "bytes" - "context" - "encoding/json" - "io" - "os" - "path/filepath" - - // Packages - "github.com/mutablelogic/go-client" - "github.com/mutablelogic/go-client/pkg/multipart" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type reqSpeech struct { - options - Text string `json:"input"` - Voice string `json:"voice"` -} - -type respSpeech struct { - bytes int64 - w io.Writer -} - -type reqTranscribe struct { - options - File multipart.File `json:"file"` -} - -// Represents a transcription response returned by model, based on the provided input. -type Transcription struct { - Task string `json:"task,omitempty"` - Language string `json:"language,omitempty"` // The language of the input audio. - Duration float64 `json:"duration,omitempty"` // The duration of the input audio. - Text string `json:"text" writer:",wrap,width:40"` - Words []struct { - Word string `json:"word"` // The text content of the word. - Start float64 `json:"start"` // Start time of the word in seconds. - End float64 `json:"end"` // End time of the word in seconds. - } `json:"words,omitempty"` // Extracted words and their corresponding timestamps. - Segments []struct { - Id uint `json:"id"` - Seek uint `json:"seek"` - Start float64 `json:"start"` - End float64 `json:"end"` - Text string `json:"text"` - Tokens []uint `json:"tokens"` // Array of token IDs for the text content. - Temperature float64 `json:"temperature,omitempty"` // Temperature parameter used for generating the segment. - AvgLogProbability float64 `json:"avg_logprob,omitempty"` // Average logprob of the segment. If the value is lower than -1, consider the logprobs failed. - CompressionRatio float64 `json:"compression_ratio,omitempty"` // Compression ratio of the segment. If the value is greater than 2.4, consider the compression failed. - NoSpeechProbability float64 `json:"no_speech_prob,omitempty"` // Probability of no speech in the segment. If the value is higher than 1.0 and the avg_logprob is below -1, consider this segment silent. - } `json:"segments,omitempty" writer:",wrap"` -} - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -const ( - defaultAudioModel = "tts-1" - defaultTranscribeModel = "whisper-1" -) - -/////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (r reqTranscribe) String() string { - data, _ := json.MarshalIndent(r, "", " ") - return string(data) -} - -/////////////////////////////////////////////////////////////////////////////// -// API CALLS - -// Creates audio for the given text, outputs to the writer and returns -// the number of bytes written -func (c *Client) TextToSpeech(ctx context.Context, w io.Writer, voice, text string, opts ...Opt) (int64, error) { - var request reqSpeech - var response respSpeech - - // Create the request and set up the response - request.Model = defaultAudioModel - request.Voice = voice - request.Text = text - response.w = w - - // Set opts - for _, opt := range opts { - if err := opt(&request.options); err != nil { - return 0, err - } - } - - // Make a response object, write the data - if payload, err := client.NewJSONRequest(request); err != nil { - return 0, err - } else if err := c.DoWithContext(ctx, payload, &response, client.OptPath("audio/speech")); err != nil { - return 0, err - } - - // Return the number of bytes written - return response.bytes, nil -} - -// Transcribes audio from audio data -func (c *Client) Transcribe(ctx context.Context, r io.Reader, opts ...Opt) (*Transcription, error) { - var request reqTranscribe - response := new(Transcription) - - name := "" - if f, ok := r.(*os.File); ok { - name = filepath.Base(f.Name()) - } - - // Create the request and set up the response - request.Model = defaultTranscribeModel - request.File = multipart.File{ - Path: name, - Body: r, - } - - // Set options - for _, opt := range opts { - if err := opt(&request.options); err != nil { - return nil, err - } - } - - // Debugging - c.Debugf("transcribe: %v", request) - - // Make a response object, write the data - if payload, err := client.NewMultipartRequest(request, client.ContentTypeJson); err != nil { - return nil, err - } else if err := c.DoWithContext(ctx, payload, response, client.OptPath("audio/transcriptions")); err != nil { - return nil, err - } - - // Return success - return response, nil -} - -// Translate audio into English -func (c *Client) Translate(ctx context.Context, r io.Reader, opts ...Opt) (*Transcription, error) { - var request reqTranscribe - response := new(Transcription) - - name := "" - if f, ok := r.(*os.File); ok { - name = filepath.Base(f.Name()) - } - - // Create the request and set up the response - request.Model = defaultTranscribeModel - request.File = multipart.File{ - Path: name, - Body: r, - } - - // Set options - for _, opt := range opts { - if err := opt(&request.options); err != nil { - return nil, err - } - } - - // Debugging - c.Debugf("translate: %v", request) - - // Make a response object, write the data - if payload, err := client.NewMultipartRequest(request, client.ContentTypeJson); err != nil { - return nil, err - } else if err := c.DoWithContext(ctx, payload, response, client.OptPath("audio/translations")); err != nil { - return nil, err - } - - // Return success - return response, nil -} - -/////////////////////////////////////////////////////////////////////////////// -// Unmarshal - -func (resp *respSpeech) Unmarshal(mimetype string, r io.Reader) error { - // Copy the data - if n, err := io.Copy(resp.w, r); err != nil { - return err - } else { - resp.bytes = n - } - - // Return success - return nil -} - -func (resp *Transcription) Unmarshal(mimetype string, r io.Reader) error { - switch mimetype { - case client.ContentTypeTextPlain: - buf := bytes.NewBuffer(nil) - if _, err := io.Copy(buf, r); err != nil { - return err - } else { - resp.Text = buf.String() - } - case client.ContentTypeJson: - if err := json.NewDecoder(r).Decode(resp); err != nil { - return err - } - default: - return ErrNotImplemented.With("Unmarshal", mimetype) - } - - // Return success - return nil -} diff --git a/pkg/openai/chat.go b/pkg/openai/chat.go deleted file mode 100644 index 25ab38d..0000000 --- a/pkg/openai/chat.go +++ /dev/null @@ -1,214 +0,0 @@ -package openai - -import ( - "context" - "encoding/json" - "io" - "reflect" - - // Packages - client "github.com/mutablelogic/go-client" - schema "github.com/mutablelogic/go-client/pkg/openai/schema" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -// A request for a chat completion -type reqChat struct { - options - Tools []reqChatTools `json:"tools,omitempty"` - Messages []*schema.Message `json:"messages,omitempty"` -} - -type reqChatTools struct { - Type string `json:"type"` - Function *schema.Tool `json:"function"` -} - -// A chat completion object -type respChat struct { - Id string `json:"id"` - Created int64 `json:"created"` - Model string `json:"model"` - Choices []*schema.MessageChoice `json:"choices"` - TokenUsage schema.TokenUsage `json:"usage,omitempty"` -} - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -const ( - defaultChatCompletion = "gpt-3.5-turbo" - endOfStreamToken = "[DONE]" -) - -/////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (v respChat) String() string { - if data, err := json.MarshalIndent(v, "", " "); err != nil { - return err.Error() - } else { - return string(data) - } -} - -/////////////////////////////////////////////////////////////////////////////// -// API CALLS - -// Chat creates a model response for the given chat conversation. -func (c *Client) Chat(ctx context.Context, messages []*schema.Message, opts ...Opt) ([]*schema.Content, error) { - var request reqChat - var response respChat - - // Set request options - request.Model = defaultChatCompletion - request.Messages = messages - for _, opt := range opts { - if err := opt(&request.options); err != nil { - return nil, err - } - } - - // Append tools - for _, tool := range request.options.Tools { - request.Tools = append(request.Tools, reqChatTools{ - Type: "function", - Function: tool, - }) - } - - // Set up the request - reqopts := []client.RequestOpt{ - client.OptPath("chat/completions"), - } - if request.Stream { - reqopts = append(reqopts, client.OptTextStreamCallback(func(event client.TextStreamEvent) error { - return response.streamCallback(event, request.StreamCallback) - })) - } - - // Request->Response - if payload, err := client.NewJSONRequest(request); err != nil { - return nil, err - } else if err := c.DoWithContext(ctx, payload, &response, reqopts...); err != nil { - return nil, err - } else if len(response.Choices) == 0 { - return nil, ErrUnexpectedResponse.With("no choices returned") - } - - // Return all choices - var result []*schema.Content - for _, choice := range response.Choices { - // A choice must have a message, content and/or tool calls - if choice.Message == nil || choice.Message.Content == nil && len(choice.Message.ToolCalls) == 0 { - continue - } - for _, tool := range choice.Message.ToolCalls { - result = append(result, schema.ToolUse(tool)) - } - if choice.Message.Content == nil { - continue - } - if choice.Message.Role != "assistant" { - return nil, ErrUnexpectedResponse.With("unexpected content role ", choice.Message.Role) - } - switch v := choice.Message.Content.(type) { - case []string: - for _, v := range v { - result = append(result, schema.Text(v)) - } - case string: - result = append(result, schema.Text(v)) - default: - return nil, ErrUnexpectedResponse.With("unexpected content type ", reflect.TypeOf(choice.Message.Content)) - } - } - - // Usage callback - if request.Usage != nil { - request.Usage(response.TokenUsage) - } - - // Return success - return result, nil -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func (response *respChat) streamCallback(v client.TextStreamEvent, fn Callback) error { - var delta schema.MessageChunk - - // [DONE] indicates the end of the stream, return io.EOF - // or decode the data into a MessageChunk - if v.Data == endOfStreamToken { - return io.EOF - } else if err := v.Json(&delta); err != nil { - return err - } - - // Set the response fields - if delta.Id != "" { - response.Id = delta.Id - } - if delta.Model != "" { - response.Model = delta.Model - } - if delta.Created != 0 { - response.Created = delta.Created - } - if delta.TokenUsage != nil { - response.TokenUsage = *delta.TokenUsage - } - - // With no choices, return success - if len(delta.Choices) == 0 { - return nil - } - - // Append choices - for _, choice := range delta.Choices { - // Sanity check the choice index - if choice.Index < 0 || choice.Index >= 6 { - continue - } - // Ensure message has the choice - for { - if choice.Index < len(response.Choices) { - break - } - response.Choices = append(response.Choices, new(schema.MessageChoice)) - } - // Append the choice data onto the messahe - if response.Choices[choice.Index].Message == nil { - response.Choices[choice.Index].Message = new(schema.Message) - } - if choice.Index != 0 { - response.Choices[choice.Index].Index = choice.Index - } - if choice.FinishReason != "" { - response.Choices[choice.Index].FinishReason = choice.FinishReason - } - if choice.Delta != nil { - if choice.Delta.Role != "" { - response.Choices[choice.Index].Message.Role = choice.Delta.Role - } - if choice.Delta.Content != "" { - response.Choices[choice.Index].Message.Add(choice.Delta.Content) - } - } - - // Callback to the client - if fn != nil { - fn(choice) - } - } - - // Return success - return nil -} diff --git a/pkg/openai/chat_test.go b/pkg/openai/chat_test.go deleted file mode 100644 index 30fadec..0000000 --- a/pkg/openai/chat_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package openai_test - -import ( - "context" - "encoding/json" - "os" - "reflect" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - openai "github.com/mutablelogic/go-client/pkg/openai" - schema "github.com/mutablelogic/go-client/pkg/openai/schema" - assert "github.com/stretchr/testify/assert" -) - -func Test_chat_001(t *testing.T) { - assert := assert.New(t) - client, err := openai.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - - message := schema.NewMessage("user", "What would be the best app to use to get the weather in berlin today?") - response, err := client.Chat(context.Background(), []*schema.Message{message}) - assert.NoError(err) - assert.NotNil(response) - assert.NotEmpty(response) - - data, err := json.MarshalIndent(response, "", " ") - assert.NoError(err) - t.Log(string(data)) - -} - -func Test_chat_002(t *testing.T) { - assert := assert.New(t) - client, err := openai.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - - message := schema.NewMessage("user", "What will the weather be like in Berlin tomorrow?") - assert.NotNil(message) - - get_weather := schema.NewTool("get_weather", "Get the weather in a specific city and country") - assert.NotNil(get_weather) - assert.NoError(get_weather.Add("city", "The city to get the weather for", true, reflect.TypeOf("string"))) - assert.NoError(get_weather.Add("country", "The country to get the weather for", true, reflect.TypeOf("string"))) - assert.NoError(get_weather.Add("time", "When to get the weather for. If not specified, defaults to the current time", true, reflect.TypeOf("string"))) - - response, err := client.Chat(context.Background(), []*schema.Message{message}, openai.OptTool(get_weather)) - assert.NoError(err) - assert.NotNil(response) - assert.NotEmpty(response) - - data, err := json.MarshalIndent(response, "", " ") - assert.NoError(err) - t.Log(string(data)) - -} - -func Test_chat_003(t *testing.T) { - assert := assert.New(t) - client, err := openai.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - - message := schema.NewMessage("user", "What is in this image") - image, err := schema.ImageUrl("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "auto") - assert.NoError(err) - assert.NotNil(message.Add(image)) - response, err := client.Chat(context.Background(), []*schema.Message{message}, openai.OptModel("gpt-4-vision-preview")) - assert.NoError(err) - assert.NotNil(response) - assert.NotEmpty(response) - - data, err := json.MarshalIndent(response, "", " ") - assert.NoError(err) - t.Log(string(data)) -} diff --git a/pkg/openai/client.go b/pkg/openai/client.go deleted file mode 100644 index 8b9f0b4..0000000 --- a/pkg/openai/client.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -openai implements an API client for OpenAI -https://platform.openai.com/docs/api-reference -*/ -package openai - -import ( - // Packages - "github.com/mutablelogic/go-client" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type Client struct { - *client.Client -} - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -const ( - endPoint = "https://api.openai.com/v1" -) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -func New(ApiKey string, opts ...client.ClientOpt) (*Client, error) { - // Create client - client, err := client.New(append(opts, client.OptEndpoint(endPoint), client.OptReqToken(client.Token{ - Scheme: client.Bearer, - Value: ApiKey, - }))...) - if err != nil { - return nil, err - } - - // Return the client - return &Client{client}, nil -} diff --git a/pkg/openai/client_test.go b/pkg/openai/client_test.go deleted file mode 100644 index c0ff050..0000000 --- a/pkg/openai/client_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package openai_test - -import ( - "os" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - openai "github.com/mutablelogic/go-client/pkg/openai" - assert "github.com/stretchr/testify/assert" -) - -func Test_client_001(t *testing.T) { - assert := assert.New(t) - client, err := openai.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - t.Log(client) -} - -/////////////////////////////////////////////////////////////////////////////// -// ENVIRONMENT - -func GetApiKey(t *testing.T) string { - key := os.Getenv("OPENAI_API_KEY") - if key == "" { - t.Skip("OPENAI_API_KEY not set") - t.SkipNow() - } - return key -} diff --git a/pkg/openai/embedding.go b/pkg/openai/embedding.go deleted file mode 100644 index 35244cf..0000000 --- a/pkg/openai/embedding.go +++ /dev/null @@ -1,58 +0,0 @@ -package openai - -import ( - "context" - - // Packages - client "github.com/mutablelogic/go-client" - schema "github.com/mutablelogic/go-client/pkg/openai/schema" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -// A request to create embeddings -type reqCreateEmbedding struct { - options - Input []string `json:"input"` - EncodingFormat string `json:"encoding_format,omitempty"` -} - -/////////////////////////////////////////////////////////////////////////////// -// API CALLS - -// CreateEmbedding creates an embedding from a string or array of strings -func (c *Client) CreateEmbedding(ctx context.Context, content any, opts ...Opt) (schema.Embeddings, error) { - - // Apply the options - var request reqCreateEmbedding - for _, opt := range opts { - if err := opt(&request.options); err != nil { - return schema.Embeddings{}, err - } - } - - // Set the input, which is either a string or array of strings - switch v := content.(type) { - case string: - request.Input = []string{v} - case []string: - request.Input = v - default: - return schema.Embeddings{}, ErrBadParameter - } - - // Return the response - var response schema.Embeddings - if payload, err := client.NewJSONRequest(request); err != nil { - return schema.Embeddings{}, err - } else if err := c.DoWithContext(ctx, payload, &response, client.OptPath("embeddings")); err != nil { - return schema.Embeddings{}, err - } - - // Return success - return response, nil -} diff --git a/pkg/openai/embedding_test.go b/pkg/openai/embedding_test.go deleted file mode 100644 index 025a684..0000000 --- a/pkg/openai/embedding_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package openai_test - -import ( - "context" - "os" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - openai "github.com/mutablelogic/go-client/pkg/openai" - assert "github.com/stretchr/testify/assert" -) - -func Test_embedding_001(t *testing.T) { - assert := assert.New(t) - client, err := openai.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - - embedding, err := client.CreateEmbedding(context.Background(), "test", openai.OptModel("text-embedding-ada-002")) - assert.NoError(err) - assert.NotEmpty(embedding) -} diff --git a/pkg/openai/image.go b/pkg/openai/image.go deleted file mode 100644 index 185e00d..0000000 --- a/pkg/openai/image.go +++ /dev/null @@ -1,127 +0,0 @@ -package openai - -import ( - "context" - "encoding/base64" - "io" - "net/http" - - // Packages - "github.com/mutablelogic/go-client" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -// A request for an image -type reqImage struct { - options -} - -type responseImages struct { - Created int64 `json:"created"` - Data []*Image `json:"data"` -} - -// An image -type Image struct { - Url string `json:"url,omitempty"` - Data string `json:"b64_json,omitempty"` - RevisedPrompt string `json:"revised_prompt,omitempty"` -} - -/////////////////////////////////////////////////////////////////////////////// -// API CALLS - -// CreateImage generates one or more images from a prompt -func (c *Client) CreateImages(ctx context.Context, prompt string, opts ...Opt) ([]*Image, error) { - var request reqImage - var response responseImages - - // Create the request - request.Prompt = prompt - for _, opt := range opts { - if err := opt(&request.options); err != nil { - return nil, err - } - } - - // Return the response - if payload, err := client.NewJSONRequest(request); err != nil { - return nil, err - } else if err := c.DoWithContext(ctx, payload, &response, client.OptPath("images/generations")); err != nil { - return nil, err - } - - // Return success - return response.Data, nil -} - -// WriteImage writes an image and returns the number of bytes written -func (c *Client) WriteImage(w io.Writer, image *Image) (int, error) { - if image == nil { - return 0, ErrBadParameter.With("WriteImage") - } - // Handle url or data - switch { - case image.Data != "": - if data, err := base64.StdEncoding.DecodeString(image.Data); err != nil { - return 0, err - } else if n, err := w.Write(data); err != nil { - return 0, err - } else { - return n, nil - } - case image.Url != "": - var resp reqUrl - resp.w = w - if req, err := http.NewRequest(http.MethodGet, image.Url, nil); err != nil { - return 0, err - } else if err := c.Request(req, &resp, client.OptToken(client.Token{})); err != nil { - return 0, err - } else { - return resp.n, nil - } - default: - return 0, ErrNotImplemented.With("WriteImage") - } -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -type reqUrl struct { - w io.Writer - n int -} - -func (i *reqUrl) Unmarshal(mimetype string, r io.Reader) error { - defer func() { - // Close the reader if it's an io.ReadCloser - if closer, ok := r.(io.ReadCloser); ok { - closer.Close() - } - }() - - buffer := make([]byte, 1024) - for { - // Read data from the reader into the buffer - bytesRead, err := r.Read(buffer) - if err == io.EOF { - // If we've reached EOF, break out of the loop - break - } else if err != nil { - return err - } else if n, err := i.w.Write(buffer[:bytesRead]); err != nil { - return err - } else { - i.n += n - } - } - - // Return success - return nil -} diff --git a/pkg/openai/image_test.go b/pkg/openai/image_test.go deleted file mode 100644 index a26b638..0000000 --- a/pkg/openai/image_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package openai_test - -import ( - "context" - "fmt" - "os" - "path/filepath" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - openai "github.com/mutablelogic/go-client/pkg/openai" - assert "github.com/stretchr/testify/assert" -) - -func Test_image_001(t *testing.T) { - assert := assert.New(t) - client, err := openai.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - - images, err := client.CreateImages(context.Background(), "A painting of a cat", openai.OptCount(1)) - assert.NoError(err) - assert.NotNil(images) - assert.NotEmpty(images) - assert.Len(images, 1) -} - -func Test_image_002(t *testing.T) { - assert := assert.New(t) - client, err := openai.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - - // Create one image - images, err := client.CreateImages(context.Background(), "A painting of a cat in the style of Salvador Dali", openai.OptResponseFormat("b64_json"), openai.OptCount(1)) - assert.NoError(err) - assert.NotNil(images) - assert.NotEmpty(images) - assert.Len(images, 1) - - // Output images - for n, image := range images { - filename := filepath.Join(t.TempDir(), fmt.Sprintf("%s-%d.png", t.Name(), n)) - if w, err := os.Create(filename); err != nil { - t.Error(err) - } else { - defer w.Close() - t.Log("Writing", w.Name()) - n, err := client.WriteImage(w, image) - assert.NoError(err) - assert.NotZero(n) - } - } - -} - -func Test_image_003(t *testing.T) { - assert := assert.New(t) - client, err := openai.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - - // Create one image - images, err := client.CreateImages(context.Background(), "A painting of a cat in the style of Van Gogh", openai.OptResponseFormat("url"), openai.OptCount(1)) - assert.NoError(err) - assert.NotNil(images) - assert.NotEmpty(images) - assert.Len(images, 1) - - // Output images - for n, image := range images { - filename := filepath.Join(t.TempDir(), fmt.Sprintf("%s-%d.png", t.Name(), n)) - if w, err := os.Create(filename); err != nil { - t.Error(err) - } else { - defer w.Close() - t.Log("Writing", w.Name()) - n, err := client.WriteImage(w, image) - assert.NoError(err) - assert.NotZero(n) - } - } - -} diff --git a/pkg/openai/model.go b/pkg/openai/model.go deleted file mode 100644 index 2fc09a3..0000000 --- a/pkg/openai/model.go +++ /dev/null @@ -1,51 +0,0 @@ -package openai - -import ( - // Packages - client "github.com/mutablelogic/go-client" - schema "github.com/mutablelogic/go-client/pkg/openai/schema" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type responseListModels struct { - Data []schema.Model `json:"data"` -} - -/////////////////////////////////////////////////////////////////////////////// -// API CALLS - -// ListModels returns all the models -func (c *Client) ListModels() ([]schema.Model, error) { - // Return the response - var response responseListModels - if err := c.Do(nil, &response, client.OptPath("models")); err != nil { - return nil, err - } - - // Return success - return response.Data, nil -} - -// GetModel returns one model -func (c *Client) GetModel(model string) (schema.Model, error) { - // Return the response - var response schema.Model - if err := c.Do(nil, &response, client.OptPath("models", model)); err != nil { - return schema.Model{}, err - } - - // Return success - return response, nil -} - -// Delete a fine-tuned model. You must have the Owner role in your organization to delete a model. -func (c *Client) DeleteModel(model string) error { - if err := c.Do(client.MethodDelete, nil, client.OptPath("models", model)); err != nil { - return err - } - - // Return success - return nil -} diff --git a/pkg/openai/model_test.go b/pkg/openai/model_test.go deleted file mode 100644 index 5459fab..0000000 --- a/pkg/openai/model_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package openai_test - -import ( - "encoding/json" - "os" - "testing" - - // Packages - opts "github.com/mutablelogic/go-client" - openai "github.com/mutablelogic/go-client/pkg/openai" - assert "github.com/stretchr/testify/assert" -) - -func Test_models_001(t *testing.T) { - assert := assert.New(t) - client, err := openai.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - response, err := client.ListModels() - assert.NoError(err) - assert.NotEmpty(response) - data, err := json.MarshalIndent(response, "", " ") - assert.NoError(err) - t.Log(string(data)) -} - -func Test_models_002(t *testing.T) { - assert := assert.New(t) - client, err := openai.New(GetApiKey(t), opts.OptTrace(os.Stderr, true)) - assert.NoError(err) - assert.NotNil(client) - response, err := client.ListModels() - assert.NoError(err) - assert.NotEmpty(response) - - for _, model := range response { - model, err := client.GetModel(model.Id) - assert.NoError(err) - assert.NotEmpty(model) - data, err := json.MarshalIndent(model, "", " ") - assert.NoError(err) - t.Log(string(data)) - } -} diff --git a/pkg/openai/moderations.go b/pkg/openai/moderations.go deleted file mode 100644 index b4b23eb..0000000 --- a/pkg/openai/moderations.go +++ /dev/null @@ -1,86 +0,0 @@ -package openai - -import ( - "context" - - // Packages - client "github.com/mutablelogic/go-client" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type reqModerations struct { - options - Input []string `json:"input"` -} - -type responseModerations struct { - Id string `json:"id"` - Model string `json:"model"` - Results []Moderation `json:"results"` -} - -// Moderation represents the moderation of a text, including whether it is flagged -type Moderation struct { - Flagged bool `json:"flagged"` - Categories struct { - Sexual bool `json:"sexual,omitempty"` - Hate bool `json:"hate,omitempty"` - Harassment bool `json:"harassment,omitempty"` - SelfHarm bool `json:"self-harm,omitempty"` - SexualMinors bool `json:"sexual/minors,omitempty"` - HateThreatening bool `json:"hate/threatening,omitempty"` - ViolenceGraphic bool `json:"violence/graphic,omitempty"` - SelfHarmIntent bool `json:"self-harm/intent,omitempty"` - HarasssmentThreatening bool `json:"harassment/threatening,omitempty"` - Violence bool `json:"violence,omitempty"` - } `json:"categories,omitempty" writer:",wrap"` - CategoryScores struct { - Sexual float32 `json:"sexual,omitempty"` - Hate float32 `json:"hate,omitempty"` - Harassment float32 `json:"harassment,omitempty"` - SelfHarm float32 `json:"self-harm,omitempty"` - SexualMinors float32 `json:"sexual/minors,omitempty"` - HateThreatening float32 `json:"hate/threatening,omitempty"` - ViolenceGraphic float32 `json:"violence/graphic,omitempty"` - SelfHarmIntent float32 `json:"self-harm/intent,omitempty"` - HarasssmentThreatening float32 `json:"harassment/threatening,omitempty"` - Violence float32 `json:"violence,omitempty"` - } `json:"category_scores,omitempty" writer:",wrap"` -} - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -const ( - defaultModerationModel = "text-moderation-latest" -) - -/////////////////////////////////////////////////////////////////////////////// -// API CALLS - -// Classifies if text is potentially harmful -func (c *Client) Moderations(ctx context.Context, text []string, opts ...Opt) ([]Moderation, error) { - var request reqModerations - var response responseModerations - - // Set options - request.Model = defaultModerationModel - request.Input = text - for _, opt := range opts { - if err := opt(&request.options); err != nil { - return nil, err - } - } - - // Request->Response - if payload, err := client.NewJSONRequest(request); err != nil { - return nil, err - } else if err := c.DoWithContext(ctx, payload, &response, client.OptPath("moderations")); err != nil { - return nil, err - } - - // Return success - return response.Results, nil -} diff --git a/pkg/openai/opts.go b/pkg/openai/opts.go deleted file mode 100644 index fd12563..0000000 --- a/pkg/openai/opts.go +++ /dev/null @@ -1,271 +0,0 @@ -package openai - -import ( - // Packages - client "github.com/mutablelogic/go-client" - schema "github.com/mutablelogic/go-client/pkg/openai/schema" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type options struct { - // Common options - Count int `json:"n,omitempty"` - MaxTokens int `json:"max_tokens,omitempty"` - Model string `json:"model,omitempty"` - ResponseFormat string `json:"response_format,omitempty"` - Seed int `json:"seed,omitempty"` - Temperature *float32 `json:"temperature,omitempty"` - User string `json:"user,omitempty"` - - // Options for chat - FrequencyPenalty float32 `json:"frequency_penalty,omitempty"` - PresencePenalty float32 `json:"presence_penalty,omitempty"` - Tools []*schema.Tool `json:"-"` - Stop []string `json:"stop,omitempty"` - Stream bool `json:"stream,omitempty"` - StreamOptions *streamoptions `json:"stream_options,omitempty"` - StreamCallback Callback `json:"-"` - - // Options for audio - Language string `json:"language,omitempty"` - Prompt string `json:"prompt,omitempty"` - Speed *float32 `json:"speed,omitempty"` - - // Options for images - Quality string `json:"quality,omitempty"` - Size string `json:"size,omitempty"` - Style string `json:"style,omitempty"` - - // Options for usage - Usage UsageFn `json:"-"` -} - -type streamoptions struct { - IncludeUsage bool `json:"include_usage,omitempty"` -} - -// Opt is a function which can be used to set options on a request -type Opt func(*options) error - -// Callback when new stream data is received -type Callback func(schema.MessageChoice) - -// Callback to set the token usage -type UsageFn func(schema.TokenUsage) - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// ID of the model to use -func OptModel(value string) Opt { - return func(o *options) error { - o.Model = value - return nil - } -} - -// Number between -2.0 and 2.0. Positive values penalize new tokens based on -// their existing frequency in the text so far, decreasing the model's likelihood -// to repeat the same line verbatim. -func OptFrequencyPenalty(value float32) Opt { - return func(o *options) error { - if value < -2.0 || value > 2.0 { - return ErrBadParameter.With("OptFrequencyPenalty") - } - o.FrequencyPenalty = value - return nil - } -} - -// Number between -2.0 and 2.0. Positive values penalize new tokens based on whether -// they appear in the text so far, increasing the model's likelihood to talk about -// new topics. -func OptPresencePenalty(value float32) Opt { - return func(o *options) error { - if value < -2.0 || value > 2.0 { - return ErrBadParameter.With("OptPresencePenalty") - } - o.PresencePenalty = value - return nil - } -} - -// Maximum number of tokens to generate in the reply -func OptMaxTokens(value int) Opt { - return func(o *options) error { - o.MaxTokens = value - return nil - } -} - -// How many chat choices or images to return -func OptCount(value int) Opt { - return func(o *options) error { - o.Count = value - return nil - } -} - -// Format of the returned response, use "json_format" to enable JSON mode, which guarantees -// the message the model generates is valid JSON. -// Important: when using JSON mode, you must also instruct the model to produce JSON -// yourself via a system or user message. -func OptResponseFormat(value string) Opt { - return func(o *options) error { - o.ResponseFormat = value - return nil - } -} - -// When set, system will make a best effort to sample deterministically, such that repeated -// requests with the same seed and parameters should return the same result. -func OptSeed(value int) Opt { - return func(o *options) error { - o.Seed = value - return nil - } -} - -// Custom text sequences that will cause the model to stop generating. -func OptStop(value ...string) Opt { - return func(o *options) error { - o.Stop = value - return nil - } -} - -// Stream the response, which will be returned as a series of message chunks. -func OptStream(fn Callback) Opt { - return func(o *options) error { - o.Stream = true - o.StreamOptions = &streamoptions{ - IncludeUsage: true, - } - o.StreamCallback = fn - return nil - } -} - -// When set, system will make a best effort to sample deterministically, such that repeated -// requests with the same seed and parameters should return the same result. -func OptTemperature(v float32) Opt { - return func(o *options) error { - if v < 0.0 || v > 2.0 { - return ErrBadParameter.With("OptTemperature") - } - o.Temperature = &v - return nil - } -} - -// A list of tools the model may call. Currently, only functions are supported as a tool. -// Use this to provide a list of functions the model may generate JSON inputs for. -// A max of 128 functions are supported. -func OptTool(value ...*schema.Tool) Opt { - return func(o *options) error { - // Check tools - for _, tool := range value { - if tool == nil { - return ErrBadParameter.With("OptTool") - } - } - - // Append tools - o.Tools = append(o.Tools, value...) - - // Return success - return nil - } -} - -// A unique identifier representing your end-user, which can help OpenAI to monitor -// and detect abuse -func OptUser(value string) Opt { - return func(o *options) error { - o.User = value - return nil - } -} - -// The speed of the generated audio. -func OptSpeed(v float32) Opt { - return func(o *options) error { - if v < 0.25 || v > 4.0 { - return ErrBadParameter.With("OptSpeed") - } - o.Speed = &v - return nil - } -} - -// An optional text to guide the model's style or continue a previous audio segment. -// The prompt should match the audio language. -func OptPrompt(value string) Opt { - return func(o *options) error { - o.Prompt = value - return nil - } -} - -// The language of the input audio. Supplying the input language in ISO-639-1 -// format will improve accuracy and latency. -func OptLanguage(value string) Opt { - return func(o *options) error { - o.Language = value - return nil - } -} - -// The quality of the image that will be generated. hd creates images with -// finer details and greater consistency across the image. -func OptQuality(value string) Opt { - return func(o *options) error { - o.Quality = value - return nil - } -} - -// The size of the generated images. Must be one of 256x256, 512x512, -// or 1024x1024 for dall-e-2. Must be one of 1024x1024, 1792x1024, -// or 1024x1792 for dall-e-3 models. -func OptSize(value string) Opt { - return func(o *options) error { - o.Size = value - return nil - } -} - -// The style of the generated images. Must be one of vivid or natural. -// Vivid causes the model to lean towards generating hyper-real and -// dramatic images. Natural causes the model to produce more natural, -// less hyper-real looking images. -func OptStyle(value string) Opt { - return func(o *options) error { - o.Style = value - return nil - } -} - -// The style of the generated images. Must be one of vivid or natural. -// Vivid causes the model to lean towards generating hyper-real and -// dramatic images. Natural causes the model to produce more natural, -// less hyper-real looking images. -func OptUsage(fn UsageFn) Opt { - return func(o *options) error { - o.Usage = fn - return nil - } -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - CLIENT - -// Set an organization where the user has access to multiple organizations -func OptOrganization(value string) client.ClientOpt { - return client.OptHeader("OpenAI-Organization", value) -} diff --git a/pkg/openai/schema/embedding.go b/pkg/openai/schema/embedding.go deleted file mode 100644 index 01325b9..0000000 --- a/pkg/openai/schema/embedding.go +++ /dev/null @@ -1,54 +0,0 @@ -package schema - -import "math" - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -// An embedding object -type Embedding struct { - Embedding []float64 `json:"embedding"` - Index int `json:"index"` -} - -// An set of created embeddings -type Embeddings struct { - Id string `json:"id" writer:",width:32"` - Data []Embedding `json:"data" writer:",wrap"` - Model string `json:"model"` - Usage struct { - PromptTokerns int `json:"prompt_tokens"` - TotalTokens int `json:"total_tokens"` - } `json:"usage" writer:",wrap"` -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -func (e Embedding) CosineDistance(other Embedding) float64 { - count := 0 - length_a := len(e.Embedding) - length_b := len(other.Embedding) - if length_a > length_b { - count = length_a - } else { - count = length_b - } - sumA := 0.0 - s1 := 0.0 - s2 := 0.0 - for k := 0; k < count; k++ { - if k >= length_a { - s2 += math.Pow(other.Embedding[k], 2) - continue - } - if k >= length_b { - s1 += math.Pow(e.Embedding[k], 2) - continue - } - sumA += e.Embedding[k] * other.Embedding[k] - s1 += math.Pow(e.Embedding[k], 2) - s2 += math.Pow(other.Embedding[k], 2) - } - return sumA / (math.Sqrt(s1) * math.Sqrt(s2)) -} diff --git a/pkg/openai/schema/message.go b/pkg/openai/schema/message.go deleted file mode 100644 index 95f6faf..0000000 --- a/pkg/openai/schema/message.go +++ /dev/null @@ -1,394 +0,0 @@ -package schema - -import ( - "encoding/base64" - "encoding/json" - "io" - "net/http" - "net/url" - "os" - "reflect" - "strings" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -// A chat completion message -type Message struct { - // user, system or assistant - Role string `json:"role,omitempty"` - - // Message Id - Id string `json:"id,omitempty"` - - // Model - Model string `json:"model,omitempty"` - - // Content can be a string, array of strings, content - // object or an array of content objects - Content any `json:"content,omitempty"` - - // Any tool calls - ToolCalls []ToolCall `json:"tool_calls,omitempty"` - - // Tool Call Id - ToolCallId string `json:"tool_call_id,omitempty"` - - // Time the message was created, in unix seconds - Created int64 `json:"created,omitempty"` -} - -// Chat completion chunk -type MessageChunk struct { - Id string `json:"id,omitempty"` - Model string `json:"model,omitempty"` - Created int64 `json:"created,omitempty"` - SystemFingerprint string `json:"system_fingerprint,omitempty"` - TokenUsage *TokenUsage `json:"usage,omitempty"` - Choices []MessageChoice `json:"choices,omitempty"` -} - -// Token usage -type TokenUsage struct { - PromptTokens int `json:"prompt_tokens,omitempty"` - CompletionTokens int `json:"completion_tokens,omitempty"` - TotalTokens int `json:"total_tokens,omitempty"` -} - -// One choice of chat completion messages -type MessageChoice struct { - Message *Message `json:"message,omitempty"` - Delta *MessageDelta `json:"delta,omitempty"` - Index int `json:"index"` - FinishReason string `json:"finish_reason,omitempty"` -} - -// Delta between messages (for streaming responses) -type MessageDelta struct { - Role string `json:"role,omitempty"` - Content string `json:"content,omitempty"` -} - -// Message Content -type Content struct { - Id string `json:"id,omitempty"` - Type string `json:"type" writer:",width:4"` - Text string `json:"text,omitempty" writer:",width:60,wrap"` - Source *contentSource `json:"source,omitempty"` - Url *contentImage `json:"image_url,omitempty"` - - // Tool Function Call - toolUse - - // Tool Result - ToolId string `json:"tool_use_id,omitempty"` - Result string `json:"content,omitempty"` -} - -// Content Source -type contentSource struct { - Type string `json:"type"` - MediaType string `json:"media_type,omitempty"` - Data string `json:"data,omitempty"` -} - -// Image Source -type contentImage struct { - Url string `json:"url,omitempty"` - Detail string `json:"detail,omitempty"` -} - -// Tool Call -type ToolCall struct { - Id string `json:"id,omitempty"` - Type string `json:"type,omitempty"` - Function ToolFunction `json:"function,omitempty"` -} - -// Tool Function and Arguments -type ToolFunction struct { - Name string `json:"name,omitempty"` - Arguments string `json:"arguments,omitempty"` -} - -// Tool call -type toolUse struct { - Name string `json:"name,omitempty"` - Input map[string]any `json:"input,omitempty"` - Json string `json:"partial_json,omitempty"` // Used by anthropic -} - -// Tool result -type toolResult struct { - ToolId string `json:"tool_use_id,omitempty"` - Result string `json:"content,omitempty"` -} - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -// Create a new message, with optional content -func NewMessage(role string, content ...any) *Message { - message := new(Message) - message.Role = role - - // Append content to messages - if len(content) > 0 && message.Add(content...) == nil { - return nil - } - - // Return success - return message -} - -// Return a new content object of type text -func Text(v string) *Content { - return &Content{Type: "text", Text: v} -} - -// Return a new content object of type image, from a io.Reader -func Image(r io.Reader) (*Content, error) { - data, err := io.ReadAll(r) - if err != nil { - return nil, err - } - - mimetype := http.DetectContentType(data) - if !strings.HasPrefix(mimetype, "image/") { - return nil, ErrBadParameter.With("Image: not an image file") - } - - return &Content{Type: "image", Source: &contentSource{ - Type: "base64", - MediaType: mimetype, - Data: base64.StdEncoding.EncodeToString(data), - }}, nil -} - -// Return a new content object of type image, from a file -func ImageData(path string) (*Content, error) { - r, err := os.Open(path) - if err != nil { - return nil, err - } - defer r.Close() - return Image(r) -} - -// Return a new content object of type image, from a Url -func ImageUrl(v, detail string) (*Content, error) { - url, err := url.Parse(v) - if err != nil { - return nil, err - } - if url.Scheme != "https" { - return nil, ErrBadParameter.With("ImageUrl: not an https url") - } - return &Content{ - Type: "image_url", - Url: &contentImage{ - Url: url.String(), - Detail: detail, - }, - }, nil -} - -// Return tool usage -func ToolUse(t ToolCall) *Content { - var input map[string]any - - // Decode the arguments - if t.Function.Arguments != "" { - if err := json.Unmarshal([]byte(t.Function.Arguments), &input); err != nil { - return nil - } - } - - // Return the content - return &Content{ - Type: t.Type, - Id: t.Id, - toolUse: toolUse{ - Name: t.Function.Name, - Input: input, - }, - } -} - -// Return a tool result -func ToolResult(id string, result string) *Content { - return &Content{Type: "tool_result", ToolId: id, Result: result} -} - -/////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (m Message) String() string { - data, _ := json.MarshalIndent(m, "", " ") - return string(data) -} - -func (m MessageChoice) String() string { - data, _ := json.MarshalIndent(m, "", " ") - return string(data) -} - -func (m MessageChunk) String() string { - data, _ := json.MarshalIndent(m, "", " ") - return string(data) -} - -func (m MessageDelta) String() string { - data, _ := json.MarshalIndent(m, "", " ") - return string(data) -} -func (c Content) String() string { - data, _ := json.MarshalIndent(c, "", " ") - return string(data) -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -func (m *Message) IsValid() bool { - if m.Role == "" { - return false - } - return reflect.ValueOf(m.Content).IsValid() -} - -// Append content to the message -func (m *Message) Add(content ...any) *Message { - if len(content) == 0 { - return nil - } - for i := 0; i < len(content); i++ { - if err := m.append(content[i]); err != nil { - panic(err) - } - } - return m -} - -// Return an input parameter as a string, returns false if the name -// is incorrect or the input doesn't exist -func (c Content) GetString(name, input string) (string, bool) { - if c.Name == name { - if value, exists := c.Input[input]; exists { - if value, ok := value.(string); ok { - return value, true - } - } - } - return "", false -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -// Append message content -func (m *Message) append(v any) error { - // Set if no content - if m.Content == nil { - return m.set(v) - } - // Promote content to array - switch m.Content.(type) { - case string: - switch v := v.(type) { - case string: - // string, string => []string - m.Content = []string{m.Content.(string), v} - return nil - case Content: - // string, Content => []Content - m.Content = []Content{{Type: "text", Text: m.Content.(string)}, v} - return nil - case *Content: - // string, *Content => []Content - m.Content = []Content{{Type: "text", Text: m.Content.(string)}, *v} - return nil - } - case []string: - switch v := v.(type) { - case string: - // []string, string => []string - m.Content = append(m.Content.([]string), v) - return nil - } - case Content: - switch v := v.(type) { - case string: - // Content, string => []Content - m.Content = []Content{m.Content.(Content), {Type: "text", Text: v}} - return nil - case Content: - // Content, Content => []Content - m.Content = []Content{m.Content.(Content), v} - return nil - case *Content: - // Content, *Content => []Content - m.Content = []Content{m.Content.(Content), *v} - return nil - } - case []Content: - switch v := v.(type) { - case string: - // []Content, string => []Content - m.Content = append(m.Content.([]Content), Content{Type: "text", Text: v}) - return nil - case *Content: - // []Content, *Content => []Content - m.Content = append(m.Content.([]Content), *v) - return nil - case Content: - // []Content, Content => []Content - m.Content = append(m.Content.([]Content), v) - return nil - case []Content: - // []Content, []Content => []Content - m.Content = append(m.Content.([]Content), v...) - return nil - } - } - return ErrBadParameter.With("append: not implemented for ", reflect.TypeOf(m.Content), ",", reflect.TypeOf(v)) -} - -// Set the message content -func (m *Message) set(v any) error { - // Append content to messages, - // m.Content will be of type string, []string or []Content - switch v := v.(type) { - case string: - m.Content = v - case []string: - m.Content = v - case *Content: - m.Content = []Content{*v} - case Content: - m.Content = []Content{v} - case []*Content: - if len(v) > 0 { - m.Content = make([]Content, 0, len(v)) - for _, v := range v { - m.Content = append(m.Content.([]Content), *v) - } - } - case []Content: - if len(v) > 0 { - m.Content = make([]Content, 0, len(v)) - for _, v := range v { - m.Content = append(m.Content.([]Content), v) - } - } - default: - return ErrBadParameter.With("Add: not implemented for type", reflect.TypeOf(v)) - } - - // Return success - return nil -} diff --git a/pkg/openai/schema/message_test.go b/pkg/openai/schema/message_test.go deleted file mode 100644 index 3394114..0000000 --- a/pkg/openai/schema/message_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package schema_test - -import ( - "testing" - - "github.com/mutablelogic/go-client/pkg/openai/schema" - "github.com/stretchr/testify/assert" -) - -const ( - IMAGE1_PATH = "../../../etc/test/IMG_20130413_095348.JPG" - IMAGE2_PATH = "../../../etc/test/mu.png" -) - -func Test_message_001(t *testing.T) { - assert := assert.New(t) - message := schema.NewMessage("user") - if !assert.NotNil(message) { - t.SkipNow() - } - assert.Equal(false, message.IsValid()) -} - -func Test_message_002(t *testing.T) { - assert := assert.New(t) - message := schema.NewMessage("user", "text1") - if !assert.NotNil(message) { - t.SkipNow() - } - assert.Equal(true, message.IsValid()) - t.Log(message) -} - -func Test_message_003(t *testing.T) { - assert := assert.New(t) - message := schema.NewMessage("user", []string{"text1", "text2", "text3"}) - if !assert.NotNil(message) { - t.SkipNow() - } - assert.Equal(true, message.IsValid()) - t.Log(message) -} - -func Test_message_004(t *testing.T) { - assert := assert.New(t) - message := schema.NewMessage("user", schema.Text("text1")) - if !assert.NotNil(message) { - t.SkipNow() - } - assert.Equal(true, message.IsValid()) - t.Log(message) -} - -func Test_message_005(t *testing.T) { - assert := assert.New(t) - message := schema.NewMessage("user", []string{"text1", "text2", "text3"}) - if !assert.NotNil(message) { - t.SkipNow() - } - assert.Equal(true, message.IsValid()) - t.Log(message) -} - -func Test_message_006(t *testing.T) { - assert := assert.New(t) - message := schema.NewMessage("user", []*schema.Content{schema.Text("text1"), schema.Text("text2"), schema.Text("text3")}) - if !assert.NotNil(message) { - t.SkipNow() - } - assert.Equal(true, message.IsValid()) - t.Log(message) -} - -func Test_message_007(t *testing.T) { - assert := assert.New(t) - message := schema.NewMessage("user", []*schema.Content{}) - if !assert.NotNil(message) { - t.SkipNow() - } - assert.Equal(false, message.IsValid()) -} - -func Test_message_008(t *testing.T) { - assert := assert.New(t) - content, err := schema.ImageData(IMAGE2_PATH) - if !assert.NoError(err) { - t.SkipNow() - } - message := schema.NewMessage("user", []*schema.Content{content, schema.Text("Desscribe this image")}) - if !assert.NotNil(message) { - t.SkipNow() - } - assert.Equal(true, message.IsValid()) - t.Log(message) -} - -func Test_message_009(t *testing.T) { - assert := assert.New(t) - message := schema.NewMessage("user") - if !assert.NotNil(message) { - t.SkipNow() - } - assert.NotNil(message.Add("Hi")) - assert.NotNil(message.Add("There")) - assert.Equal(true, message.IsValid()) - t.Log(message) -} - -func Test_message_010(t *testing.T) { - assert := assert.New(t) - message := schema.NewMessage("user") - if !assert.NotNil(message) { - t.SkipNow() - } - assert.NotNil(message.Add("text1")) - assert.NotNil(message.Add(schema.Text("text2"), schema.Text("text3"))) - assert.Equal(true, message.IsValid()) - t.Log(message) -} - -func Test_message_011(t *testing.T) { - assert := assert.New(t) - message := schema.NewMessage("user", schema.Text("text1")) - if !assert.NotNil(message) { - t.SkipNow() - } - assert.NotNil(message.Add(schema.Text("text2"), schema.Text("text3"))) - assert.Equal(true, message.IsValid()) - t.Log(message) -} diff --git a/pkg/openai/schema/model.go b/pkg/openai/schema/model.go deleted file mode 100644 index b67ebda..0000000 --- a/pkg/openai/schema/model.go +++ /dev/null @@ -1,11 +0,0 @@ -package schema - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -// A model object -type Model struct { - Id string `json:"id" writer:",width:30"` - Created int64 `json:"created,omitempty"` - Owner string `json:"owned_by,omitempty"` -} diff --git a/pkg/openai/schema/tool.go b/pkg/openai/schema/tool.go deleted file mode 100644 index 6916ebe..0000000 --- a/pkg/openai/schema/tool.go +++ /dev/null @@ -1,158 +0,0 @@ -package schema - -import ( - "encoding/json" - "fmt" - "reflect" - - // Package imports - "github.com/djthorpe/go-tablewriter/pkg/meta" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -// A tool function -type Tool struct { - Name string `json:"name"` - Description string `json:"description"` - Type string `json:"type,omitempty"` - Parameters *toolParameters `json:"parameters,omitempty"` // Used by OpenAI, Mistral - InputSchema *toolParameters `json:"input_schema,omitempty"` // Used by anthropic -} - -// Tool function parameters -type toolParameters struct { - Type string `json:"type,omitempty"` - Properties map[string]toolParameter `json:"properties,omitempty"` - Required []string `json:"required,omitempty"` -} - -// Tool function call parameter -type toolParameter struct { - Name string `json:"-"` - Type string `json:"type"` - Enum []string `json:"enum,omitempty"` - Description string `json:"description"` -} - -/////////////////////////////////////////////////////////////////////////////// -// GLOBALS - -const ( - tagParameter = "json" -) - -var ( - typeString = reflect.TypeOf("") - typeBool = reflect.TypeOf(true) - typeInt = reflect.TypeOf(int(0)) -) - -/////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -func NewTool(name, description string) *Tool { - return &Tool{ - Name: name, - Description: description, - } -} - -func NewToolEx(name, description string, parameters any) (*Tool, error) { - t := NewTool(name, description) - if parameters == nil { - return t, nil - } - - // Get tool metadata - meta, err := meta.New(parameters, tagParameter) - if err != nil { - return nil, err - } - - // Iterate over fields, and add parameters - for _, field := range meta.Fields() { - if err := t.Add(field.Name(), field.Tag("description"), !field.Is("omitempty"), field.Type()); err != nil { - return nil, fmt.Errorf("field %q: %w", field.Name(), err) - } - } - - // Return the tool - return t, nil -} - -func NewToolResult(id string, result map[string]any) *Message { - var message Message - message.Role = "tool" - message.ToolCallId = id - - data, err := json.Marshal(result) - if err != nil { - return nil - } else { - message.Content = string(data) - } - - return &message -} - -/////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -func (t Tool) String() string { - data, _ := json.MarshalIndent(t, "", " ") - return string(data) -} - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -func (tool *Tool) Add(name, description string, required bool, t reflect.Type) error { - if name == "" { - return ErrBadParameter.With("missing name") - } - if tool.Parameters == nil { - tool.Parameters = &toolParameters{ - Type: "object", - Properties: make(map[string]toolParameter), - } - } - if _, exists := tool.Parameters.Properties[name]; exists { - return ErrDuplicateEntry.With(name) - } - typ, err := typeOf(t) - if err != nil { - return err - } - tool.Parameters.Properties[name] = toolParameter{ - Name: name, - Type: typ, - Description: description, - } - if required { - tool.Parameters.Required = append(tool.Parameters.Required, name) - } - - // Return success - return nil -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - -func typeOf(v reflect.Type) (string, error) { - switch v { - case typeString: - return "string", nil - case typeBool: - return "boolean", nil - case typeInt: - return "integer", nil - default: - return "", ErrBadParameter.Withf("unsupported type %q", v) - } -} diff --git a/pkg/openai/schema/tool_test.go b/pkg/openai/schema/tool_test.go deleted file mode 100644 index 2db1761..0000000 --- a/pkg/openai/schema/tool_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package schema_test - -import ( - "reflect" - "testing" - - "github.com/mutablelogic/go-client/pkg/openai/schema" - "github.com/stretchr/testify/assert" -) - -func Test_tool_001(t *testing.T) { - assert := assert.New(t) - tool := schema.NewTool("get_stock_price", "Get the current stock price for a given ticker symbol.") - assert.NotNil(tool) - assert.NoError(tool.Add("ticker", "The stock ticker symbol, e.g. AAPL for Apple Inc.", true, reflect.TypeOf(""))) - t.Log(tool) -} - -func Test_tool_002(t *testing.T) { - assert := assert.New(t) - tool, err := schema.NewToolEx("get_stock_price", "Get the current stock price for a given ticker symbol.", struct { - Ticker string `json:"ticker,omitempty" description:"The stock ticker symbol, e.g. AAPL for Apple Inc."` - }{}) - assert.NoError(err) - assert.NotNil(tool) - t.Log(tool) -} diff --git a/pkg/weatherapi/agent.go b/pkg/weatherapi/agent.go deleted file mode 100644 index 6be6429..0000000 --- a/pkg/weatherapi/agent.go +++ /dev/null @@ -1,215 +0,0 @@ -package weatherapi - -import ( - "context" - - // Packages - agent "github.com/mutablelogic/go-client/pkg/agent" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -/////////////////////////////////////////////////////////////////////////////// -// TYPES - -type tool struct { - name string - description string - params []agent.ToolParameter - run func(context.Context, *agent.ToolCall) (*agent.ToolResult, error) -} - -// Ensure tool satisfies the agent.Tool interface -var _ agent.Tool = (*tool)(nil) - -/////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -// Return all the agent tools for the weatherapi -func (c *Client) Tools() []agent.Tool { - return []agent.Tool{ - &tool{ - name: "current_weather", - description: "Return the current weather", - run: c.agentCurrentWeatherAuto, - }, &tool{ - name: "current_weather_city", - description: "Return the current weather for a city", - params: []agent.ToolParameter{ - {Name: "city", Description: "City name", Required: true}, - }, - run: c.agentCurrentWeatherCity, - }, &tool{ - name: "current_weather_zip", - description: "Return the current weather for a zipcode or postcode", - params: []agent.ToolParameter{ - {Name: "zip", Description: "Zipcode or Postcode", Required: true}, - }, - run: c.agentCurrentWeatherZipcode, - }, &tool{ - name: "weather_forecast", - description: "Return the weather forecast", - run: c.agentForecastWeatherAuto, - params: []agent.ToolParameter{ - {Name: "days", Description: "Number of days to forecast ahead", Required: true}, - }, - }, &tool{ - name: "weather_forecast_city", - description: "Return the weather forecast for a city", - run: c.agentForecastWeatherCity, - params: []agent.ToolParameter{ - {Name: "city", Description: "City name", Required: true}, - {Name: "days", Description: "Number of days to forecast ahead", Required: true}, - }, - }, - } -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - TOOL - -func (*tool) Provider() string { - return "weatherapi" -} - -func (t *tool) Name() string { - return t.name -} - -func (t *tool) Description() string { - return t.description -} - -func (t *tool) Params() []agent.ToolParameter { - return t.params -} - -func (t *tool) Run(ctx context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - return t.run(ctx, call) -} - -/////////////////////////////////////////////////////////////////////////////// -// PRIVATE METHODS - TOOL - -// Return the current weather -func (c *Client) agentCurrentWeatherAuto(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - response, err := c.Current("auto:ip") - if err != nil { - return nil, err - } - return &agent.ToolResult{ - Id: call.Id, - Result: map[string]any{ - "type": "text", - "location": response.Location, - "weather": response.Current, - }, - }, nil -} - -// Return the current weather in a specific city -func (c *Client) agentCurrentWeatherCity(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - city, ok := call.Args["city"].(string) - if !ok || city == "" { - return nil, ErrBadParameter.Withf("city is required") - } - response, err := c.Current(city) - if err != nil { - return nil, err - } - return &agent.ToolResult{ - Id: call.Id, - Result: map[string]any{ - "type": "text", - "location": response.Location, - "weather": response.Current, - }, - }, nil -} - -// Return the current weather for a zipcode -func (c *Client) agentCurrentWeatherZipcode(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - zip, ok := call.Args["zip"].(string) - if !ok || zip == "" { - return nil, ErrBadParameter.Withf("zipcode is required") - } - response, err := c.Current(zip) - if err != nil { - return nil, err - } - return &agent.ToolResult{ - Id: call.Id, - Result: map[string]any{ - "type": "text", - "location": response.Location, - "weather": response.Current, - }, - }, nil -} - -// Return the weather forecast -func (c *Client) agentForecastWeatherAuto(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - // Get days parameter - days, err := call.Int("days") - if err != nil { - return nil, err - } - - // Get response - response, err := c.Forecast("auto:ip", OptDays(days)) - if err != nil { - return nil, err - } - - // Get forecast by day - result := map[string]Day{} - for _, day := range response.Forecast.Day { - result[day.Date] = *day.Day - } - - return &agent.ToolResult{ - Id: call.Id, - Result: map[string]any{ - "type": "text", - "location": response.Location, - "days": result, - }, - }, nil -} - -// Return the weather forecast for a city -func (c *Client) agentForecastWeatherCity(_ context.Context, call *agent.ToolCall) (*agent.ToolResult, error) { - // Get city parameter - city, ok := call.Args["city"].(string) - if !ok || city == "" { - return nil, ErrBadParameter.Withf("city is required") - } - - // Get days parameter - days, err := call.Int("days") - if err != nil { - return nil, err - } - - // Get response - response, err := c.Forecast(city, OptDays(days)) - if err != nil { - return nil, err - } - - // Get forecast by day - result := map[string]Day{} - for _, day := range response.Forecast.Day { - result[day.Date] = *day.Day - } - - return &agent.ToolResult{ - Id: call.Id, - Result: map[string]any{ - "type": "text", - "location": response.Location, - "days": result, - }, - }, nil -}