Skip to content

Commit de37ad7

Browse files
committed
feat: add support for the new SDK server
This change introduces the fork/exec of the SDK server. Then all operations are requests to that server. The starting and stopping of this server is handled by creating/closing clients. Signed-off-by: Donnie Adams <[email protected]>
1 parent 07a3b27 commit de37ad7

10 files changed

+299
-360
lines changed

README.md

+49-16
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@ Additionally, you need the `gptscript` binary. You can install it on your system
1818

1919
## Client
2020

21-
There are currently a couple "global" options, and the client helps to manage those. A client without any options is
22-
likely what you want. However, here are the current global options:
23-
24-
- `gptscriptURL`: The URL (including `http(s)://) of an "SDK server" to use instead of the fork/exec model.
25-
- `gptscriptBin`: The path to a `gptscript` binary to use instead of the bundled one.
21+
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.
2622

2723
## Options
2824

@@ -32,7 +28,6 @@ None of the options is required, and the defaults will reduce the number of call
3228
- `cache`: Enable or disable caching. Default (true).
3329
- `cacheDir`: Specify the cache directory.
3430
- `quiet`: No output logging
35-
- `chdir`: Change current working directory
3631
- `subTool`: Use tool of this name, not the first tool
3732
- `input`: Input arguments for the tool run
3833
- `workspace`: Directory to use for the workspace, if specified it will not be deleted on exit
@@ -57,7 +52,11 @@ import (
5752
)
5853

5954
func listTools(ctx context.Context) (string, error) {
60-
client := gptscript.NewClient(gptscript.ClientOpts{})
55+
client, err := gptscript.NewClient()
56+
if err != nil {
57+
return "", err
58+
}
59+
defer client.Close()
6160
return client.ListTools(ctx)
6261
}
6362
```
@@ -78,7 +77,11 @@ import (
7877
)
7978

8079
func listModels(ctx context.Context) ([]string, error) {
81-
client := gptscript.NewClient(gptscript.ClientOpts{})
80+
client, err := gptscript.NewClient()
81+
if err != nil {
82+
return nil, err
83+
}
84+
defer client.Close()
8285
return client.ListModels(ctx)
8386
}
8487
```
@@ -97,7 +100,12 @@ import (
97100
)
98101

99102
func parse(ctx context.Context, fileName string) ([]gptscript.Node, error) {
100-
client := gptscript.NewClient(gptscript.ClientOpts{})
103+
client, err := gptscript.NewClient()
104+
if err != nil {
105+
return nil, err
106+
}
107+
defer client.Close()
108+
101109
return client.Parse(ctx, fileName)
102110
}
103111
```
@@ -116,7 +124,12 @@ import (
116124
)
117125

118126
func parseTool(ctx context.Context, contents string) ([]gptscript.Node, error) {
119-
client := gptscript.NewClient(gptscript.ClientOpts{})
127+
client, err := gptscript.NewClient()
128+
if err != nil {
129+
return nil, err
130+
}
131+
defer client.Close()
132+
120133
return client.ParseTool(ctx, contents)
121134
}
122135
```
@@ -135,7 +148,12 @@ import (
135148
)
136149

137150
func parse(ctx context.Context, nodes []gptscript.Node) (string, error) {
138-
client := gptscript.NewClient(gptscript.ClientOpts{})
151+
client, err := gptscript.NewClient()
152+
if err != nil {
153+
return "", err
154+
}
155+
defer client.Close()
156+
139157
return client.Fmt(ctx, nodes)
140158
}
141159
```
@@ -158,8 +176,13 @@ func runTool(ctx context.Context) (string, error) {
158176
Instructions: "who was the president of the united states in 1928?",
159177
}
160178

161-
client := gptscript.NewClient(gptscript.ClientOpts{})
162-
run, err := client.Evaluate(ctx, gptscript.Opts{}, t)
179+
client, err := gptscript.NewClient()
180+
if err != nil {
181+
return "", err
182+
}
183+
defer client.Close()
184+
185+
run, err := client.Evaluate(ctx, gptscript.Options{}, t)
163186
if err != nil {
164187
return "", err
165188
}
@@ -182,12 +205,17 @@ import (
182205
)
183206

184207
func runFile(ctx context.Context) (string, error) {
185-
opts := gptscript.Opts{
208+
opts := gptscript.Options{
186209
DisableCache: &[]bool{true}[0],
187210
Input: "--input hello",
188211
}
189212

190-
client := gptscript.NewClient(gptscript.ClientOpts{})
213+
client, err := gptscript.NewClient()
214+
if err != nil {
215+
return "", err
216+
}
217+
defer client.Close()
218+
191219
run, err := client.Run(ctx, "./hello.gpt", opts)
192220
if err != nil {
193221
return "", err
@@ -217,7 +245,12 @@ func streamExecTool(ctx context.Context) error {
217245
Input: "--input world",
218246
}
219247

220-
client := gptscript.NewClient(gptscript.ClientOpts{})
248+
client, err := gptscript.NewClient()
249+
if err != nil {
250+
return "", err
251+
}
252+
defer client.Close()
253+
221254
run, err := client.Run(ctx, "./hello.gpt", opts)
222255
if err != nil {
223256
return err

client.go

+106-50
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,107 @@ import (
55
"encoding/json"
66
"fmt"
77
"log/slog"
8+
"net/http"
89
"os"
10+
"os/exec"
911
"path/filepath"
1012
"strings"
13+
"sync"
14+
"time"
15+
)
16+
17+
var (
18+
serverProcess *exec.Cmd
19+
serverProcessCancel context.CancelFunc
20+
clientCount int
21+
lock sync.Mutex
1122
)
1223

1324
const relativeToBinaryPath = "<me>"
1425

15-
type ClientOpts struct {
16-
GPTScriptURL string
17-
GPTScriptBin string
26+
type Client interface {
27+
Run(context.Context, string, Options) (*Run, error)
28+
Evaluate(context.Context, Options, ...fmt.Stringer) (*Run, error)
29+
Parse(ctx context.Context, fileName string) ([]Node, error)
30+
ParseTool(ctx context.Context, toolDef string) ([]Node, error)
31+
Version(ctx context.Context) (string, error)
32+
Fmt(ctx context.Context, nodes []Node) (string, error)
33+
ListTools(ctx context.Context) (string, error)
34+
ListModels(ctx context.Context) ([]string, error)
35+
Close()
36+
}
37+
38+
type client struct {
39+
gptscriptURL string
1840
}
1941

20-
type Client struct {
21-
opts ClientOpts
42+
func NewClient() (Client, error) {
43+
lock.Lock()
44+
defer lock.Unlock()
45+
clientCount++
46+
47+
serverURL := os.Getenv("GPTSCRIPT_URL")
48+
if serverURL == "" {
49+
serverURL = "127.0.0.1:9090"
50+
}
51+
52+
if serverProcessCancel == nil && os.Getenv("GPTSCRIPT_DISABLE_SERVER") != "true" {
53+
var ctx context.Context
54+
ctx, serverProcessCancel = context.WithCancel(context.Background())
55+
56+
command := getCommand()
57+
serverProcess = exec.CommandContext(ctx, command, "--listen-address", serverURL, "clicky")
58+
if err := serverProcess.Start(); err != nil {
59+
serverProcessCancel()
60+
return nil, fmt.Errorf("failed to start server: %w", err)
61+
}
62+
63+
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
64+
defer cancel()
65+
if err := waitForServerReady(timeoutCtx, serverURL); err != nil {
66+
serverProcessCancel()
67+
_ = serverProcess.Wait()
68+
return nil, fmt.Errorf("failed to wait for gptscript to be ready: %w", err)
69+
}
70+
}
71+
return &client{gptscriptURL: "http://" + serverURL}, nil
2272
}
2373

24-
func NewClient(opts ClientOpts) *Client {
25-
c := &Client{opts: opts}
26-
c.complete()
27-
return c
74+
func waitForServerReady(ctx context.Context, serverURL string) error {
75+
for {
76+
resp, err := http.Get("http://" + serverURL + "/healthz")
77+
if err != nil {
78+
slog.DebugContext(ctx, "waiting for server to become ready")
79+
} else {
80+
_ = resp.Body.Close()
81+
82+
if resp.StatusCode == http.StatusOK {
83+
return nil
84+
}
85+
}
86+
87+
select {
88+
case <-ctx.Done():
89+
return ctx.Err()
90+
case <-time.After(time.Second):
91+
}
92+
}
2893
}
2994

30-
func (c *Client) complete() {
31-
if c.opts.GPTScriptBin == "" {
32-
c.opts.GPTScriptBin = getCommand()
95+
func (c *client) Close() {
96+
lock.Lock()
97+
defer lock.Unlock()
98+
clientCount--
99+
100+
if clientCount == 0 && serverProcessCancel != nil {
101+
serverProcessCancel()
102+
_ = serverProcess.Wait()
33103
}
34104
}
35105

36-
func (c *Client) Evaluate(ctx context.Context, opts Opts, tools ...fmt.Stringer) (*Run, error) {
106+
func (c *client) Evaluate(ctx context.Context, opts Options, tools ...fmt.Stringer) (*Run, error) {
37107
return (&Run{
38-
url: c.opts.GPTScriptURL,
39-
binPath: c.opts.GPTScriptBin,
108+
url: c.gptscriptURL,
40109
requestPath: "evaluate",
41110
state: Creating,
42111
opts: opts,
@@ -45,10 +114,9 @@ func (c *Client) Evaluate(ctx context.Context, opts Opts, tools ...fmt.Stringer)
45114
}).NextChat(ctx, opts.Input)
46115
}
47116

48-
func (c *Client) Run(ctx context.Context, toolPath string, opts Opts) (*Run, error) {
117+
func (c *client) Run(ctx context.Context, toolPath string, opts Options) (*Run, error) {
49118
return (&Run{
50-
url: c.opts.GPTScriptURL,
51-
binPath: c.opts.GPTScriptBin,
119+
url: c.gptscriptURL,
52120
requestPath: "run",
53121
state: Creating,
54122
opts: opts,
@@ -58,8 +126,8 @@ func (c *Client) Run(ctx context.Context, toolPath string, opts Opts) (*Run, err
58126
}
59127

60128
// Parse will parse the given file into an array of Nodes.
61-
func (c *Client) Parse(ctx context.Context, fileName string) ([]Node, error) {
62-
out, err := c.runBasicCommand(ctx, "parse", "parse", fileName, "")
129+
func (c *client) Parse(ctx context.Context, fileName string) ([]Node, error) {
130+
out, err := c.runBasicCommand(ctx, "parse", fileName, "")
63131
if err != nil {
64132
return nil, err
65133
}
@@ -73,8 +141,8 @@ func (c *Client) Parse(ctx context.Context, fileName string) ([]Node, error) {
73141
}
74142

75143
// ParseTool will parse the given string into a tool.
76-
func (c *Client) ParseTool(ctx context.Context, toolDef string) ([]Node, error) {
77-
out, err := c.runBasicCommand(ctx, "parse", "parse", "", toolDef)
144+
func (c *client) ParseTool(ctx context.Context, toolDef string) ([]Node, error) {
145+
out, err := c.runBasicCommand(ctx, "parse", "", toolDef)
78146
if err != nil {
79147
return nil, err
80148
}
@@ -88,29 +156,23 @@ func (c *Client) ParseTool(ctx context.Context, toolDef string) ([]Node, error)
88156
}
89157

90158
// Fmt will format the given nodes into a string.
91-
func (c *Client) Fmt(ctx context.Context, nodes []Node) (string, error) {
159+
func (c *client) Fmt(ctx context.Context, nodes []Node) (string, error) {
92160
b, err := json.Marshal(Document{Nodes: nodes})
93161
if err != nil {
94162
return "", fmt.Errorf("failed to marshal nodes: %w", err)
95163
}
96164

97165
run := &runSubCommand{
98166
Run: Run{
99-
url: c.opts.GPTScriptURL,
100-
binPath: c.opts.GPTScriptBin,
167+
url: c.gptscriptURL,
101168
requestPath: "fmt",
102169
state: Creating,
103170
toolPath: "",
104171
content: string(b),
105172
},
106173
}
107174

108-
if run.url != "" {
109-
err = run.request(ctx, Document{Nodes: nodes})
110-
} else {
111-
err = run.exec(ctx, "fmt")
112-
}
113-
if err != nil {
175+
if err = run.request(ctx, Document{Nodes: nodes}); err != nil {
114176
return "", err
115177
}
116178

@@ -126,8 +188,8 @@ func (c *Client) Fmt(ctx context.Context, nodes []Node) (string, error) {
126188
}
127189

128190
// Version will return the output of `gptscript --version`
129-
func (c *Client) Version(ctx context.Context) (string, error) {
130-
out, err := c.runBasicCommand(ctx, "--version", "version", "", "")
191+
func (c *client) Version(ctx context.Context) (string, error) {
192+
out, err := c.runBasicCommand(ctx, "version", "", "")
131193
if err != nil {
132194
return "", err
133195
}
@@ -136,8 +198,8 @@ func (c *Client) Version(ctx context.Context) (string, error) {
136198
}
137199

138200
// ListTools will list all the available tools.
139-
func (c *Client) ListTools(ctx context.Context) (string, error) {
140-
out, err := c.runBasicCommand(ctx, "--list-tools", "list-tools", "", "")
201+
func (c *client) ListTools(ctx context.Context) (string, error) {
202+
out, err := c.runBasicCommand(ctx, "list-tools", "", "")
141203
if err != nil {
142204
return "", err
143205
}
@@ -146,38 +208,32 @@ func (c *Client) ListTools(ctx context.Context) (string, error) {
146208
}
147209

148210
// ListModels will list all the available models.
149-
func (c *Client) ListModels(ctx context.Context) ([]string, error) {
150-
out, err := c.runBasicCommand(ctx, "--list-models", "list-models", "", "")
211+
func (c *client) ListModels(ctx context.Context) ([]string, error) {
212+
out, err := c.runBasicCommand(ctx, "list-models", "", "")
151213
if err != nil {
152214
return nil, err
153215
}
154216

155217
return strings.Split(strings.TrimSpace(out), "\n"), nil
156218
}
157219

158-
func (c *Client) runBasicCommand(ctx context.Context, command, requestPath, toolPath, content string) (string, error) {
220+
func (c *client) runBasicCommand(ctx context.Context, requestPath, toolPath, content string) (string, error) {
159221
run := &runSubCommand{
160222
Run: Run{
161-
url: c.opts.GPTScriptURL,
162-
binPath: c.opts.GPTScriptBin,
223+
url: c.gptscriptURL,
163224
requestPath: requestPath,
164225
state: Creating,
165226
toolPath: toolPath,
166227
content: content,
167228
},
168229
}
169230

170-
var err error
171-
if run.url != "" {
172-
var m any
173-
if content != "" || toolPath != "" {
174-
m = map[string]any{"content": content, "file": toolPath}
175-
}
176-
err = run.request(ctx, m)
177-
} else {
178-
err = run.exec(ctx, command)
231+
var m any
232+
if content != "" || toolPath != "" {
233+
m = map[string]any{"content": content, "file": toolPath}
179234
}
180-
if err != nil {
235+
236+
if err := run.request(ctx, m); err != nil {
181237
return "", err
182238
}
183239

0 commit comments

Comments
 (0)