diff --git a/README.md b/README.md index 25ec783..07b9f07 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ To use the module, you need to first set the OPENAI_API_KEY environment variable Additionally, you need the `gptscript` binary. You can install it on your system using the [installation instructions](https://github.com/gptscript-ai/gptscript?tab=readme-ov-file#1-install-the-latest-release). The binary can be on the PATH, or the `GPTSCRIPT_BIN` environment variable can be used to specify its location. -## Client +## GPTScript -The client allows the caller to run gptscript files, tools, and other operations (see below). There are currently no options for this client, so calling `NewClient()` is all you need. Although, the intention is that a single client is all you need for the life of your application, you should call `Close()` on the client when you are done. +The GPTScript instance allows the caller to run gptscript files, tools, and other operations (see below). There are currently no options for this instance, so calling `NewGPTScript()` is all you need. Although, the intention is that a single GPTScript instance is all you need for the life of your application, you should call `Close()` on the instance when you are done. ## Options @@ -54,12 +54,12 @@ import ( ) func listTools(ctx context.Context) (string, error) { - client, err := gptscript.NewClient() + g, err := gptscript.NewGPTScript() if err != nil { return "", err } - defer client.Close() - return client.ListTools(ctx) + defer g.Close() + return g.ListTools(ctx) } ``` @@ -79,12 +79,12 @@ import ( ) func listModels(ctx context.Context) ([]string, error) { - client, err := gptscript.NewClient() + g, err := gptscript.NewGPTScript() if err != nil { return nil, err } - defer client.Close() - return client.ListModels(ctx) + defer g.Close() + return g.ListModels(ctx) } ``` @@ -102,13 +102,13 @@ import ( ) func parse(ctx context.Context, fileName string) ([]gptscript.Node, error) { - client, err := gptscript.NewClient() + g, err := gptscript.NewGPTScript() if err != nil { return nil, err } - defer client.Close() + defer g.Close() - return client.Parse(ctx, fileName) + return g.Parse(ctx, fileName) } ``` @@ -126,13 +126,13 @@ import ( ) func parseTool(ctx context.Context, contents string) ([]gptscript.Node, error) { - client, err := gptscript.NewClient() + g, err := gptscript.NewGPTScript() if err != nil { return nil, err } - defer client.Close() + defer g.Close() - return client.ParseTool(ctx, contents) + return g.ParseTool(ctx, contents) } ``` @@ -150,13 +150,13 @@ import ( ) func parse(ctx context.Context, nodes []gptscript.Node) (string, error) { - client, err := gptscript.NewClient() + g, err := gptscript.NewGPTScript() if err != nil { return "", err } - defer client.Close() + defer g.Close() - return client.Fmt(ctx, nodes) + return g.Fmt(ctx, nodes) } ``` @@ -178,13 +178,13 @@ func runTool(ctx context.Context) (string, error) { Instructions: "who was the president of the united states in 1928?", } - client, err := gptscript.NewClient() + g, err := gptscript.NewGPTScript() if err != nil { return "", err } - defer client.Close() + defer g.Close() - run, err := client.Evaluate(ctx, gptscript.Options{}, t) + run, err := g.Evaluate(ctx, gptscript.Options{}, t) if err != nil { return "", err } @@ -212,13 +212,13 @@ func runFile(ctx context.Context) (string, error) { Input: "--input hello", } - client, err := gptscript.NewClient() + g, err := gptscript.NewGPTScript() if err != nil { return "", err } - defer client.Close() + defer g.Close() - run, err := client.Run(ctx, "./hello.gpt", opts) + run, err := g.Run(ctx, "./hello.gpt", opts) if err != nil { return "", err } @@ -247,13 +247,13 @@ func streamExecTool(ctx context.Context) error { Input: "--input world", } - client, err := gptscript.NewClient() + g, err := gptscript.NewGPTScript() if err != nil { return "", err } - defer client.Close() + defer g.Close() - run, err := client.Run(ctx, "./hello.gpt", opts) + run, err := g.Run(ctx, "./hello.gpt", opts) if err != nil { return err } @@ -288,13 +288,13 @@ func runFileWithConfirm(ctx context.Context) (string, error) { IncludeEvents: true, } - client, err := gptscript.NewClient() + g, err := gptscript.NewGPTScript() if err != nil { return "", err } - defer client.Close() + defer g.Close() - run, err := client.Run(ctx, "./hello.gpt", opts) + run, err := g.Run(ctx, "./hello.gpt", opts) if err != nil { return "", err } @@ -304,7 +304,7 @@ func runFileWithConfirm(ctx context.Context) (string, error) { // event.Tool has the information on the command being run. // and event.Input will have the input to the command being run. - err = client.Confirm(ctx, gptscript.AuthResponse{ + err = g.Confirm(ctx, gptscript.AuthResponse{ ID: event.ID, Accept: true, // Or false if not allowed. Message: "", // A message explaining why the command is not allowed (ignored if allowed). @@ -342,13 +342,13 @@ func runFileWithPrompt(ctx context.Context) (string, error) { IncludeEvents: true, } - client, err := gptscript.NewClient() + g, err := gptscript.NewGPTScript() if err != nil { return "", err } - defer client.Close() + defer g.Close() - run, err := client.Run(ctx, "./hello.gpt", opts) + run, err := g.Run(ctx, "./hello.gpt", opts) if err != nil { return "", err } @@ -357,7 +357,7 @@ func runFileWithPrompt(ctx context.Context) (string, error) { if event.Prompt != nil { // event.Prompt has the information to prompt the user. - err = client.PromptResponse(ctx, gptscript.PromptResponse{ + err = g.PromptResponse(ctx, gptscript.PromptResponse{ ID: event.Prompt.ID, // Responses is a map[string]string of Fields to values Responses: map[string]string{ diff --git a/client.go b/gptscript.go similarity index 66% rename from client.go rename to gptscript.go index 957b0fc..9c8f118 100644 --- a/client.go +++ b/gptscript.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "log/slog" + "net" "net/http" "os" "os/exec" @@ -18,13 +19,14 @@ import ( var ( serverProcess *exec.Cmd serverProcessCancel context.CancelFunc - clientCount int + gptscriptCount int + serverURL string lock sync.Mutex ) const relativeToBinaryPath = "" -type Client interface { +type GPTScript interface { Run(context.Context, string, Options) (*Run, error) Evaluate(context.Context, Options, ...fmt.Stringer) (*Run, error) Parse(context.Context, string) ([]Node, error) @@ -38,21 +40,31 @@ type Client interface { Close() } -type client struct { - gptscriptURL string +type gptscript struct { + url string } -func NewClient() (Client, error) { +func NewGPTScript() (GPTScript, error) { lock.Lock() defer lock.Unlock() - clientCount++ - - serverURL := os.Getenv("GPTSCRIPT_URL") - if serverURL == "" { - serverURL = "127.0.0.1:9090" - } + gptscriptCount++ if serverProcessCancel == nil && os.Getenv("GPTSCRIPT_DISABLE_SERVER") != "true" { + serverURL = os.Getenv("GPTSCRIPT_URL") + if serverURL == "" { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + slog.Debug("failed to start gptscript listener", "err", err) + return nil, fmt.Errorf("failed to start gptscript: %w", err) + } + + serverURL = l.Addr().String() + if err = l.Close(); err != nil { + slog.Debug("failed to close gptscript listener", "err", err) + return nil, fmt.Errorf("failed to start gptscript: %w", err) + } + } + ctx, cancel := context.WithCancel(context.Background()) in, _ := io.Pipe() @@ -77,7 +89,7 @@ func NewClient() (Client, error) { return nil, fmt.Errorf("failed to wait for gptscript to be ready: %w", err) } } - return &client{gptscriptURL: "http://" + serverURL}, nil + return &gptscript{url: "http://" + serverURL}, nil } func waitForServerReady(ctx context.Context, serverURL string) error { @@ -101,20 +113,20 @@ func waitForServerReady(ctx context.Context, serverURL string) error { } } -func (c *client) Close() { +func (g *gptscript) Close() { lock.Lock() defer lock.Unlock() - clientCount-- + gptscriptCount-- - if clientCount == 0 && serverProcessCancel != nil { + if gptscriptCount == 0 && serverProcessCancel != nil { serverProcessCancel() _ = serverProcess.Wait() } } -func (c *client) Evaluate(ctx context.Context, opts Options, tools ...fmt.Stringer) (*Run, error) { +func (g *gptscript) Evaluate(ctx context.Context, opts Options, tools ...fmt.Stringer) (*Run, error) { return (&Run{ - url: c.gptscriptURL, + url: g.url, requestPath: "evaluate", state: Creating, opts: opts, @@ -122,9 +134,9 @@ func (c *client) Evaluate(ctx context.Context, opts Options, tools ...fmt.String }).NextChat(ctx, opts.Input) } -func (c *client) Run(ctx context.Context, toolPath string, opts Options) (*Run, error) { +func (g *gptscript) Run(ctx context.Context, toolPath string, opts Options) (*Run, error) { return (&Run{ - url: c.gptscriptURL, + url: g.url, requestPath: "run", state: Creating, opts: opts, @@ -133,8 +145,8 @@ func (c *client) Run(ctx context.Context, toolPath string, opts Options) (*Run, } // Parse will parse the given file into an array of Nodes. -func (c *client) Parse(ctx context.Context, fileName string) ([]Node, error) { - out, err := c.runBasicCommand(ctx, "parse", map[string]any{"file": fileName}) +func (g *gptscript) Parse(ctx context.Context, fileName string) ([]Node, error) { + out, err := g.runBasicCommand(ctx, "parse", map[string]any{"file": fileName}) if err != nil { return nil, err } @@ -148,8 +160,8 @@ func (c *client) Parse(ctx context.Context, fileName string) ([]Node, error) { } // ParseTool will parse the given string into a tool. -func (c *client) ParseTool(ctx context.Context, toolDef string) ([]Node, error) { - out, err := c.runBasicCommand(ctx, "parse", map[string]any{"content": toolDef}) +func (g *gptscript) ParseTool(ctx context.Context, toolDef string) ([]Node, error) { + out, err := g.runBasicCommand(ctx, "parse", map[string]any{"content": toolDef}) if err != nil { return nil, err } @@ -163,8 +175,8 @@ func (c *client) ParseTool(ctx context.Context, toolDef string) ([]Node, error) } // Fmt will format the given nodes into a string. -func (c *client) Fmt(ctx context.Context, nodes []Node) (string, error) { - out, err := c.runBasicCommand(ctx, "fmt", Document{Nodes: nodes}) +func (g *gptscript) Fmt(ctx context.Context, nodes []Node) (string, error) { + out, err := g.runBasicCommand(ctx, "fmt", Document{Nodes: nodes}) if err != nil { return "", err } @@ -173,8 +185,8 @@ func (c *client) Fmt(ctx context.Context, nodes []Node) (string, error) { } // Version will return the output of `gptscript --version` -func (c *client) Version(ctx context.Context) (string, error) { - out, err := c.runBasicCommand(ctx, "version", nil) +func (g *gptscript) Version(ctx context.Context) (string, error) { + out, err := g.runBasicCommand(ctx, "version", nil) if err != nil { return "", err } @@ -183,8 +195,8 @@ func (c *client) Version(ctx context.Context) (string, error) { } // ListTools will list all the available tools. -func (c *client) ListTools(ctx context.Context) (string, error) { - out, err := c.runBasicCommand(ctx, "list-tools", nil) +func (g *gptscript) ListTools(ctx context.Context) (string, error) { + out, err := g.runBasicCommand(ctx, "list-tools", nil) if err != nil { return "", err } @@ -193,8 +205,8 @@ func (c *client) ListTools(ctx context.Context) (string, error) { } // ListModels will list all the available models. -func (c *client) ListModels(ctx context.Context) ([]string, error) { - out, err := c.runBasicCommand(ctx, "list-models", nil) +func (g *gptscript) ListModels(ctx context.Context) ([]string, error) { + out, err := g.runBasicCommand(ctx, "list-models", nil) if err != nil { return nil, err } @@ -202,19 +214,19 @@ func (c *client) ListModels(ctx context.Context) ([]string, error) { return strings.Split(strings.TrimSpace(out), "\n"), nil } -func (c *client) Confirm(ctx context.Context, resp AuthResponse) error { - _, err := c.runBasicCommand(ctx, "confirm/"+resp.ID, resp) +func (g *gptscript) Confirm(ctx context.Context, resp AuthResponse) error { + _, err := g.runBasicCommand(ctx, "confirm/"+resp.ID, resp) return err } -func (c *client) PromptResponse(ctx context.Context, resp PromptResponse) error { - _, err := c.runBasicCommand(ctx, "prompt-response/"+resp.ID, resp.Responses) +func (g *gptscript) PromptResponse(ctx context.Context, resp PromptResponse) error { + _, err := g.runBasicCommand(ctx, "prompt-response/"+resp.ID, resp.Responses) return err } -func (c *client) runBasicCommand(ctx context.Context, requestPath string, body any) (string, error) { +func (g *gptscript) runBasicCommand(ctx context.Context, requestPath string, body any) (string, error) { run := &Run{ - url: c.gptscriptURL, + url: g.url, requestPath: requestPath, state: Creating, basicCommand: true, diff --git a/client_test.go b/gptscript_test.go similarity index 90% rename from client_test.go rename to gptscript_test.go index 08bfb4f..65e7c6c 100644 --- a/client_test.go +++ b/gptscript_test.go @@ -12,7 +12,7 @@ import ( "github.com/getkin/kin-openapi/openapi3" ) -var c Client +var g GPTScript func TestMain(m *testing.M) { if os.Getenv("OPENAI_API_KEY") == "" && os.Getenv("GPTSCRIPT_URL") == "" { @@ -20,18 +20,35 @@ func TestMain(m *testing.M) { } var err error - c, err = NewClient() + g, err = NewGPTScript() if err != nil { - panic(fmt.Sprintf("error creating client: %s", err)) + panic(fmt.Sprintf("error creating gptscript: %s", err)) } exitCode := m.Run() - c.Close() + g.Close() os.Exit(exitCode) } +func TestCreateAnotherGPTScript(t *testing.T) { + g, err := NewGPTScript() + if err != nil { + t.Errorf("error creating gptscript: %s", err) + } + defer g.Close() + + version, err := g.Version(context.Background()) + if err != nil { + t.Errorf("error getting version from second gptscript: %s", err) + } + + if !strings.Contains(version, "gptscript version") { + t.Errorf("unexpected gptscript version: %s", version) + } +} + func TestVersion(t *testing.T) { - out, err := c.Version(context.Background()) + out, err := g.Version(context.Background()) if err != nil { t.Errorf("Error getting version: %v", err) } @@ -42,7 +59,7 @@ func TestVersion(t *testing.T) { } func TestListTools(t *testing.T) { - tools, err := c.ListTools(context.Background()) + tools, err := g.ListTools(context.Background()) if err != nil { t.Errorf("Error listing tools: %v", err) } @@ -53,7 +70,7 @@ func TestListTools(t *testing.T) { } func TestListModels(t *testing.T) { - models, err := c.ListModels(context.Background()) + models, err := g.ListModels(context.Background()) if err != nil { t.Errorf("Error listing models: %v", err) } @@ -66,7 +83,7 @@ func TestListModels(t *testing.T) { func TestAbortRun(t *testing.T) { tool := &ToolDef{Instructions: "What is the capital of the united states?"} - run, err := c.Evaluate(context.Background(), Options{DisableCache: true, IncludeEvents: true}, tool) + run, err := g.Evaluate(context.Background(), Options{DisableCache: true, IncludeEvents: true}, tool) if err != nil { t.Errorf("Error executing tool: %v", err) } @@ -90,7 +107,7 @@ func TestAbortRun(t *testing.T) { func TestSimpleEvaluate(t *testing.T) { tool := &ToolDef{Instructions: "What is the capital of the united states?"} - run, err := c.Evaluate(context.Background(), Options{}, tool) + run, err := g.Evaluate(context.Background(), Options{}, tool) if err != nil { t.Errorf("Error executing tool: %v", err) } @@ -132,7 +149,7 @@ func TestEvaluateWithContext(t *testing.T) { }, } - run, err := c.Evaluate(context.Background(), Options{DisableCache: true, IncludeEvents: true}, tool) + run, err := g.Evaluate(context.Background(), Options{DisableCache: true, IncludeEvents: true}, tool) if err != nil { t.Errorf("Error executing tool: %v", err) } @@ -165,7 +182,7 @@ the response should be in JSON and match the format: `, } - run, err := c.Evaluate(context.Background(), Options{DisableCache: true}, tool) + run, err := g.Evaluate(context.Background(), Options{DisableCache: true}, tool) if err != nil { t.Errorf("Error executing tool: %v", err) } @@ -201,7 +218,7 @@ func TestEvaluateWithToolList(t *testing.T) { }, } - run, err := c.Evaluate(context.Background(), Options{}, tools...) + run, err := g.Evaluate(context.Background(), Options{}, tools...) if err != nil { t.Errorf("Error executing tool: %v", err) } @@ -242,7 +259,7 @@ func TestEvaluateWithToolListAndSubTool(t *testing.T) { }, } - run, err := c.Evaluate(context.Background(), Options{SubTool: "other"}, tools...) + run, err := g.Evaluate(context.Background(), Options{SubTool: "other"}, tools...) if err != nil { t.Errorf("Error executing tool: %v", err) } @@ -261,7 +278,7 @@ func TestStreamEvaluate(t *testing.T) { var eventContent string tool := &ToolDef{Instructions: "What is the capital of the united states?"} - run, err := c.Evaluate(context.Background(), Options{IncludeEvents: true}, tool) + run, err := g.Evaluate(context.Background(), Options{IncludeEvents: true}, tool) if err != nil { t.Fatalf("Error executing tool: %v", err) } @@ -299,7 +316,7 @@ func TestStreamRun(t *testing.T) { } var eventContent string - run, err := c.Run(context.Background(), wd+"/test/catcher.gpt", Options{IncludeEvents: true}) + run, err := g.Run(context.Background(), wd+"/test/catcher.gpt", Options{IncludeEvents: true}) if err != nil { t.Fatalf("Error executing file: %v", err) } @@ -336,7 +353,7 @@ func TestParseSimpleFile(t *testing.T) { t.Fatalf("Error getting working directory: %v", err) } - tools, err := c.Parse(context.Background(), wd+"/test/test.gpt") + tools, err := g.Parse(context.Background(), wd+"/test/test.gpt") if err != nil { t.Errorf("Error parsing file: %v", err) } @@ -355,7 +372,7 @@ func TestParseSimpleFile(t *testing.T) { } func TestParseTool(t *testing.T) { - tools, err := c.ParseTool(context.Background(), "echo hello") + tools, err := g.ParseTool(context.Background(), "echo hello") if err != nil { t.Errorf("Error parsing tool: %v", err) } @@ -374,7 +391,7 @@ func TestParseTool(t *testing.T) { } func TestParseToolWithTextNode(t *testing.T) { - tools, err := c.ParseTool(context.Background(), "echo hello\n---\n!markdown\nhello") + tools, err := g.ParseTool(context.Background(), "echo hello\n---\n!markdown\nhello") if err != nil { t.Errorf("Error parsing tool: %v", err) } @@ -435,7 +452,7 @@ func TestFmt(t *testing.T) { }, } - out, err := c.Fmt(context.Background(), nodes) + out, err := g.Fmt(context.Background(), nodes) if err != nil { t.Errorf("Error formatting nodes: %v", err) } @@ -446,7 +463,7 @@ echo hello there --- Name: echo -Args: input: The string input to echo +Parameter: input: The string input to echo #!/bin/bash echo hello there @@ -495,7 +512,7 @@ func TestFmtWithTextNode(t *testing.T) { }, } - out, err := c.Fmt(context.Background(), nodes) + out, err := g.Fmt(context.Background(), nodes) if err != nil { t.Errorf("Error formatting nodes: %v", err) } @@ -509,7 +526,7 @@ echo hello there We now echo hello there --- Name: echo -Args: input: The string input to echo +Parameter: input: The string input to echo #!/bin/bash echo hello there @@ -525,7 +542,7 @@ func TestToolChat(t *testing.T) { Tools: []string{"sys.chat.finish"}, } - run, err := c.Evaluate(context.Background(), Options{DisableCache: true}, tool) + run, err := g.Evaluate(context.Background(), Options{DisableCache: true}, tool) if err != nil { t.Fatalf("Error executing tool: %v", err) } @@ -571,7 +588,7 @@ func TestFileChat(t *testing.T) { t.Fatalf("Error getting current working directory: %v", err) } - run, err := c.Run(context.Background(), wd+"/test/chat.gpt", Options{}) + run, err := g.Run(context.Background(), wd+"/test/chat.gpt", Options{}) if err != nil { t.Fatalf("Error executing tool: %v", err) } @@ -620,7 +637,7 @@ func TestToolWithGlobalTools(t *testing.T) { var eventContent string - run, err := c.Run(context.Background(), wd+"/test/global-tools.gpt", Options{DisableCache: true, IncludeEvents: true}) + run, err := g.Run(context.Background(), wd+"/test/global-tools.gpt", Options{DisableCache: true, IncludeEvents: true}) if err != nil { t.Fatalf("Error executing tool: %v", err) } @@ -678,7 +695,7 @@ func TestConfirm(t *testing.T) { }, } - run, err := c.Evaluate(context.Background(), Options{IncludeEvents: true, Confirm: true}, tools...) + run, err := g.Evaluate(context.Background(), Options{IncludeEvents: true, Confirm: true}, tools...) if err != nil { t.Errorf("Error executing tool: %v", err) } @@ -706,7 +723,7 @@ func TestConfirm(t *testing.T) { t.Errorf("unexpected confirm input: %s", confirmCallEvent.Input) } - if err = c.Confirm(context.Background(), AuthResponse{ + if err = g.Confirm(context.Background(), AuthResponse{ ID: confirmCallEvent.ID, Accept: true, }); err != nil { @@ -749,7 +766,7 @@ func TestConfirmDeny(t *testing.T) { }, } - run, err := c.Evaluate(context.Background(), Options{IncludeEvents: true, Confirm: true}, tools...) + run, err := g.Evaluate(context.Background(), Options{IncludeEvents: true, Confirm: true}, tools...) if err != nil { t.Errorf("Error executing tool: %v", err) } @@ -777,7 +794,7 @@ func TestConfirmDeny(t *testing.T) { t.Errorf("unexpected confirm input: %s", confirmCallEvent.Input) } - if err = c.Confirm(context.Background(), AuthResponse{ + if err = g.Confirm(context.Background(), AuthResponse{ ID: confirmCallEvent.ID, Accept: false, Message: "I will not allow it!", @@ -821,7 +838,7 @@ func TestPrompt(t *testing.T) { }, } - run, err := c.Evaluate(context.Background(), Options{IncludeEvents: true, Prompt: true}, tools...) + run, err := g.Evaluate(context.Background(), Options{IncludeEvents: true, Prompt: true}, tools...) if err != nil { t.Errorf("Error executing tool: %v", err) } @@ -862,7 +879,7 @@ func TestPrompt(t *testing.T) { t.Errorf("Unexpected field: %s", promptFrame.Fields[0]) } - if err = c.PromptResponse(context.Background(), PromptResponse{ + if err = g.PromptResponse(context.Background(), PromptResponse{ ID: promptFrame.ID, Responses: map[string]string{promptFrame.Fields[0]: "Clicky"}, }); err != nil { @@ -904,7 +921,7 @@ func TestPromptWithoutPromptAllowed(t *testing.T) { }, } - run, err := c.Evaluate(context.Background(), Options{IncludeEvents: true}, tools...) + run, err := g.Evaluate(context.Background(), Options{IncludeEvents: true}, tools...) if err != nil { t.Errorf("Error executing tool: %v", err) } diff --git a/run_test.go b/run_test.go index b48283f..3ebc47c 100644 --- a/run_test.go +++ b/run_test.go @@ -20,7 +20,7 @@ func TestRestartingErrorRun(t *testing.T) { Instructions: instructions, } - run, err := c.Evaluate(context.Background(), Options{Env: []string{"EXIT_CODE=1"}, IncludeEvents: true}, tool, contextTool) + run, err := g.Evaluate(context.Background(), Options{Env: []string{"EXIT_CODE=1"}, IncludeEvents: true}, tool, contextTool) if err != nil { t.Errorf("Error executing tool: %v", err) }