From 6ede09cbd6dd4ffba2d2bad263a25b1840a7f481 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Tue, 14 Jan 2025 08:09:04 -0500 Subject: [PATCH 1/2] feat: add file revision API for workspaces Signed-off-by: Donnie Adams --- workspace.go | 116 ++++++++++++++++++++++++++-- workspace_test.go | 189 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 300 insertions(+), 5 deletions(-) diff --git a/workspace.go b/workspace.go index a1214b8..321602a 100644 --- a/workspace.go +++ b/workspace.go @@ -209,6 +209,14 @@ func (g *GPTScript) ReadFileInWorkspace(ctx context.Context, filePath string, op return base64.StdEncoding.DecodeString(out) } +type FileInfo struct { + WorkspaceID string + Name string + Size int64 + ModTime time.Time + MimeType string +} + type StatFileInWorkspaceOptions struct { WorkspaceID string } @@ -247,10 +255,108 @@ func (g *GPTScript) StatFileInWorkspace(ctx context.Context, filePath string, op return info, nil } -type FileInfo struct { +type RevisionInfo struct { + FileInfo + RevisionID string +} + +type ListRevisionsForFileInWorkspaceOptions struct { WorkspaceID string - Name string - Size int64 - ModTime time.Time - MimeType string +} + +func (g *GPTScript) ListRevisionsForFileInWorkspace(ctx context.Context, filePath string, opts ...ListRevisionsForFileInWorkspaceOptions) ([]RevisionInfo, error) { + var opt ListRevisionsForFileInWorkspaceOptions + 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/list-revisions", 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 + } + + var info []RevisionInfo + err = json.Unmarshal([]byte(out), &info) + if err != nil { + return nil, err + } + + return info, nil +} + +type GetRevisionForFileInWorkspaceOptions struct { + WorkspaceID string +} + +func (g *GPTScript) GetRevisionForFileInWorkspace(ctx context.Context, filePath, revisionID string, opts ...GetRevisionForFileInWorkspaceOptions) ([]byte, error) { + var opt GetRevisionForFileInWorkspaceOptions + 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/get-revision", map[string]any{ + "id": opt.WorkspaceID, + "filePath": filePath, + "revisionID": revisionID, + "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 DeleteRevisionForFileInWorkspaceOptions struct { + WorkspaceID string +} + +func (g *GPTScript) DeleteRevisionForFileInWorkspace(ctx context.Context, filePath, revisionID string, opts ...DeleteRevisionForFileInWorkspaceOptions) error { + var opt DeleteRevisionForFileInWorkspaceOptions + for _, o := range opts { + if o.WorkspaceID != "" { + opt.WorkspaceID = o.WorkspaceID + } + } + + if opt.WorkspaceID == "" { + opt.WorkspaceID = os.Getenv("GPTSCRIPT_WORKSPACE_ID") + } + + _, err := g.runBasicCommand(ctx, "workspaces/delete-revision", map[string]any{ + "id": opt.WorkspaceID, + "filePath": filePath, + "revisionID": revisionID, + "workspaceTool": g.globalOpts.WorkspaceTool, + "env": g.globalOpts.Env, + }) + if err != nil && strings.HasSuffix(err.Error(), fmt.Sprintf("not found: %s/%s", opt.WorkspaceID, filePath)) { + return newNotFoundInWorkspaceError(opt.WorkspaceID, fmt.Sprintf("revision %s for %s", revisionID, filePath)) + } + + return err } diff --git a/workspace_test.go b/workspace_test.go index d2d5706..38bb552 100644 --- a/workspace_test.go +++ b/workspace_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "fmt" "os" "testing" ) @@ -91,6 +92,98 @@ func TestWriteReadAndDeleteFileFromWorkspace(t *testing.T) { } } +func TestRevisionsForFileInWorkspace(t *testing.T) { + id, err := g.CreateWorkspace(context.Background(), "directory") + if err != nil { + 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(), "test.txt", []byte("test0"), WriteFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Fatalf("Error creating file: %v", err) + } + + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test1"), WriteFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Fatalf("Error creating file: %v", err) + } + + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test2"), WriteFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Fatalf("Error creating file: %v", err) + } + + revisions, err := g.ListRevisionsForFileInWorkspace(context.Background(), "test.txt", ListRevisionsForFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Errorf("Error reading file: %v", err) + } + + if len(revisions) != 2 { + t.Errorf("Unexpected number of revisions: %d", len(revisions)) + } + + for i, rev := range revisions { + if rev.WorkspaceID != id { + t.Errorf("Unexpected file workspace ID: %v", rev.WorkspaceID) + } + + if rev.Name != "test.txt" { + t.Errorf("Unexpected file name: %s", rev.Name) + } + + if rev.Size != 5 { + t.Errorf("Unexpected file size: %d", rev.Size) + } + + if rev.ModTime.IsZero() { + t.Errorf("Unexpected file mod time: %v", rev.ModTime) + } + + if rev.MimeType != "text/plain" { + t.Errorf("Unexpected file mime type: %s", rev.MimeType) + } + + if rev.RevisionID != fmt.Sprintf("%d", i+1) { + t.Errorf("Unexpected revision ID: %s", rev.RevisionID) + } + } + + err = g.DeleteRevisionForFileInWorkspace(context.Background(), "test.txt", "1", DeleteRevisionForFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Errorf("Error deleting revision for file: %v", err) + } + + revisions, err = g.ListRevisionsForFileInWorkspace(context.Background(), "test.txt", ListRevisionsForFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Errorf("Error reading file: %v", err) + } + + if len(revisions) != 1 { + t.Errorf("Unexpected number of revisions: %d", len(revisions)) + } + + err = g.DeleteFileInWorkspace(context.Background(), "test.txt", DeleteFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Errorf("Error deleting file: %v", err) + } + + revisions, err = g.ListRevisionsForFileInWorkspace(context.Background(), "test.txt", ListRevisionsForFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Errorf("Error reading file: %v", err) + } + + if len(revisions) != 0 { + t.Errorf("Unexpected number of revisions: %d", len(revisions)) + } +} + func TestLsComplexWorkspace(t *testing.T) { id, err := g.CreateWorkspace(context.Background(), "directory") if err != nil { @@ -246,6 +339,102 @@ func TestWriteReadAndDeleteFileFromWorkspaceS3(t *testing.T) { } } +func TestRevisionsForFileInWorkspaceS3(t *testing.T) { + if os.Getenv("AWS_ACCESS_KEY_ID") == "" || os.Getenv("AWS_SECRET_ACCESS_KEY") == "" || os.Getenv("WORKSPACE_PROVIDER_S3_BUCKET") == "" { + t.Skip("Skipping test because AWS credentials are not set") + } + + id, err := g.CreateWorkspace(context.Background(), "s3") + if err != nil { + 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(), "test.txt", []byte("test0"), WriteFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Fatalf("Error creating file: %v", err) + } + + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test1"), WriteFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Fatalf("Error creating file: %v", err) + } + + err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test2"), WriteFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Fatalf("Error creating file: %v", err) + } + + revisions, err := g.ListRevisionsForFileInWorkspace(context.Background(), "test.txt", ListRevisionsForFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Errorf("Error reading file: %v", err) + } + + if len(revisions) != 2 { + t.Errorf("Unexpected number of revisions: %d", len(revisions)) + } + + for i, rev := range revisions { + if rev.WorkspaceID != id { + t.Errorf("Unexpected file workspace ID: %v", rev.WorkspaceID) + } + + if rev.Name != "test.txt" { + t.Errorf("Unexpected file name: %s", rev.Name) + } + + if rev.Size != 5 { + t.Errorf("Unexpected file size: %d", rev.Size) + } + + if rev.ModTime.IsZero() { + t.Errorf("Unexpected file mod time: %v", rev.ModTime) + } + + if rev.MimeType != "text/plain" { + t.Errorf("Unexpected file mime type: %s", rev.MimeType) + } + + if rev.RevisionID != fmt.Sprintf("%d", i+1) { + t.Errorf("Unexpected revision ID: %s", rev.RevisionID) + } + } + + err = g.DeleteRevisionForFileInWorkspace(context.Background(), "test.txt", "1", DeleteRevisionForFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Errorf("Error deleting revision for file: %v", err) + } + + revisions, err = g.ListRevisionsForFileInWorkspace(context.Background(), "test.txt", ListRevisionsForFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Errorf("Error reading file: %v", err) + } + + if len(revisions) != 1 { + t.Errorf("Unexpected number of revisions: %d", len(revisions)) + } + + err = g.DeleteFileInWorkspace(context.Background(), "test.txt", DeleteFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Errorf("Error deleting file: %v", err) + } + + revisions, err = g.ListRevisionsForFileInWorkspace(context.Background(), "test.txt", ListRevisionsForFileInWorkspaceOptions{WorkspaceID: id}) + if err != nil { + t.Errorf("Error reading file: %v", err) + } + + if len(revisions) != 0 { + t.Errorf("Unexpected number of revisions: %d", len(revisions)) + } +} + func TestLsComplexWorkspaceS3(t *testing.T) { if os.Getenv("AWS_ACCESS_KEY_ID") == "" || os.Getenv("AWS_SECRET_ACCESS_KEY") == "" || os.Getenv("WORKSPACE_PROVIDER_S3_BUCKET") == "" { t.Skip("Skipping test because AWS credentials are not set") From 5b6d8ab476e4667bf6608fa5f7f3faebff8b0c06 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Tue, 14 Jan 2025 12:40:58 -0500 Subject: [PATCH 2/2] chore: use model type for listing models Signed-off-by: Donnie Adams --- gptscript.go | 35 +++++++++++++++++++++++++++++++++-- gptscript_test.go | 8 ++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/gptscript.go b/gptscript.go index 1e30d95..28a46a2 100644 --- a/gptscript.go +++ b/gptscript.go @@ -293,8 +293,34 @@ type ListModelsOptions struct { CredentialOverrides []string } +type Model struct { + CreatedAt int64 `json:"created"` + ID string `json:"id"` + Object string `json:"object"` + OwnedBy string `json:"owned_by"` + Permission []Permission `json:"permission"` + Root string `json:"root"` + Parent string `json:"parent"` + Metadata map[string]string `json:"metadata"` +} + +type Permission struct { + CreatedAt int64 `json:"created"` + ID string `json:"id"` + Object string `json:"object"` + AllowCreateEngine bool `json:"allow_create_engine"` + AllowSampling bool `json:"allow_sampling"` + AllowLogprobs bool `json:"allow_logprobs"` + AllowSearchIndices bool `json:"allow_search_indices"` + AllowView bool `json:"allow_view"` + AllowFineTuning bool `json:"allow_fine_tuning"` + Organization string `json:"organization"` + Group interface{} `json:"group"` + IsBlocking bool `json:"is_blocking"` +} + // ListModels will list all the available models. -func (g *GPTScript) ListModels(ctx context.Context, opts ...ListModelsOptions) ([]string, error) { +func (g *GPTScript) ListModels(ctx context.Context, opts ...ListModelsOptions) ([]Model, error) { var o ListModelsOptions for _, opt := range opts { o.Providers = append(o.Providers, opt.Providers...) @@ -314,7 +340,12 @@ func (g *GPTScript) ListModels(ctx context.Context, opts ...ListModelsOptions) ( return nil, err } - return strings.Split(strings.TrimSpace(out), "\n"), nil + var models []Model + if err = json.Unmarshal([]byte(out), &models); err != nil { + return nil, fmt.Errorf("failed to parse models: %w", err) + } + + return models, nil } func (g *GPTScript) Confirm(ctx context.Context, resp AuthResponse) error { diff --git a/gptscript_test.go b/gptscript_test.go index 32adff9..834181e 100644 --- a/gptscript_test.go +++ b/gptscript_test.go @@ -98,8 +98,8 @@ func TestListModelsWithProvider(t *testing.T) { } for _, model := range models { - if !strings.HasPrefix(model, "claude-3-") || !strings.HasSuffix(model, "from github.com/gptscript-ai/claude3-anthropic-provider") { - t.Errorf("Unexpected model name: %s", model) + if !strings.HasPrefix(model.ID, "claude-3-") || !strings.HasSuffix(model.ID, "from github.com/gptscript-ai/claude3-anthropic-provider") { + t.Errorf("Unexpected model name: %s", model.ID) } } } @@ -128,8 +128,8 @@ func TestListModelsWithDefaultProvider(t *testing.T) { } for _, model := range models { - if !strings.HasPrefix(model, "claude-3-") || !strings.HasSuffix(model, "from github.com/gptscript-ai/claude3-anthropic-provider") { - t.Errorf("Unexpected model name: %s", model) + if !strings.HasPrefix(model.ID, "claude-3-") || !strings.HasSuffix(model.ID, "from github.com/gptscript-ai/claude3-anthropic-provider") { + t.Errorf("Unexpected model name: %s", model.ID) } } }