@@ -5,38 +5,107 @@ import (
5
5
"encoding/json"
6
6
"fmt"
7
7
"log/slog"
8
+ "net/http"
8
9
"os"
10
+ "os/exec"
9
11
"path/filepath"
10
12
"strings"
13
+ "sync"
14
+ "time"
15
+ )
16
+
17
+ var (
18
+ serverProcess * exec.Cmd
19
+ serverProcessCancel context.CancelFunc
20
+ clientCount int
21
+ lock sync.Mutex
11
22
)
12
23
13
24
const relativeToBinaryPath = "<me>"
14
25
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
18
40
}
19
41
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
22
72
}
23
73
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
+ }
28
93
}
29
94
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 ()
33
103
}
34
104
}
35
105
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 ) {
37
107
return (& Run {
38
- url : c .opts .GPTScriptURL ,
39
- binPath : c .opts .GPTScriptBin ,
108
+ url : c .gptscriptURL ,
40
109
requestPath : "evaluate" ,
41
110
state : Creating ,
42
111
opts : opts ,
@@ -45,10 +114,9 @@ func (c *Client) Evaluate(ctx context.Context, opts Opts, tools ...fmt.Stringer)
45
114
}).NextChat (ctx , opts .Input )
46
115
}
47
116
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 ) {
49
118
return (& Run {
50
- url : c .opts .GPTScriptURL ,
51
- binPath : c .opts .GPTScriptBin ,
119
+ url : c .gptscriptURL ,
52
120
requestPath : "run" ,
53
121
state : Creating ,
54
122
opts : opts ,
@@ -58,8 +126,8 @@ func (c *Client) Run(ctx context.Context, toolPath string, opts Opts) (*Run, err
58
126
}
59
127
60
128
// 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 , "" )
63
131
if err != nil {
64
132
return nil , err
65
133
}
@@ -73,8 +141,8 @@ func (c *Client) Parse(ctx context.Context, fileName string) ([]Node, error) {
73
141
}
74
142
75
143
// 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 )
78
146
if err != nil {
79
147
return nil , err
80
148
}
@@ -88,29 +156,23 @@ func (c *Client) ParseTool(ctx context.Context, toolDef string) ([]Node, error)
88
156
}
89
157
90
158
// 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 ) {
92
160
b , err := json .Marshal (Document {Nodes : nodes })
93
161
if err != nil {
94
162
return "" , fmt .Errorf ("failed to marshal nodes: %w" , err )
95
163
}
96
164
97
165
run := & runSubCommand {
98
166
Run : Run {
99
- url : c .opts .GPTScriptURL ,
100
- binPath : c .opts .GPTScriptBin ,
167
+ url : c .gptscriptURL ,
101
168
requestPath : "fmt" ,
102
169
state : Creating ,
103
170
toolPath : "" ,
104
171
content : string (b ),
105
172
},
106
173
}
107
174
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 {
114
176
return "" , err
115
177
}
116
178
@@ -126,8 +188,8 @@ func (c *Client) Fmt(ctx context.Context, nodes []Node) (string, error) {
126
188
}
127
189
128
190
// 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" , "" , "" )
131
193
if err != nil {
132
194
return "" , err
133
195
}
@@ -136,8 +198,8 @@ func (c *Client) Version(ctx context.Context) (string, error) {
136
198
}
137
199
138
200
// 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" , "" , "" )
141
203
if err != nil {
142
204
return "" , err
143
205
}
@@ -146,38 +208,32 @@ func (c *Client) ListTools(ctx context.Context) (string, error) {
146
208
}
147
209
148
210
// 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" , "" , "" )
151
213
if err != nil {
152
214
return nil , err
153
215
}
154
216
155
217
return strings .Split (strings .TrimSpace (out ), "\n " ), nil
156
218
}
157
219
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 ) {
159
221
run := & runSubCommand {
160
222
Run : Run {
161
- url : c .opts .GPTScriptURL ,
162
- binPath : c .opts .GPTScriptBin ,
223
+ url : c .gptscriptURL ,
163
224
requestPath : requestPath ,
164
225
state : Creating ,
165
226
toolPath : toolPath ,
166
227
content : content ,
167
228
},
168
229
}
169
230
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 }
179
234
}
180
- if err != nil {
235
+
236
+ if err := run .request (ctx , m ); err != nil {
181
237
return "" , err
182
238
}
183
239
0 commit comments