From 965e8afaa113af64eb9d7b81394e42d07d3fbfa5 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Thu, 13 Feb 2025 10:04:53 -0500 Subject: [PATCH 1/2] enhance: add ability to get revision ID when opening file in a workspace Signed-off-by: Donnie Adams --- go.mod | 13 ++-- go.sum | 27 ++++---- gptscript_test.go | 2 +- workspace.go | 75 ++++++++++++--------- workspace_test.go | 168 +++++++++++++++++++++++++++++++++++----------- 5 files changed, 195 insertions(+), 90 deletions(-) diff --git a/go.mod b/go.mod index 283f8d6..0ca27e7 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,19 @@ module github.com/gptscript-ai/go-gptscript go 1.23.0 require ( - github.com/getkin/kin-openapi v0.124.0 - github.com/stretchr/testify v1.8.4 + github.com/getkin/kin-openapi v0.129.0 + github.com/stretchr/testify v1.10.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-openapi/jsonpointer v0.20.2 // indirect - github.com/go-openapi/swag v0.22.8 // indirect - github.com/invopop/yaml v0.2.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect + github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 9d94c4d..1c7e6b3 100644 --- a/go.sum +++ b/go.sum @@ -1,38 +1,39 @@ 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/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= -github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= -github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= -github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= -github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= +github.com/getkin/kin-openapi v0.129.0 h1:QGYTNcmyP5X0AtFQ2Dkou9DGBJsUETeLH9rFrJXZh30= +github.com/getkin/kin-openapi v0.129.0/go.mod h1:gmWI+b/J45xqpyK5wJmRRZse5wefA5H0RDMK46kLUtI= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= -github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v3 v3.0.0/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/gptscript_test.go b/gptscript_test.go index 8f299c8..bf0662e 100644 --- a/gptscript_test.go +++ b/gptscript_test.go @@ -30,7 +30,7 @@ func TestMain(m *testing.M) { panic(fmt.Sprintf("error creating gptscript: %s", err)) } - g, err = NewGPTScript(GlobalOptions{OpenAIAPIKey: os.Getenv("OPENAI_API_KEY")}) + g, err = NewGPTScript(GlobalOptions{OpenAIAPIKey: os.Getenv("OPENAI_API_KEY"), WorkspaceTool: "/Users/thedadams/code/workspace-provider"}) if err != nil { gFirst.Close() panic(fmt.Sprintf("error creating gptscript: %s", err)) diff --git a/workspace.go b/workspace.go index 8bd5a6f..e35d1cd 100644 --- a/workspace.go +++ b/workspace.go @@ -147,9 +147,9 @@ func (g *GPTScript) RemoveAll(ctx context.Context, opts ...RemoveAllOptions) err } type WriteFileInWorkspaceOptions struct { - WorkspaceID string - CreateRevision *bool - LatestRevision string + WorkspaceID string + CreateRevision *bool + LatestRevisionID string } func (g *GPTScript) WriteFileInWorkspace(ctx context.Context, filePath string, contents []byte, opts ...WriteFileInWorkspaceOptions) error { @@ -161,8 +161,8 @@ func (g *GPTScript) WriteFileInWorkspace(ctx context.Context, filePath string, c if o.CreateRevision != nil { opt.CreateRevision = o.CreateRevision } - if o.LatestRevision != "" { - opt.LatestRevision = o.LatestRevision + if o.LatestRevisionID != "" { + opt.LatestRevisionID = o.LatestRevisionID } } @@ -171,13 +171,13 @@ func (g *GPTScript) WriteFileInWorkspace(ctx context.Context, filePath string, c } _, err := g.runBasicCommand(ctx, "workspaces/write-file", map[string]any{ - "id": opt.WorkspaceID, - "contents": base64.StdEncoding.EncodeToString(contents), - "filePath": filePath, - "createRevision": opt.CreateRevision, - "latestRevision": opt.LatestRevision, - "workspaceTool": g.globalOpts.WorkspaceTool, - "env": g.globalOpts.Env, + "id": opt.WorkspaceID, + "contents": base64.StdEncoding.EncodeToString(contents), + "filePath": filePath, + "createRevision": opt.CreateRevision, + "latestRevisionID": opt.LatestRevisionID, + "workspaceTool": g.globalOpts.WorkspaceTool, + "env": g.globalOpts.Env, }) return parsePossibleConflictInWorkspaceError(err) @@ -214,15 +214,22 @@ func (g *GPTScript) DeleteFileInWorkspace(ctx context.Context, filePath string, } type ReadFileInWorkspaceOptions struct { - WorkspaceID string + WorkspaceID string + WithLatestRevisionID bool } -func (g *GPTScript) ReadFileInWorkspace(ctx context.Context, filePath string, opts ...ReadFileInWorkspaceOptions) ([]byte, error) { +type ReadFileInWorkspaceResponse struct { + Content []byte `json:"content"` + RevisionID string `json:"revisionID"` +} + +func (g *GPTScript) ReadFileInWorkspace(ctx context.Context, filePath string, opts ...ReadFileInWorkspaceOptions) (*ReadFileInWorkspaceResponse, error) { var opt ReadFileInWorkspaceOptions for _, o := range opts { if o.WorkspaceID != "" { opt.WorkspaceID = o.WorkspaceID } + opt.WithLatestRevisionID = opt.WithLatestRevisionID || o.WithLatestRevisionID } if opt.WorkspaceID == "" { @@ -230,10 +237,11 @@ func (g *GPTScript) ReadFileInWorkspace(ctx context.Context, filePath string, op } out, err := g.runBasicCommand(ctx, "workspaces/read-file", map[string]any{ - "id": opt.WorkspaceID, - "filePath": filePath, - "workspaceTool": g.globalOpts.WorkspaceTool, - "env": g.globalOpts.Env, + "id": opt.WorkspaceID, + "filePath": filePath, + "withLatestRevisionID": opt.WithLatestRevisionID, + "workspaceTool": g.globalOpts.WorkspaceTool, + "env": g.globalOpts.Env, }) if err != nil { if strings.HasSuffix(err.Error(), fmt.Sprintf("not found: %s/%s", opt.WorkspaceID, filePath)) { @@ -242,7 +250,13 @@ func (g *GPTScript) ReadFileInWorkspace(ctx context.Context, filePath string, op return nil, err } - return base64.StdEncoding.DecodeString(out) + var resp ReadFileInWorkspaceResponse + err = json.Unmarshal([]byte(out), &resp) + if err != nil { + return nil, err + } + + return &resp, nil } type FileInfo struct { @@ -251,10 +265,12 @@ type FileInfo struct { Size int64 ModTime time.Time MimeType string + RevisionID string } type StatFileInWorkspaceOptions struct { - WorkspaceID string + WorkspaceID string + WithLatestRevisionID bool } func (g *GPTScript) StatFileInWorkspace(ctx context.Context, filePath string, opts ...StatFileInWorkspaceOptions) (FileInfo, error) { @@ -263,6 +279,7 @@ func (g *GPTScript) StatFileInWorkspace(ctx context.Context, filePath string, op if o.WorkspaceID != "" { opt.WorkspaceID = o.WorkspaceID } + opt.WithLatestRevisionID = opt.WithLatestRevisionID || o.WithLatestRevisionID } if opt.WorkspaceID == "" { @@ -270,10 +287,11 @@ func (g *GPTScript) StatFileInWorkspace(ctx context.Context, filePath string, op } out, err := g.runBasicCommand(ctx, "workspaces/stat-file", map[string]any{ - "id": opt.WorkspaceID, - "filePath": filePath, - "workspaceTool": g.globalOpts.WorkspaceTool, - "env": g.globalOpts.Env, + "id": opt.WorkspaceID, + "filePath": filePath, + "withLatestRevisionID": opt.WithLatestRevisionID, + "workspaceTool": g.globalOpts.WorkspaceTool, + "env": g.globalOpts.Env, }) if err != nil { if strings.HasSuffix(err.Error(), fmt.Sprintf("not found: %s/%s", opt.WorkspaceID, filePath)) { @@ -291,16 +309,11 @@ func (g *GPTScript) StatFileInWorkspace(ctx context.Context, filePath string, op return info, nil } -type RevisionInfo struct { - FileInfo - RevisionID string -} - type ListRevisionsForFileInWorkspaceOptions struct { WorkspaceID string } -func (g *GPTScript) ListRevisionsForFileInWorkspace(ctx context.Context, filePath string, opts ...ListRevisionsForFileInWorkspaceOptions) ([]RevisionInfo, error) { +func (g *GPTScript) ListRevisionsForFileInWorkspace(ctx context.Context, filePath string, opts ...ListRevisionsForFileInWorkspaceOptions) ([]FileInfo, error) { var opt ListRevisionsForFileInWorkspaceOptions for _, o := range opts { if o.WorkspaceID != "" { @@ -325,7 +338,7 @@ func (g *GPTScript) ListRevisionsForFileInWorkspace(ctx context.Context, filePat return nil, err } - var info []RevisionInfo + var info []FileInfo err = json.Unmarshal([]byte(out), &info) if err != nil { return nil, err diff --git a/workspace_test.go b/workspace_test.go index 669f2b0..962cde2 100644 --- a/workspace_test.go +++ b/workspace_test.go @@ -33,6 +33,13 @@ func TestCreateAndDeleteWorkspaceFromWorkspace(t *testing.T) { t.Fatalf("Error creating workspace: %v", err) } + t.Cleanup(func() { + err = g.DeleteWorkspace(context.Background(), id) + if err != nil { + t.Errorf("Error deleting workspace: %v", err) + } + }) + err = g.WriteFileInWorkspace(context.Background(), "file.txt", []byte("hello world"), WriteFileInWorkspaceOptions{ WorkspaceID: id, }) @@ -45,26 +52,21 @@ func TestCreateAndDeleteWorkspaceFromWorkspace(t *testing.T) { t.Errorf("Error creating workspace from workspace: %v", err) } - data, err := g.ReadFileInWorkspace(context.Background(), "file.txt", ReadFileInWorkspaceOptions{ + content, err := g.ReadFileInWorkspace(context.Background(), "file.txt", ReadFileInWorkspaceOptions{ WorkspaceID: newID, }) if err != nil { - t.Errorf("Error reading file: %v", err) + t.Fatalf("Error reading file: %v", err) } - if !bytes.Equal(data, []byte("hello world")) { - t.Errorf("Unexpected content: %s", data) + if !bytes.Equal(content.Content, []byte("hello world")) { + t.Errorf("Unexpected content: %s", content.Content) } err = g.DeleteWorkspace(context.Background(), id) if err != nil { t.Errorf("Error deleting workspace: %v", err) } - - err = g.DeleteWorkspace(context.Background(), newID) - if err != nil { - t.Errorf("Error deleting new workspace: %v", err) - } } func TestWriteReadAndDeleteFileFromWorkspace(t *testing.T) { @@ -90,8 +92,26 @@ func TestWriteReadAndDeleteFileFromWorkspace(t *testing.T) { t.Errorf("Error reading file: %v", err) } - if !bytes.Equal(content, []byte("test")) { - t.Errorf("Unexpected content: %s", content) + if !bytes.Equal(content.Content, []byte("test")) { + t.Errorf("Unexpected content: %s", content.Content) + } + + if content.RevisionID != "" { + t.Errorf("Unexpected file revision ID when not requesting it: %s", content.RevisionID) + } + + // Read the file and request the revision ID + content, err = g.ReadFileInWorkspace(context.Background(), "test.txt", ReadFileInWorkspaceOptions{WorkspaceID: id, WithLatestRevisionID: true}) + if err != nil { + t.Errorf("Error reading file: %v", err) + } + + if !bytes.Equal(content.Content, []byte("test")) { + t.Errorf("Unexpected content: %s", content.Content) + } + + if content.RevisionID == "" { + t.Errorf("Expected file revision ID when requesting it: %s", content.RevisionID) } // Stat the file to ensure it exists @@ -120,6 +140,24 @@ func TestWriteReadAndDeleteFileFromWorkspace(t *testing.T) { t.Errorf("Unexpected file mime type: %s", fileInfo.MimeType) } + if fileInfo.RevisionID != "" { + t.Errorf("Unexpected file revision ID when not requesting it: %s", fileInfo.RevisionID) + } + + // Stat file and request the revision ID + fileInfo, err = g.StatFileInWorkspace(context.Background(), "test.txt", StatFileInWorkspaceOptions{WorkspaceID: id, WithLatestRevisionID: true}) + if err != nil { + t.Errorf("Error statting file: %v", err) + } + + if fileInfo.WorkspaceID != id { + t.Errorf("Unexpected file workspace ID: %v", fileInfo.WorkspaceID) + } + + if fileInfo.RevisionID == "" { + t.Errorf("Expected file revision ID when requesting it: %s", fileInfo.RevisionID) + } + // Ensure we get the error we expect when trying to read a non-existent file _, err = g.ReadFileInWorkspace(context.Background(), "test1.txt", ReadFileInWorkspaceOptions{WorkspaceID: id}) if nf := (*NotFoundInWorkspaceError)(nil); !errors.As(err, &nf) { @@ -322,7 +360,7 @@ func TestConflictsForFileInWorkspace(t *testing.T) { ce := (*ConflictInWorkspaceError)(nil) // Writing a new file with a non-zero latest revision should fail - err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test0"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: "1"}) + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test0"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevisionID: "1"}) if err == nil || !errors.As(err, &ce) { t.Errorf("Expected error writing file with non-zero latest revision: %v", err) } @@ -347,7 +385,7 @@ func TestConflictsForFileInWorkspace(t *testing.T) { } // Writing to the file with the latest revision should succeed - err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test2"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: revisions[0].RevisionID}) + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test2"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevisionID: revisions[0].RevisionID}) if err != nil { t.Fatalf("Error creating file: %v", err) } @@ -362,12 +400,13 @@ func TestConflictsForFileInWorkspace(t *testing.T) { } // Writing to the file with the same revision should fail - err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test3"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: revisions[0].RevisionID}) + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test3"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevisionID: revisions[0].RevisionID}) if err == nil || !errors.As(err, &ce) { t.Errorf("Expected error writing file with same revision: %v", err) } - err = g.DeleteRevisionForFileInWorkspace(context.Background(), "test.txt", revisions[1].RevisionID, DeleteRevisionForFileInWorkspaceOptions{WorkspaceID: id}) + latestRevisionID := revisions[1].RevisionID + err = g.DeleteRevisionForFileInWorkspace(context.Background(), "test.txt", latestRevisionID, DeleteRevisionForFileInWorkspaceOptions{WorkspaceID: id}) if err != nil { t.Errorf("Error deleting revision for file: %v", err) } @@ -381,10 +420,16 @@ func TestConflictsForFileInWorkspace(t *testing.T) { t.Errorf("Unexpected number of revisions: %d", len(revisions)) } + // Ensure we cannot write a new file with the zero-th revision ID + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test4"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevisionID: revisions[0].RevisionID}) + if err == nil || !errors.As(err, &ce) { + t.Errorf("Unexpected error writing to file: %v", err) + } + // Ensure we can write a new file after deleting the latest revision - err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test4"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: revisions[0].RevisionID}) + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test4"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevisionID: latestRevisionID}) if err != nil { - t.Fatalf("Error creating file: %v", err) + t.Errorf("Error writing file: %v", err) } err = g.DeleteFileInWorkspace(context.Background(), "test.txt", DeleteFileInWorkspaceOptions{WorkspaceID: id}) @@ -501,15 +546,15 @@ func TestCreateAndDeleteWorkspaceFromWorkspaceS3(t *testing.T) { t.Errorf("Error creating workspace from workspace: %v", err) } - data, err := g.ReadFileInWorkspace(context.Background(), "file.txt", ReadFileInWorkspaceOptions{ + content, err := g.ReadFileInWorkspace(context.Background(), "file.txt", ReadFileInWorkspaceOptions{ WorkspaceID: newID, }) if err != nil { t.Errorf("Error reading file: %v", err) } - if !bytes.Equal(data, []byte("hello world")) { - t.Errorf("Unexpected content: %s", data) + if !bytes.Equal(content.Content, []byte("hello world")) { + t.Errorf("Unexpected content: %s", content.Content) } err = g.DeleteWorkspace(context.Background(), id) @@ -545,15 +590,15 @@ func TestCreateAndDeleteDirectoryWorkspaceFromWorkspaceS3(t *testing.T) { t.Errorf("Error creating workspace from workspace: %v", err) } - data, err := g.ReadFileInWorkspace(context.Background(), "file.txt", ReadFileInWorkspaceOptions{ + content, err := g.ReadFileInWorkspace(context.Background(), "file.txt", ReadFileInWorkspaceOptions{ WorkspaceID: newID, }) if err != nil { t.Errorf("Error reading file: %v", err) } - if !bytes.Equal(data, []byte("hello world")) { - t.Errorf("Unexpected content: %s", data) + if !bytes.Equal(content.Content, []byte("hello world")) { + t.Errorf("Unexpected content: %s", content.Content) } err = g.DeleteWorkspace(context.Background(), id) @@ -577,6 +622,13 @@ func TestCreateAndDeleteS3WorkspaceFromWorkspaceDirectory(t *testing.T) { t.Fatalf("Error creating workspace: %v", err) } + t.Cleanup(func() { + err = g.DeleteWorkspace(context.Background(), id) + if err != nil { + t.Errorf("Error deleting workspace: %v", err) + } + }) + err = g.WriteFileInWorkspace(context.Background(), "file.txt", []byte("hello world"), WriteFileInWorkspaceOptions{ WorkspaceID: id, }) @@ -589,26 +641,21 @@ func TestCreateAndDeleteS3WorkspaceFromWorkspaceDirectory(t *testing.T) { t.Errorf("Error creating workspace from workspace: %v", err) } - data, err := g.ReadFileInWorkspace(context.Background(), "file.txt", ReadFileInWorkspaceOptions{ + content, err := g.ReadFileInWorkspace(context.Background(), "file.txt", ReadFileInWorkspaceOptions{ WorkspaceID: newID, }) if err != nil { - t.Errorf("Error reading file: %v", err) + t.Fatalf("Error reading file: %v", err) } - if !bytes.Equal(data, []byte("hello world")) { - t.Errorf("Unexpected content: %s", data) + if !bytes.Equal(content.Content, []byte("hello world")) { + t.Errorf("Unexpected content: %s", content.Content) } err = g.DeleteWorkspace(context.Background(), id) if err != nil { t.Errorf("Error deleting workspace: %v", err) } - - err = g.DeleteWorkspace(context.Background(), newID) - if err != nil { - t.Errorf("Error deleting new workspace: %v", err) - } } func TestWriteReadAndDeleteFileFromWorkspaceS3(t *testing.T) { @@ -638,8 +685,26 @@ func TestWriteReadAndDeleteFileFromWorkspaceS3(t *testing.T) { t.Errorf("Error reading file: %v", err) } - if !bytes.Equal(content, []byte("test")) { - t.Errorf("Unexpected content: %s", content) + if !bytes.Equal(content.Content, []byte("test")) { + t.Errorf("Unexpected content: %s", content.Content) + } + + if content.RevisionID != "" { + t.Errorf("Unexpected file revision ID when not requesting it: %s", content.RevisionID) + } + + // Read the file and request the revision ID + content, err = g.ReadFileInWorkspace(context.Background(), "test.txt", ReadFileInWorkspaceOptions{WorkspaceID: id, WithLatestRevisionID: true}) + if err != nil { + t.Errorf("Error reading file: %v", err) + } + + if !bytes.Equal(content.Content, []byte("test")) { + t.Errorf("Unexpected content: %s", content.Content) + } + + if content.RevisionID == "" { + t.Errorf("Expected file revision ID when requesting it: %s", content.RevisionID) } // Stat the file to ensure it exists @@ -668,6 +733,24 @@ func TestWriteReadAndDeleteFileFromWorkspaceS3(t *testing.T) { t.Errorf("Unexpected file mime type: %s", fileInfo.MimeType) } + if fileInfo.RevisionID != "" { + t.Errorf("Unexpected file revision ID when not requesting it: %s", fileInfo.RevisionID) + } + + // Stat file and request the revision ID + fileInfo, err = g.StatFileInWorkspace(context.Background(), "test.txt", StatFileInWorkspaceOptions{WorkspaceID: id, WithLatestRevisionID: true}) + if err != nil { + t.Errorf("Error statting file: %v", err) + } + + if fileInfo.WorkspaceID != id { + t.Errorf("Unexpected file workspace ID: %v", fileInfo.WorkspaceID) + } + + if fileInfo.RevisionID == "" { + t.Errorf("Expected file revision ID when requesting it: %s", fileInfo.RevisionID) + } + // Ensure we get the error we expect when trying to read a non-existent file _, err = g.ReadFileInWorkspace(context.Background(), "test1.txt", ReadFileInWorkspaceOptions{WorkspaceID: id}) if nf := (*NotFoundInWorkspaceError)(nil); !errors.As(err, &nf) { @@ -795,7 +878,7 @@ func TestConflictsForFileInWorkspaceS3(t *testing.T) { ce := (*ConflictInWorkspaceError)(nil) // Writing a new file with a non-zero latest revision should fail - err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test0"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: "1"}) + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test0"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevisionID: "1"}) if err == nil || !errors.As(err, &ce) { t.Errorf("Expected error writing file with non-zero latest revision: %v", err) } @@ -820,7 +903,7 @@ func TestConflictsForFileInWorkspaceS3(t *testing.T) { } // Writing to the file with the latest revision should succeed - err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test2"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: revisions[0].RevisionID}) + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test2"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevisionID: revisions[0].RevisionID}) if err != nil { t.Fatalf("Error creating file: %v", err) } @@ -835,12 +918,13 @@ func TestConflictsForFileInWorkspaceS3(t *testing.T) { } // Writing to the file with the same revision should fail - err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test3"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: revisions[0].RevisionID}) + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test3"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevisionID: revisions[0].RevisionID}) if err == nil || !errors.As(err, &ce) { t.Errorf("Expected error writing file with same revision: %v", err) } - err = g.DeleteRevisionForFileInWorkspace(context.Background(), "test.txt", revisions[1].RevisionID, DeleteRevisionForFileInWorkspaceOptions{WorkspaceID: id}) + latestRevisionID := revisions[1].RevisionID + err = g.DeleteRevisionForFileInWorkspace(context.Background(), "test.txt", latestRevisionID, DeleteRevisionForFileInWorkspaceOptions{WorkspaceID: id}) if err != nil { t.Errorf("Error deleting revision for file: %v", err) } @@ -854,8 +938,14 @@ func TestConflictsForFileInWorkspaceS3(t *testing.T) { t.Errorf("Unexpected number of revisions: %d", len(revisions)) } + // Ensure we cannot write a new file with the zero-th revision ID + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test4"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevisionID: revisions[0].RevisionID}) + if err == nil || !errors.As(err, &ce) { + t.Fatalf("Error creating file: %v", err) + } + // Ensure we can write a new file after deleting the latest revision - err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test4"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: revisions[0].RevisionID}) + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test4"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevisionID: latestRevisionID}) if err != nil { t.Fatalf("Error creating file: %v", err) } From 25d959a071ffde8baecb2facc62b7d8a1a173622 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Wed, 19 Feb 2025 06:36:18 -0500 Subject: [PATCH 2/2] Read with revision updates Signed-off-by: Donnie Adams --- gptscript_test.go | 2 +- workspace.go | 49 +++++++++++++++++++++++++++++++++----------- workspace_test.go | 52 ++++++++++++++++++++--------------------------- 3 files changed, 60 insertions(+), 43 deletions(-) diff --git a/gptscript_test.go b/gptscript_test.go index bf0662e..8f299c8 100644 --- a/gptscript_test.go +++ b/gptscript_test.go @@ -30,7 +30,7 @@ func TestMain(m *testing.M) { panic(fmt.Sprintf("error creating gptscript: %s", err)) } - g, err = NewGPTScript(GlobalOptions{OpenAIAPIKey: os.Getenv("OPENAI_API_KEY"), WorkspaceTool: "/Users/thedadams/code/workspace-provider"}) + g, err = NewGPTScript(GlobalOptions{OpenAIAPIKey: os.Getenv("OPENAI_API_KEY")}) if err != nil { gFirst.Close() panic(fmt.Sprintf("error creating gptscript: %s", err)) diff --git a/workspace.go b/workspace.go index e35d1cd..f384a85 100644 --- a/workspace.go +++ b/workspace.go @@ -214,34 +214,59 @@ func (g *GPTScript) DeleteFileInWorkspace(ctx context.Context, filePath string, } type ReadFileInWorkspaceOptions struct { - WorkspaceID string - WithLatestRevisionID bool + WorkspaceID string } -type ReadFileInWorkspaceResponse struct { +func (g *GPTScript) ReadFileInWorkspace(ctx context.Context, filePath string, opts ...ReadFileInWorkspaceOptions) ([]byte, error) { + var opt ReadFileInWorkspaceOptions + for _, o := range opts { + if o.WorkspaceID != "" { + opt.WorkspaceID = o.WorkspaceID + } + } + + if opt.WorkspaceID == "" { + opt.WorkspaceID = os.Getenv("GPTSCRIPT_WORKSPACE_ID") + } + + out, err := g.runBasicCommand(ctx, "workspaces/read-file", map[string]any{ + "id": opt.WorkspaceID, + "filePath": filePath, + "workspaceTool": g.globalOpts.WorkspaceTool, + "env": g.globalOpts.Env, + }) + if err != nil { + if strings.HasSuffix(err.Error(), fmt.Sprintf("not found: %s/%s", opt.WorkspaceID, filePath)) { + return nil, newNotFoundInWorkspaceError(opt.WorkspaceID, filePath) + } + return nil, err + } + + return base64.StdEncoding.DecodeString(out) +} + +type ReadFileWithRevisionInWorkspaceResponse struct { Content []byte `json:"content"` RevisionID string `json:"revisionID"` } -func (g *GPTScript) ReadFileInWorkspace(ctx context.Context, filePath string, opts ...ReadFileInWorkspaceOptions) (*ReadFileInWorkspaceResponse, error) { +func (g *GPTScript) ReadFileWithRevisionInWorkspace(ctx context.Context, filePath string, opts ...ReadFileInWorkspaceOptions) (*ReadFileWithRevisionInWorkspaceResponse, error) { var opt ReadFileInWorkspaceOptions for _, o := range opts { if o.WorkspaceID != "" { opt.WorkspaceID = o.WorkspaceID } - opt.WithLatestRevisionID = opt.WithLatestRevisionID || o.WithLatestRevisionID } if opt.WorkspaceID == "" { opt.WorkspaceID = os.Getenv("GPTSCRIPT_WORKSPACE_ID") } - out, err := g.runBasicCommand(ctx, "workspaces/read-file", map[string]any{ - "id": opt.WorkspaceID, - "filePath": filePath, - "withLatestRevisionID": opt.WithLatestRevisionID, - "workspaceTool": g.globalOpts.WorkspaceTool, - "env": g.globalOpts.Env, + out, err := g.runBasicCommand(ctx, "workspaces/read-file-with-revision", map[string]any{ + "id": opt.WorkspaceID, + "filePath": filePath, + "workspaceTool": g.globalOpts.WorkspaceTool, + "env": g.globalOpts.Env, }) if err != nil { if strings.HasSuffix(err.Error(), fmt.Sprintf("not found: %s/%s", opt.WorkspaceID, filePath)) { @@ -250,7 +275,7 @@ func (g *GPTScript) ReadFileInWorkspace(ctx context.Context, filePath string, op return nil, err } - var resp ReadFileInWorkspaceResponse + var resp ReadFileWithRevisionInWorkspaceResponse err = json.Unmarshal([]byte(out), &resp) if err != nil { return nil, err diff --git a/workspace_test.go b/workspace_test.go index 962cde2..eb895d3 100644 --- a/workspace_test.go +++ b/workspace_test.go @@ -59,8 +59,8 @@ func TestCreateAndDeleteWorkspaceFromWorkspace(t *testing.T) { t.Fatalf("Error reading file: %v", err) } - if !bytes.Equal(content.Content, []byte("hello world")) { - t.Errorf("Unexpected content: %s", content.Content) + if !bytes.Equal(content, []byte("hello world")) { + t.Errorf("Unexpected content: %s", content) } err = g.DeleteWorkspace(context.Background(), id) @@ -92,26 +92,22 @@ func TestWriteReadAndDeleteFileFromWorkspace(t *testing.T) { t.Errorf("Error reading file: %v", err) } - if !bytes.Equal(content.Content, []byte("test")) { - t.Errorf("Unexpected content: %s", content.Content) - } - - if content.RevisionID != "" { - t.Errorf("Unexpected file revision ID when not requesting it: %s", content.RevisionID) + if !bytes.Equal(content, []byte("test")) { + t.Errorf("Unexpected content: %s", content) } // Read the file and request the revision ID - content, err = g.ReadFileInWorkspace(context.Background(), "test.txt", ReadFileInWorkspaceOptions{WorkspaceID: id, WithLatestRevisionID: true}) + contentWithRevision, err := g.ReadFileWithRevisionInWorkspace(context.Background(), "test.txt", ReadFileInWorkspaceOptions{WorkspaceID: id}) if err != nil { t.Errorf("Error reading file: %v", err) } - if !bytes.Equal(content.Content, []byte("test")) { - t.Errorf("Unexpected content: %s", content.Content) + if !bytes.Equal(contentWithRevision.Content, []byte("test")) { + t.Errorf("Unexpected content: %s", contentWithRevision.Content) } - if content.RevisionID == "" { - t.Errorf("Expected file revision ID when requesting it: %s", content.RevisionID) + if contentWithRevision.RevisionID == "" { + t.Errorf("Expected file revision ID when requesting it: %s", contentWithRevision.RevisionID) } // Stat the file to ensure it exists @@ -553,8 +549,8 @@ func TestCreateAndDeleteWorkspaceFromWorkspaceS3(t *testing.T) { t.Errorf("Error reading file: %v", err) } - if !bytes.Equal(content.Content, []byte("hello world")) { - t.Errorf("Unexpected content: %s", content.Content) + if !bytes.Equal(content, []byte("hello world")) { + t.Errorf("Unexpected content: %s", content) } err = g.DeleteWorkspace(context.Background(), id) @@ -597,8 +593,8 @@ func TestCreateAndDeleteDirectoryWorkspaceFromWorkspaceS3(t *testing.T) { t.Errorf("Error reading file: %v", err) } - if !bytes.Equal(content.Content, []byte("hello world")) { - t.Errorf("Unexpected content: %s", content.Content) + if !bytes.Equal(content, []byte("hello world")) { + t.Errorf("Unexpected content: %s", content) } err = g.DeleteWorkspace(context.Background(), id) @@ -648,8 +644,8 @@ func TestCreateAndDeleteS3WorkspaceFromWorkspaceDirectory(t *testing.T) { t.Fatalf("Error reading file: %v", err) } - if !bytes.Equal(content.Content, []byte("hello world")) { - t.Errorf("Unexpected content: %s", content.Content) + if !bytes.Equal(content, []byte("hello world")) { + t.Errorf("Unexpected content: %s", content) } err = g.DeleteWorkspace(context.Background(), id) @@ -685,26 +681,22 @@ func TestWriteReadAndDeleteFileFromWorkspaceS3(t *testing.T) { t.Errorf("Error reading file: %v", err) } - if !bytes.Equal(content.Content, []byte("test")) { - t.Errorf("Unexpected content: %s", content.Content) - } - - if content.RevisionID != "" { - t.Errorf("Unexpected file revision ID when not requesting it: %s", content.RevisionID) + if !bytes.Equal(content, []byte("test")) { + t.Errorf("Unexpected content: %s", content) } // Read the file and request the revision ID - content, err = g.ReadFileInWorkspace(context.Background(), "test.txt", ReadFileInWorkspaceOptions{WorkspaceID: id, WithLatestRevisionID: true}) + contentWithRevision, err := g.ReadFileWithRevisionInWorkspace(context.Background(), "test.txt", ReadFileInWorkspaceOptions{WorkspaceID: id}) if err != nil { t.Errorf("Error reading file: %v", err) } - if !bytes.Equal(content.Content, []byte("test")) { - t.Errorf("Unexpected content: %s", content.Content) + if !bytes.Equal(contentWithRevision.Content, []byte("test")) { + t.Errorf("Unexpected content: %s", contentWithRevision.Content) } - if content.RevisionID == "" { - t.Errorf("Expected file revision ID when requesting it: %s", content.RevisionID) + if contentWithRevision.RevisionID == "" { + t.Errorf("Expected file revision ID when requesting it: %s", contentWithRevision.RevisionID) } // Stat the file to ensure it exists