Skip to content

Commit 2fd9480

Browse files
authored
Merge pull request #15 from thedadams/add-parse-and-fmt
feat: add support for parse and fmt
2 parents 2ea5546 + 52d3823 commit 2fd9480

File tree

5 files changed

+389
-15
lines changed

5 files changed

+389
-15
lines changed

exec.go

+54-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package gptscript
22

33
import (
4+
"bytes"
45
"context"
6+
"encoding/json"
57
"fmt"
68
"io"
79
"os"
@@ -38,7 +40,7 @@ func (o Opts) toArgs() []string {
3840
// Version will return the output of `gptscript --version`
3941
func Version(ctx context.Context) (string, error) {
4042
out, err := exec.CommandContext(ctx, getCommand(), "--version").CombinedOutput()
41-
return string(out), err
43+
return string(bytes.TrimSpace(out)), err
4244
}
4345

4446
// ListTools will list all the available tools.
@@ -242,6 +244,57 @@ func StreamExecFileWithEvents(ctx context.Context, toolPath, input string, opts
242244
return stdout, stderr, eventsRead, wait
243245
}
244246

247+
// Parse will parse the given file into an array of Nodes.
248+
func Parse(ctx context.Context, fileName string, opts Opts) ([]Node, error) {
249+
output, err := exec.CommandContext(ctx, getCommand(), append(opts.toArgs(), "parse", fileName)...).CombinedOutput()
250+
if err != nil {
251+
return nil, err
252+
}
253+
254+
var doc Document
255+
if err = json.Unmarshal(output, &doc); err != nil {
256+
return nil, err
257+
}
258+
259+
return doc.Nodes, nil
260+
}
261+
262+
// ParseTool will parse the given string into a tool.
263+
func ParseTool(ctx context.Context, input string) ([]Node, error) {
264+
c := exec.CommandContext(ctx, getCommand(), "parse", "-")
265+
c.Stdin = strings.NewReader(input)
266+
267+
output, err := c.CombinedOutput()
268+
if err != nil {
269+
return nil, err
270+
}
271+
272+
var doc Document
273+
if err = json.Unmarshal(output, &doc); err != nil {
274+
return nil, err
275+
}
276+
277+
return doc.Nodes, nil
278+
}
279+
280+
// Fmt will format the given nodes into a string.
281+
func Fmt(ctx context.Context, nodes []Node) (string, error) {
282+
b, err := json.Marshal(Document{Nodes: nodes})
283+
if err != nil {
284+
return "", fmt.Errorf("failed to marshal nodes: %w", err)
285+
}
286+
287+
c := exec.CommandContext(ctx, getCommand(), "fmt", "-")
288+
c.Stdin = bytes.NewReader(b)
289+
290+
output, err := c.CombinedOutput()
291+
if err != nil {
292+
return "", err
293+
}
294+
295+
return string(output), nil
296+
}
297+
245298
func concatTools(tools []fmt.Stringer) string {
246299
var sb strings.Builder
247300
for i, tool := range tools {

exec_test.go

+210-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"runtime"
99
"strings"
1010
"testing"
11+
12+
"github.com/getkin/kin-openapi/openapi3"
1113
)
1214

1315
func TestMain(m *testing.M) {
@@ -77,7 +79,7 @@ func TestExecFileChdir(t *testing.T) {
7779
}
7880

7981
func TestExecComplexTool(t *testing.T) {
80-
tool := &Tool{
82+
tool := &SimpleTool{
8183
JSONResponse: true,
8284
Instructions: `
8385
Create three short graphic artist descriptions and their muses.
@@ -110,11 +112,11 @@ func TestExecWithToolList(t *testing.T) {
110112
shebang = "#!/usr/bin/env powershell.exe"
111113
}
112114
tools := []fmt.Stringer{
113-
&Tool{
115+
&SimpleTool{
114116
Tools: []string{"echo"},
115117
Instructions: "echo hello there",
116118
},
117-
&Tool{
119+
&SimpleTool{
118120
Name: "echo",
119121
Tools: []string{"sys.exec"},
120122
Description: "Echoes the input",
@@ -141,16 +143,16 @@ func TestExecWithToolListAndSubTool(t *testing.T) {
141143
shebang = "#!/usr/bin/env powershell.exe"
142144
}
143145
tools := []fmt.Stringer{
144-
&Tool{
146+
&SimpleTool{
145147
Tools: []string{"echo"},
146148
Instructions: "echo hello there",
147149
},
148-
&Tool{
150+
&SimpleTool{
149151
Name: "other",
150152
Tools: []string{"echo"},
151153
Instructions: "echo hello somewhere else",
152154
},
153-
&Tool{
155+
&SimpleTool{
154156
Name: "echo",
155157
Tools: []string{"sys.exec"},
156158
Description: "Echoes the input",
@@ -296,3 +298,205 @@ func TestStreamExecFileWithEvents(t *testing.T) {
296298
t.Error("No events output")
297299
}
298300
}
301+
302+
func TestParseSimpleFile(t *testing.T) {
303+
tools, err := Parse(context.Background(), "./test/test.gpt", Opts{})
304+
if err != nil {
305+
t.Errorf("Error parsing file: %v", err)
306+
}
307+
308+
if len(tools) != 1 {
309+
t.Errorf("Unexpected number of tools: %d", len(tools))
310+
}
311+
312+
if tools[0].ToolNode == nil {
313+
t.Error("No tool node found")
314+
}
315+
316+
if tools[0].ToolNode.Tool.Instructions != "Respond with a hello, in a random language. Also include the language in the response." {
317+
t.Errorf("Unexpected instructions: %s", tools[0].ToolNode.Tool.Instructions)
318+
}
319+
}
320+
321+
func TestParseSimpleFileWithChdir(t *testing.T) {
322+
tools, err := Parse(context.Background(), "./test.gpt", Opts{Chdir: "./test"})
323+
if err != nil {
324+
t.Errorf("Error parsing file: %v", err)
325+
}
326+
327+
if len(tools) != 1 {
328+
t.Errorf("Unexpected number of tools: %d", len(tools))
329+
}
330+
331+
if tools[0].ToolNode == nil {
332+
t.Error("No tool node found")
333+
}
334+
335+
if tools[0].ToolNode.Tool.Instructions != "Respond with a hello, in a random language. Also include the language in the response." {
336+
t.Errorf("Unexpected instructions: %s", tools[0].ToolNode.Tool.Instructions)
337+
}
338+
}
339+
340+
func TestParseTool(t *testing.T) {
341+
tools, err := ParseTool(context.Background(), "echo hello")
342+
if err != nil {
343+
t.Errorf("Error parsing tool: %v", err)
344+
}
345+
346+
if len(tools) != 1 {
347+
t.Errorf("Unexpected number of tools: %d", len(tools))
348+
}
349+
350+
if tools[0].ToolNode == nil {
351+
t.Error("No tool node found")
352+
}
353+
354+
if tools[0].ToolNode.Tool.Instructions != "echo hello" {
355+
t.Errorf("Unexpected instructions: %s", tools[0].ToolNode.Tool.Instructions)
356+
}
357+
}
358+
359+
func TestParseToolWithTextNode(t *testing.T) {
360+
tools, err := ParseTool(context.Background(), "echo hello\n---\n!markdown\nhello")
361+
if err != nil {
362+
t.Errorf("Error parsing tool: %v", err)
363+
}
364+
365+
if len(tools) != 2 {
366+
t.Errorf("Unexpected number of tools: %d", len(tools))
367+
}
368+
369+
if tools[0].ToolNode == nil {
370+
t.Error("No tool node found")
371+
}
372+
373+
if tools[0].ToolNode.Tool.Instructions != "echo hello" {
374+
t.Errorf("Unexpected instructions: %s", tools[0].ToolNode.Tool.Instructions)
375+
}
376+
377+
if tools[1].TextNode == nil {
378+
t.Error("No text node found")
379+
}
380+
381+
if tools[1].TextNode.Text != "!markdown\nhello\n" {
382+
t.Errorf("Unexpected text: %s", tools[1].TextNode.Text)
383+
}
384+
}
385+
386+
func TestFmt(t *testing.T) {
387+
nodes := []Node{
388+
{
389+
ToolNode: &ToolNode{
390+
Tool: Tool{
391+
Parameters: Parameters{
392+
Tools: []string{"echo"},
393+
},
394+
Instructions: "echo hello there",
395+
},
396+
},
397+
},
398+
{
399+
ToolNode: &ToolNode{
400+
Tool: Tool{
401+
Parameters: Parameters{
402+
Name: "echo",
403+
Arguments: &openapi3.Schema{
404+
Type: "object",
405+
Properties: map[string]*openapi3.SchemaRef{
406+
"input": {
407+
Value: &openapi3.Schema{
408+
Description: "The string input to echo",
409+
Type: "string",
410+
},
411+
},
412+
},
413+
},
414+
},
415+
Instructions: "#!/bin/bash\necho hello there",
416+
},
417+
},
418+
},
419+
}
420+
421+
out, err := Fmt(context.Background(), nodes)
422+
if err != nil {
423+
t.Errorf("Error formatting nodes: %v", err)
424+
}
425+
426+
if out != `Tools: echo
427+
428+
echo hello there
429+
430+
---
431+
Name: echo
432+
Args: input: The string input to echo
433+
434+
#!/bin/bash
435+
echo hello there
436+
` {
437+
t.Errorf("Unexpected output: %s", out)
438+
}
439+
}
440+
441+
func TestFmtWithTextNode(t *testing.T) {
442+
nodes := []Node{
443+
{
444+
ToolNode: &ToolNode{
445+
Tool: Tool{
446+
Parameters: Parameters{
447+
Tools: []string{"echo"},
448+
},
449+
Instructions: "echo hello there",
450+
},
451+
},
452+
},
453+
{
454+
TextNode: &TextNode{
455+
Text: "!markdown\nWe now echo hello there\n",
456+
},
457+
},
458+
{
459+
ToolNode: &ToolNode{
460+
Tool: Tool{
461+
Parameters: Parameters{
462+
Name: "echo",
463+
Arguments: &openapi3.Schema{
464+
Type: "object",
465+
Properties: map[string]*openapi3.SchemaRef{
466+
"input": {
467+
Value: &openapi3.Schema{
468+
Description: "The string input to echo",
469+
Type: "string",
470+
},
471+
},
472+
},
473+
},
474+
},
475+
Instructions: "#!/bin/bash\necho hello there",
476+
},
477+
},
478+
},
479+
}
480+
481+
out, err := Fmt(context.Background(), nodes)
482+
if err != nil {
483+
t.Errorf("Error formatting nodes: %v", err)
484+
}
485+
486+
if out != `Tools: echo
487+
488+
echo hello there
489+
490+
---
491+
!markdown
492+
We now echo hello there
493+
---
494+
Name: echo
495+
Args: input: The string input to echo
496+
497+
#!/bin/bash
498+
echo hello there
499+
` {
500+
t.Errorf("Unexpected output: %s", out)
501+
}
502+
}

go.mod

+13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
11
module github.com/gptscript-ai/go-gptscript
22

33
go 1.22.2
4+
5+
require github.com/getkin/kin-openapi v0.123.0
6+
7+
require (
8+
github.com/go-openapi/jsonpointer v0.20.2 // indirect
9+
github.com/go-openapi/swag v0.22.8 // indirect
10+
github.com/invopop/yaml v0.2.0 // indirect
11+
github.com/josharian/intern v1.0.0 // indirect
12+
github.com/mailru/easyjson v0.7.7 // indirect
13+
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
14+
github.com/perimeterx/marshmallow v1.1.5 // indirect
15+
gopkg.in/yaml.v3 v3.0.1 // indirect
16+
)

go.sum

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8=
4+
github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
5+
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
6+
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
7+
github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw=
8+
github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI=
9+
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
10+
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
11+
github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY=
12+
github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
13+
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
14+
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
15+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
16+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
17+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
18+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
19+
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
20+
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
21+
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
22+
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
23+
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
24+
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
25+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
26+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
27+
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
28+
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
29+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
30+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
31+
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
32+
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
33+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
34+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
35+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
36+
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
37+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
38+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)