From b77cd130a99f9abb4720b13c52dac18ee5cad2c8 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Tue, 6 Aug 2024 09:39:59 -0400 Subject: [PATCH 01/83] enhance: credentials: add GPTSCRIPT_CREDENTIAL_EXPIRATION (#709) Signed-off-by: Grant Linville --- docs/docs/03-tools/04-credential-tools.md | 18 ++++++++ integration/cred_test.go | 24 ++++++++-- integration/scripts/cred_expiration.gpt | 46 +++++++++++++++++++ .../{credscopes.gpt => cred_scopes.gpt} | 0 pkg/config/cliconfig.go | 2 + pkg/credentials/credential.go | 1 + pkg/runner/runner.go | 9 ++++ 7 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 integration/scripts/cred_expiration.gpt rename integration/scripts/{credscopes.gpt => cred_scopes.gpt} (100%) diff --git a/docs/docs/03-tools/04-credential-tools.md b/docs/docs/03-tools/04-credential-tools.md index 3e6a678a..1911dc34 100644 --- a/docs/docs/03-tools/04-credential-tools.md +++ b/docs/docs/03-tools/04-credential-tools.md @@ -204,3 +204,21 @@ that environment variable, and if it is set, get the refresh token from the exis typically without user interaction. For an example of a tool that uses the refresh feature, see the [Gateway OAuth2 tool](https://github.com/gptscript-ai/gateway-oauth2). + +### GPTSCRIPT_CREDENTIAL_EXPIRATION environment variable + +When a tool references a credential tool, GPTScript will add the environment variables from the credential to the tool's +environment before executing the tool. If at least one of the credentials has an `expiresAt` field, GPTScript will also +set the environment variable `GPTSCRIPT_CREDENTIAL_EXPIRATION`, which contains the nearest expiration time out of all +credentials referenced by the tool, in RFC 3339 format. That way, it can be referenced in the tool body if needed. +Here is an example: + +``` +Credential: my-credential-tool.gpt as myCred + +#!python3 + +import os + +print("myCred expires at " + os.getenv("GPTSCRIPT_CREDENTIAL_EXPIRATION", "")) +``` diff --git a/integration/cred_test.go b/integration/cred_test.go index 67298ef8..d77f096c 100644 --- a/integration/cred_test.go +++ b/integration/cred_test.go @@ -1,7 +1,9 @@ package integration import ( + "strings" "testing" + "time" "github.com/stretchr/testify/require" ) @@ -15,15 +17,31 @@ func TestGPTScriptCredential(t *testing.T) { // TestCredentialScopes makes sure that environment variables set by credential tools and shared credential tools // are only available to the correct tools. See scripts/credscopes.gpt for more details. func TestCredentialScopes(t *testing.T) { - out, err := RunScript("scripts/credscopes.gpt", "--sub-tool", "oneOne") + out, err := RunScript("scripts/cred_scopes.gpt", "--sub-tool", "oneOne") require.NoError(t, err) require.Contains(t, out, "good") - out, err = RunScript("scripts/credscopes.gpt", "--sub-tool", "twoOne") + out, err = RunScript("scripts/cred_scopes.gpt", "--sub-tool", "twoOne") require.NoError(t, err) require.Contains(t, out, "good") - out, err = RunScript("scripts/credscopes.gpt", "--sub-tool", "twoTwo") + out, err = RunScript("scripts/cred_scopes.gpt", "--sub-tool", "twoTwo") require.NoError(t, err) require.Contains(t, out, "good") } + +// TestCredentialExpirationEnv tests a GPTScript with two credentials that expire at different times. +// One expires after two hours, and the other expires after one hour. +// This test makes sure that the GPTSCRIPT_CREDENTIAL_EXPIRATION environment variable is set to the nearer expiration time (1h). +func TestCredentialExpirationEnv(t *testing.T) { + out, err := RunScript("scripts/cred_expiration.gpt") + require.NoError(t, err) + + for _, line := range strings.Split(out, "\n") { + if timestamp, found := strings.CutPrefix(line, "Expires: "); found { + expiresTime, err := time.Parse(time.RFC3339, timestamp) + require.NoError(t, err) + require.True(t, time.Until(expiresTime) < time.Hour) + } + } +} diff --git a/integration/scripts/cred_expiration.gpt b/integration/scripts/cred_expiration.gpt new file mode 100644 index 00000000..da535df0 --- /dev/null +++ b/integration/scripts/cred_expiration.gpt @@ -0,0 +1,46 @@ +cred: credentialTool with 2 as hours +cred: credentialTool with 1 as hours + +#!python3 + +import os + +print("Expires: " + os.getenv("GPTSCRIPT_CREDENTIAL_EXPIRATION", ""), end="") + +--- +name: credentialTool +args: hours: the number of hours from now to expire + +#!python3 + +import os +import json +from datetime import datetime, timedelta, timezone + +class Output: + def __init__(self, env, expires_at): + self.env = env + self.expiresAt = expires_at + + def to_dict(self): + return { + "env": self.env, + "expiresAt": self.expiresAt.isoformat() + } + +hours_str = os.getenv("HOURS") +if hours_str is None: + print("HOURS environment variable is not set") + os._exit(1) + +try: + hours = int(hours_str) +except ValueError: + print("failed to parse HOURS") + os._exit(1) + +expires_at = datetime.now(timezone.utc) + timedelta(hours=hours) +out = Output(env={"yeet": "yote"}, expires_at=expires_at) +out_json = json.dumps(out.to_dict()) + +print(out_json) diff --git a/integration/scripts/credscopes.gpt b/integration/scripts/cred_scopes.gpt similarity index 100% rename from integration/scripts/credscopes.gpt rename to integration/scripts/cred_scopes.gpt diff --git a/pkg/config/cliconfig.go b/pkg/config/cliconfig.go index e4aa49ab..7970415f 100644 --- a/pkg/config/cliconfig.go +++ b/pkg/config/cliconfig.go @@ -55,6 +55,8 @@ type CLIConfig struct { Auths map[string]AuthConfig `json:"auths,omitempty"` CredentialsStore string `json:"credsStore,omitempty"` GPTScriptConfigFile string `json:"gptscriptConfig,omitempty"` + GatewayURL string `json:"gatewayURL,omitempty"` + Integrations map[string]string `json:"integrations,omitempty"` auths map[string]types.AuthConfig authsLock *sync.Mutex diff --git a/pkg/credentials/credential.go b/pkg/credentials/credential.go index 605208a0..3d1e2192 100644 --- a/pkg/credentials/credential.go +++ b/pkg/credentials/credential.go @@ -16,6 +16,7 @@ const ( CredentialTypeTool CredentialType = "tool" CredentialTypeModelProvider CredentialType = "modelProvider" ExistingCredential = "GPTSCRIPT_EXISTING_CREDENTIAL" + CredentialExpiration = "GPTSCRIPT_CREDENTIAL_EXPIRATION" ) type Credential struct { diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 3a33c720..c2137bea 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -865,6 +865,7 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env } } + var nearestExpiration *time.Time for _, ref := range credToolRefs { toolName, credentialAlias, args, err := types.ParseCredentialArgs(ref.Reference, callCtx.Input) if err != nil { @@ -967,11 +968,19 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env } else { log.Warnf("Not saving credential for tool %s - credentials will only be saved for tools from GitHub, or tools that use aliases.", toolName) } + + if c.ExpiresAt != nil && (nearestExpiration == nil || nearestExpiration.After(*c.ExpiresAt)) { + nearestExpiration = c.ExpiresAt + } } for k, v := range c.Env { env = append(env, fmt.Sprintf("%s=%s", k, v)) } + + if nearestExpiration != nil { + env = append(env, fmt.Sprintf("%s=%s", credentials.CredentialExpiration, nearestExpiration.Format(time.RFC3339))) + } } return env, nil From 0f820879b595fe317a659dd53f67b8fd2826a958 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Sun, 2 Jun 2024 22:46:37 -0700 Subject: [PATCH 02/83] feat: add basic bash support for windows --- go.mod | 12 +-- go.sum | 24 ++--- pkg/engine/cmd.go | 5 + pkg/repos/download/extract.go | 14 +++ pkg/repos/get.go | 43 ++++++-- pkg/repos/runtimes/busybox/SHASUMS256.txt | 1 + pkg/repos/runtimes/busybox/busybox.go | 107 ++++++++++++++++++++ pkg/repos/runtimes/busybox/busybox_test.go | 41 ++++++++ pkg/repos/runtimes/busybox/log.go | 5 + pkg/repos/runtimes/default.go | 2 + pkg/tests/runner_test.go | 3 - pkg/tests/testdata/TestContextArg/other.gpt | 4 +- pkg/tests/testdata/TestContextArg/test.gpt | 2 +- pkg/tests/tester/runner.go | 12 ++- 14 files changed, 243 insertions(+), 32 deletions(-) create mode 100644 pkg/repos/runtimes/busybox/SHASUMS256.txt create mode 100644 pkg/repos/runtimes/busybox/busybox.go create mode 100644 pkg/repos/runtimes/busybox/busybox_test.go create mode 100644 pkg/repos/runtimes/busybox/log.go diff --git a/go.mod b/go.mod index 783e638b..c84dae97 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc golang.org/x/sync v0.7.0 - golang.org/x/term v0.20.0 + golang.org/x/term v0.22.0 gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.1 sigs.k8s.io/yaml v1.4.0 @@ -108,10 +108,10 @@ require ( github.com/yuin/goldmark v1.5.4 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect go4.org v0.0.0-20200411211856-f5505b9728dd // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.23.0 // indirect mvdan.cc/gofumpt v0.6.0 // indirect ) diff --git a/go.sum b/go.sum index fd165d3f..7e2d7b75 100644 --- a/go.sum +++ b/go.sum @@ -396,8 +396,8 @@ golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -419,8 +419,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -474,8 +474,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -485,8 +485,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -498,8 +498,8 @@ golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -531,8 +531,8 @@ golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/engine/cmd.go b/pkg/engine/cmd.go index d62aad2e..8e7b234e 100644 --- a/pkg/engine/cmd.go +++ b/pkg/engine/cmd.go @@ -10,6 +10,7 @@ import ( "io" "os" "os/exec" + "path" "path/filepath" "runtime" "sort" @@ -269,6 +270,10 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T }) } + if runtime.GOOS == "windows" && (args[0] == "/bin/bash" || args[0] == "/bin/sh") { + args[0] = path.Base(args[0]) + } + if runtime.GOOS == "windows" && (args[0] == "/usr/bin/env" || args[0] == "/bin/env") { args = args[1:] } diff --git a/pkg/repos/download/extract.go b/pkg/repos/download/extract.go index 95a82d74..4cf09f0c 100644 --- a/pkg/repos/download/extract.go +++ b/pkg/repos/download/extract.go @@ -9,7 +9,9 @@ import ( "net/http" "net/url" "os" + "path" "path/filepath" + "strings" "time" "github.com/mholt/archiver/v4" @@ -60,6 +62,18 @@ func Extract(ctx context.Context, downloadURL, digest, targetDir string) error { return err } + bin := path.Base(parsedURL.Path) + if strings.HasSuffix(bin, ".exe") { + dst, err := os.Create(filepath.Join(targetDir, bin)) + if err != nil { + return err + } + defer dst.Close() + + _, err = io.Copy(dst, tmpFile) + return err + } + format, input, err := archiver.Identify(filepath.Base(parsedURL.Path), tmpFile) if err != nil { return err diff --git a/pkg/repos/get.go b/pkg/repos/get.go index 2f96d8b3..bead4a7a 100644 --- a/pkg/repos/get.go +++ b/pkg/repos/get.go @@ -15,6 +15,7 @@ import ( "github.com/BurntSushi/locker" "github.com/gptscript-ai/gptscript/pkg/config" "github.com/gptscript-ai/gptscript/pkg/credentials" + "github.com/gptscript-ai/gptscript/pkg/hash" "github.com/gptscript-ai/gptscript/pkg/loader/github" "github.com/gptscript-ai/gptscript/pkg/repos/git" "github.com/gptscript-ai/gptscript/pkg/repos/runtimes/golang" @@ -51,6 +52,7 @@ type Manager struct { credHelperDirs credentials.CredentialHelperDirs runtimes []Runtime credHelperConfig *credHelperConfig + supportLocal bool } type credHelperConfig struct { @@ -60,6 +62,10 @@ type credHelperConfig struct { env []string } +func (m *Manager) SetSupportLocal() { + m.supportLocal = true +} + func New(cacheDir string, runtimes ...Runtime) *Manager { root := filepath.Join(cacheDir, "repos") return &Manager{ @@ -200,8 +206,14 @@ func (m *Manager) setup(ctx context.Context, runtime Runtime, tool types.Tool, e _ = os.RemoveAll(doneFile) _ = os.RemoveAll(target) - if err := git.Checkout(ctx, m.gitDir, tool.Source.Repo.Root, tool.Source.Repo.Revision, target); err != nil { - return "", nil, err + if tool.Source.Repo.VCS == "git" { + if err := git.Checkout(ctx, m.gitDir, tool.Source.Repo.Root, tool.Source.Repo.Revision, target); err != nil { + return "", nil, err + } + } else { + if err := os.MkdirAll(target, 0755); err != nil { + return "", nil, err + } } newEnv, err := runtime.Setup(ctx, m.runtimeDir, targetFinal, env) @@ -227,12 +239,25 @@ func (m *Manager) setup(ctx context.Context, runtime Runtime, tool types.Tool, e } func (m *Manager) GetContext(ctx context.Context, tool types.Tool, cmd, env []string) (string, []string, error) { - if tool.Source.Repo == nil { - return tool.WorkingDir, env, nil - } + var isLocal bool + if !m.supportLocal { + if tool.Source.Repo == nil { + return tool.WorkingDir, env, nil + } - if tool.Source.Repo.VCS != "git" { - return "", nil, fmt.Errorf("only git is supported, found VCS %s for %s", tool.Source.Repo.VCS, tool.ID) + if tool.Source.Repo.VCS != "git" { + return "", nil, fmt.Errorf("only git is supported, found VCS %s for %s", tool.Source.Repo.VCS, tool.ID) + } + } else if tool.Source.Repo == nil { + isLocal = true + id := hash.Digest(tool)[:12] + tool.Source.Repo = &types.Repo{ + VCS: "", + Root: id, + Path: "/", + Name: id, + Revision: id, + } } for _, runtime := range m.runtimes { @@ -242,5 +267,9 @@ func (m *Manager) GetContext(ctx context.Context, tool types.Tool, cmd, env []st } } + if isLocal { + return tool.WorkingDir, env, nil + } + return m.setup(ctx, &noopRuntime{}, tool, env) } diff --git a/pkg/repos/runtimes/busybox/SHASUMS256.txt b/pkg/repos/runtimes/busybox/SHASUMS256.txt new file mode 100644 index 00000000..7da1aff6 --- /dev/null +++ b/pkg/repos/runtimes/busybox/SHASUMS256.txt @@ -0,0 +1 @@ +6d2dfd1c1412c3550a89071a1b36a6f6073844320e687343d1dfc72719ecb8d9 FRP-5301-gda71f7c57/busybox-w64-FRP-5301-gda71f7c57.exe \ No newline at end of file diff --git a/pkg/repos/runtimes/busybox/busybox.go b/pkg/repos/runtimes/busybox/busybox.go new file mode 100644 index 00000000..b0c00a0c --- /dev/null +++ b/pkg/repos/runtimes/busybox/busybox.go @@ -0,0 +1,107 @@ +package busybox + +import ( + "bufio" + "bytes" + "context" + _ "embed" + "errors" + "fmt" + "io/fs" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" + + runtimeEnv "github.com/gptscript-ai/gptscript/pkg/env" + "github.com/gptscript-ai/gptscript/pkg/hash" + "github.com/gptscript-ai/gptscript/pkg/repos/download" +) + +//go:embed SHASUMS256.txt +var releasesData []byte + +const downloadURL = "https://github.com/gptscript-ai/busybox-w32/releases/download/%s" + +type Runtime struct { +} + +func (r *Runtime) ID() string { + return "busybox" +} + +func (r *Runtime) Supports(cmd []string) bool { + if runtime.GOOS != "windows" { + return false + } + for _, bin := range []string{"bash", "sh", "/bin/sh", "/bin/bash"} { + if runtimeEnv.Matches(cmd, bin) { + return true + } + } + return false +} + +func (r *Runtime) Setup(ctx context.Context, dataRoot, _ string, env []string) ([]string, error) { + binPath, err := r.getRuntime(ctx, dataRoot) + if err != nil { + return nil, err + } + + newEnv := runtimeEnv.AppendPath(env, binPath) + return newEnv, nil +} + +func (r *Runtime) getReleaseAndDigest() (string, string, error) { + scanner := bufio.NewScanner(bytes.NewReader(releasesData)) + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + return fmt.Sprintf(downloadURL, fields[1]), fields[0], nil + } + + return "", "", fmt.Errorf("failed to find %s release", r.ID()) +} + +func (r *Runtime) getRuntime(ctx context.Context, cwd string) (string, error) { + url, sha, err := r.getReleaseAndDigest() + if err != nil { + return "", err + } + + target := filepath.Join(cwd, "busybox", hash.ID(url, sha)) + if _, err := os.Stat(target); err == nil { + return target, nil + } else if !errors.Is(err, fs.ErrNotExist) { + return "", err + } + + log.Infof("Downloading Busybox") + tmp := target + ".download" + defer os.RemoveAll(tmp) + + if err := os.MkdirAll(tmp, 0755); err != nil { + return "", err + } + + if err := download.Extract(ctx, url, sha, tmp); err != nil { + return "", err + } + + bbExe := filepath.Join(tmp, path.Base(url)) + + cmd := exec.Command(bbExe, "--install", ".") + cmd.Dir = filepath.Dir(bbExe) + + if err := cmd.Run(); err != nil { + return "", err + } + + if err := os.Rename(tmp, target); err != nil { + return "", err + } + + return target, nil +} diff --git a/pkg/repos/runtimes/busybox/busybox_test.go b/pkg/repos/runtimes/busybox/busybox_test.go new file mode 100644 index 00000000..f3add18a --- /dev/null +++ b/pkg/repos/runtimes/busybox/busybox_test.go @@ -0,0 +1,41 @@ +package busybox + +import ( + "context" + "errors" + "io/fs" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/adrg/xdg" + "github.com/samber/lo" + "github.com/stretchr/testify/require" +) + +var ( + testCacheHome = lo.Must(xdg.CacheFile("gptscript-test-cache/runtime")) +) + +func firstPath(s []string) string { + _, p, _ := strings.Cut(s[0], "=") + return strings.Split(p, string(os.PathListSeparator))[0] +} + +func TestRuntime(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip() + } + + r := Runtime{} + + s, err := r.Setup(context.Background(), testCacheHome, "testdata", os.Environ()) + require.NoError(t, err) + _, err = os.Stat(filepath.Join(firstPath(s), "busybox.exe")) + if errors.Is(err, fs.ErrNotExist) { + _, err = os.Stat(filepath.Join(firstPath(s), "busybox")) + } + require.NoError(t, err) +} diff --git a/pkg/repos/runtimes/busybox/log.go b/pkg/repos/runtimes/busybox/log.go new file mode 100644 index 00000000..b7e486f1 --- /dev/null +++ b/pkg/repos/runtimes/busybox/log.go @@ -0,0 +1,5 @@ +package busybox + +import "github.com/gptscript-ai/gptscript/pkg/mvl" + +var log = mvl.Package() diff --git a/pkg/repos/runtimes/default.go b/pkg/repos/runtimes/default.go index d37cca8f..3782e26e 100644 --- a/pkg/repos/runtimes/default.go +++ b/pkg/repos/runtimes/default.go @@ -3,12 +3,14 @@ package runtimes import ( "github.com/gptscript-ai/gptscript/pkg/engine" "github.com/gptscript-ai/gptscript/pkg/repos" + "github.com/gptscript-ai/gptscript/pkg/repos/runtimes/busybox" "github.com/gptscript-ai/gptscript/pkg/repos/runtimes/golang" "github.com/gptscript-ai/gptscript/pkg/repos/runtimes/node" "github.com/gptscript-ai/gptscript/pkg/repos/runtimes/python" ) var Runtimes = []repos.Runtime{ + &busybox.Runtime{}, &python.Runtime{ Version: "3.12", Default: true, diff --git a/pkg/tests/runner_test.go b/pkg/tests/runner_test.go index 12eff23a..60f1cfc3 100644 --- a/pkg/tests/runner_test.go +++ b/pkg/tests/runner_test.go @@ -751,9 +751,6 @@ func TestGlobalErr(t *testing.T) { } func TestContextArg(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip() - } runner := tester.NewRunner(t) x, err := runner.Run("", `{ "file": "foo.db" diff --git a/pkg/tests/testdata/TestContextArg/other.gpt b/pkg/tests/testdata/TestContextArg/other.gpt index b1acd66a..f97b4ba6 100644 --- a/pkg/tests/testdata/TestContextArg/other.gpt +++ b/pkg/tests/testdata/TestContextArg/other.gpt @@ -2,5 +2,5 @@ name: fromcontext args: first: an arg args: second: an arg -#!/bin/bash -echo this is from other context ${first} and then ${second} \ No newline at end of file +#!/usr/bin/env bash +echo this is from other context ${FIRST} and then ${SECOND} \ No newline at end of file diff --git a/pkg/tests/testdata/TestContextArg/test.gpt b/pkg/tests/testdata/TestContextArg/test.gpt index 9569aaf9..50d2ccf2 100644 --- a/pkg/tests/testdata/TestContextArg/test.gpt +++ b/pkg/tests/testdata/TestContextArg/test.gpt @@ -9,4 +9,4 @@ name: fromcontext args: first: an arg #!/bin/bash -echo this is from context -- ${first} \ No newline at end of file +echo this is from context -- ${FIRST} \ No newline at end of file diff --git a/pkg/tests/tester/runner.go b/pkg/tests/tester/runner.go index 775f0248..ef75c0f5 100644 --- a/pkg/tests/tester/runner.go +++ b/pkg/tests/tester/runner.go @@ -8,8 +8,11 @@ import ( "path/filepath" "testing" + "github.com/adrg/xdg" "github.com/gptscript-ai/gptscript/pkg/credentials" "github.com/gptscript-ai/gptscript/pkg/loader" + "github.com/gptscript-ai/gptscript/pkg/repos" + "github.com/gptscript-ai/gptscript/pkg/repos/runtimes" "github.com/gptscript-ai/gptscript/pkg/runner" "github.com/gptscript-ai/gptscript/pkg/types" "github.com/hexops/autogold/v2" @@ -171,8 +174,15 @@ func NewRunner(t *testing.T) *Runner { t: t, } + cacheDir, err := xdg.CacheFile("gptscript-test-cache/runtime") + require.NoError(t, err) + + rm := runtimes.Default(cacheDir) + rm.(*repos.Manager).SetSupportLocal() + run, err := runner.New(c, credentials.NoopStore{}, runner.Options{ - Sequential: true, + Sequential: true, + RuntimeManager: rm, }) require.NoError(t, err) From 4c3da3a352b5a5f26553685fb07938529d7ce766 Mon Sep 17 00:00:00 2001 From: Taylor Price Date: Fri, 2 Aug 2024 16:02:37 -0700 Subject: [PATCH 03/83] chore: uppercase env variable names Signed-off-by: Taylor Price --- integration/scripts/cred_scopes.gpt | 4 ++-- pkg/engine/cmd.go | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/integration/scripts/cred_scopes.gpt b/integration/scripts/cred_scopes.gpt index 7319f163..dc8e24e7 100644 --- a/integration/scripts/cred_scopes.gpt +++ b/integration/scripts/cred_scopes.gpt @@ -149,8 +149,8 @@ name: getcred import os import json -var = os.getenv('var') -val = os.getenv('val') +var = os.getenv('VAR') +val = os.getenv('VAL') output = { "env": { diff --git a/pkg/engine/cmd.go b/pkg/engine/cmd.go index 8e7b234e..869e67dc 100644 --- a/pkg/engine/cmd.go +++ b/pkg/engine/cmd.go @@ -204,10 +204,9 @@ var ignoreENV = map[string]struct{}{ } func appendEnv(envs []string, k, v string) []string { - for _, k := range []string{k, env.ToEnvLike(k)} { - if _, ignore := ignoreENV[k]; !ignore { - envs = append(envs, k+"="+v) - } + //fmt.Printf("%s=%s\n", k, v) + if _, ignore := ignoreENV[k]; !ignore { + envs = append(envs, strings.ToUpper(k)+"="+v) } return envs } From 9b8dc494ac29a921ea274b12814b2117514b655f Mon Sep 17 00:00:00 2001 From: Taylor Price Date: Mon, 5 Aug 2024 10:05:35 -0700 Subject: [PATCH 04/83] fix: correct small comment typo --- pkg/tests/smoke/smoke_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tests/smoke/smoke_test.go b/pkg/tests/smoke/smoke_test.go index 66374ef1..a6d0ab2c 100644 --- a/pkg/tests/smoke/smoke_test.go +++ b/pkg/tests/smoke/smoke_test.go @@ -83,7 +83,7 @@ func TestSmoke(t *testing.T) { actualEvents, ` - disregard differences in timestamps, generated IDs, natural language verbiage, and event order -- omit callProgress events from the comparision +- omit callProgress events from the comparison - the overall stream of events and set of tools called should roughly match - arguments passed in tool calls should be roughly the same - the final callFinish event should be semantically similar From ae328ac395ecfc710399a5b50fbc0aaa799c4168 Mon Sep 17 00:00:00 2001 From: Taylor Price Date: Mon, 5 Aug 2024 14:05:16 -0700 Subject: [PATCH 05/83] chore: ensure key is env-like Signed-off-by: Taylor Price --- pkg/engine/cmd.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/engine/cmd.go b/pkg/engine/cmd.go index 869e67dc..484b00cb 100644 --- a/pkg/engine/cmd.go +++ b/pkg/engine/cmd.go @@ -204,9 +204,8 @@ var ignoreENV = map[string]struct{}{ } func appendEnv(envs []string, k, v string) []string { - //fmt.Printf("%s=%s\n", k, v) if _, ignore := ignoreENV[k]; !ignore { - envs = append(envs, strings.ToUpper(k)+"="+v) + envs = append(envs, strings.ToUpper(env.ToEnvLike(k))+"="+v) } return envs } From 806fdc38b48658cb5bb68cb492f411f4fc1d55c0 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Mon, 5 Aug 2024 20:56:56 -0700 Subject: [PATCH 06/83] feat: support inline package.json and requirements.txt --- pkg/engine/cmd.go | 2 +- pkg/env/env.go | 9 ++ pkg/parser/parser.go | 40 +++++- pkg/parser/parser_test.go | 29 ++++ pkg/repos/get.go | 30 ++-- pkg/repos/runtimes/busybox/busybox.go | 5 +- pkg/repos/runtimes/busybox/busybox_test.go | 3 +- pkg/repos/runtimes/golang/golang.go | 8 +- pkg/repos/runtimes/golang/golang_test.go | 3 +- pkg/repos/runtimes/node/node.go | 26 +++- pkg/repos/runtimes/node/node_test.go | 5 +- pkg/repos/runtimes/python/python.go | 29 +++- pkg/repos/runtimes/python/python_test.go | 3 +- pkg/tests/runner_test.go | 21 +++ .../testdata/TestRuntimes/call1-resp.golden | 16 +++ pkg/tests/testdata/TestRuntimes/call1.golden | 37 +++++ .../testdata/TestRuntimes/call2-resp.golden | 16 +++ pkg/tests/testdata/TestRuntimes/call2.golden | 70 +++++++++ .../testdata/TestRuntimes/call3-resp.golden | 16 +++ pkg/tests/testdata/TestRuntimes/call3.golden | 103 +++++++++++++ .../testdata/TestRuntimes/call4-resp.golden | 9 ++ pkg/tests/testdata/TestRuntimes/call4.golden | 136 ++++++++++++++++++ pkg/tests/testdata/TestRuntimes/test.gpt | 58 ++++++++ pkg/tests/tester/runner.go | 2 - pkg/types/tool.go | 5 + 25 files changed, 635 insertions(+), 46 deletions(-) create mode 100644 pkg/tests/testdata/TestRuntimes/call1-resp.golden create mode 100644 pkg/tests/testdata/TestRuntimes/call1.golden create mode 100644 pkg/tests/testdata/TestRuntimes/call2-resp.golden create mode 100644 pkg/tests/testdata/TestRuntimes/call2.golden create mode 100644 pkg/tests/testdata/TestRuntimes/call3-resp.golden create mode 100644 pkg/tests/testdata/TestRuntimes/call3.golden create mode 100644 pkg/tests/testdata/TestRuntimes/call4-resp.golden create mode 100644 pkg/tests/testdata/TestRuntimes/call4.golden create mode 100644 pkg/tests/testdata/TestRuntimes/test.gpt diff --git a/pkg/engine/cmd.go b/pkg/engine/cmd.go index 484b00cb..57dfd477 100644 --- a/pkg/engine/cmd.go +++ b/pkg/engine/cmd.go @@ -282,7 +282,7 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T ) if strings.TrimSpace(rest) != "" { - f, err := os.CreateTemp("", version.ProgramName+requiredFileExtensions[args[0]]) + f, err := os.CreateTemp(env.Getenv("GPTSCRIPT_TMPDIR", envvars), version.ProgramName+requiredFileExtensions[args[0]]) if err != nil { return nil, nil, err } diff --git a/pkg/env/env.go b/pkg/env/env.go index bedd5f9d..2994825b 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -26,6 +26,15 @@ func ToEnvLike(v string) string { return strings.ToUpper(v) } +func Getenv(key string, envs []string) string { + for i := len(envs) - 1; i >= 0; i-- { + if k, v, ok := strings.Cut(envs[i], "="); ok && k == key { + return v + } + } + return "" +} + func Matches(cmd []string, bin string) bool { switch len(cmd) { case 0: diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index ff5d1374..b57cb658 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -16,7 +16,7 @@ import ( var ( sepRegex = regexp.MustCompile(`^\s*---+\s*$`) strictSepRegex = regexp.MustCompile(`^---\n$`) - skipRegex = regexp.MustCompile(`^![-\w]+\s*$`) + skipRegex = regexp.MustCompile(`^![-.:\w]+\s*$`) ) func normalize(key string) string { @@ -308,6 +308,8 @@ func Parse(input io.Reader, opts ...Options) (Document, error) { } } + nodes = assignMetadata(nodes) + if !opt.AssignGlobals { return Document{ Nodes: nodes, @@ -359,6 +361,42 @@ func Parse(input io.Reader, opts ...Options) (Document, error) { }, nil } +func assignMetadata(nodes []Node) (result []Node) { + metadata := map[string]map[string]string{} + result = make([]Node, 0, len(nodes)) + for _, node := range nodes { + if node.TextNode != nil { + body, ok := strings.CutPrefix(node.TextNode.Text, "!metadata:") + if ok { + line, rest, ok := strings.Cut(body, "\n") + if ok { + toolName, metaKey, ok := strings.Cut(strings.TrimSpace(line), ":") + if ok { + d, ok := metadata[toolName] + if !ok { + d = map[string]string{} + metadata[toolName] = d + } + d[metaKey] = strings.TrimSpace(rest) + } + } + } + } + } + if len(metadata) == 0 { + return nodes + } + + for _, node := range nodes { + if node.ToolNode != nil { + node.ToolNode.Tool.MetaData = metadata[node.ToolNode.Tool.Name] + } + result = append(result, node) + } + + return +} + func isGPTScriptHashBang(line string) bool { if !strings.HasPrefix(line, "#!") { return false diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go index 9f682efa..3967ebd5 100644 --- a/pkg/parser/parser_test.go +++ b/pkg/parser/parser_test.go @@ -6,6 +6,7 @@ import ( "github.com/gptscript-ai/gptscript/pkg/types" "github.com/hexops/autogold/v2" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -239,3 +240,31 @@ share output filters: shared }}, }}).Equal(t, out) } + +func TestParseMetaData(t *testing.T) { + input := ` +name: first + +body +--- +!metadata:first:package.json +foo=base +f + +--- +!metadata:first2:requirements.txt +asdf2 + +--- +!metadata:first:requirements.txt +asdf +` + tools, err := ParseTools(strings.NewReader(input)) + require.NoError(t, err) + + assert.Len(t, tools, 1) + autogold.Expect(map[string]string{ + "package.json": "foo=base\nf", + "requirements.txt": "asdf", + }).Equal(t, tools[0].MetaData) +} diff --git a/pkg/repos/get.go b/pkg/repos/get.go index bead4a7a..fc675c58 100644 --- a/pkg/repos/get.go +++ b/pkg/repos/get.go @@ -26,8 +26,8 @@ const credentialHelpersRepo = "github.com/gptscript-ai/gptscript-credential-help type Runtime interface { ID() string - Supports(cmd []string) bool - Setup(ctx context.Context, dataRoot, toolSource string, env []string) ([]string, error) + Supports(tool types.Tool, cmd []string) bool + Setup(ctx context.Context, tool types.Tool, dataRoot, toolSource string, env []string) ([]string, error) } type noopRuntime struct { @@ -37,11 +37,11 @@ func (n noopRuntime) ID() string { return "none" } -func (n noopRuntime) Supports(_ []string) bool { +func (n noopRuntime) Supports(_ types.Tool, _ []string) bool { return false } -func (n noopRuntime) Setup(_ context.Context, _, _ string, _ []string) ([]string, error) { +func (n noopRuntime) Setup(_ context.Context, _ types.Tool, _, _ string, _ []string) ([]string, error) { return nil, nil } @@ -52,7 +52,6 @@ type Manager struct { credHelperDirs credentials.CredentialHelperDirs runtimes []Runtime credHelperConfig *credHelperConfig - supportLocal bool } type credHelperConfig struct { @@ -62,10 +61,6 @@ type credHelperConfig struct { env []string } -func (m *Manager) SetSupportLocal() { - m.supportLocal = true -} - func New(cacheDir string, runtimes ...Runtime) *Manager { root := filepath.Join(cacheDir, "repos") return &Manager{ @@ -216,7 +211,7 @@ func (m *Manager) setup(ctx context.Context, runtime Runtime, tool types.Tool, e } } - newEnv, err := runtime.Setup(ctx, m.runtimeDir, targetFinal, env) + newEnv, err := runtime.Setup(ctx, tool, m.runtimeDir, targetFinal, env) if err != nil { return "", nil, err } @@ -240,17 +235,10 @@ func (m *Manager) setup(ctx context.Context, runtime Runtime, tool types.Tool, e func (m *Manager) GetContext(ctx context.Context, tool types.Tool, cmd, env []string) (string, []string, error) { var isLocal bool - if !m.supportLocal { - if tool.Source.Repo == nil { - return tool.WorkingDir, env, nil - } - - if tool.Source.Repo.VCS != "git" { - return "", nil, fmt.Errorf("only git is supported, found VCS %s for %s", tool.Source.Repo.VCS, tool.ID) - } - } else if tool.Source.Repo == nil { + if tool.Source.Repo == nil { isLocal = true - id := hash.Digest(tool)[:12] + d, _ := json.Marshal(tool) + id := hash.Digest(d)[:12] tool.Source.Repo = &types.Repo{ VCS: "", Root: id, @@ -261,7 +249,7 @@ func (m *Manager) GetContext(ctx context.Context, tool types.Tool, cmd, env []st } for _, runtime := range m.runtimes { - if runtime.Supports(cmd) { + if runtime.Supports(tool, cmd) { log.Debugf("Runtime %s supports %v", runtime.ID(), cmd) return m.setup(ctx, runtime, tool, env) } diff --git a/pkg/repos/runtimes/busybox/busybox.go b/pkg/repos/runtimes/busybox/busybox.go index b0c00a0c..542ba94a 100644 --- a/pkg/repos/runtimes/busybox/busybox.go +++ b/pkg/repos/runtimes/busybox/busybox.go @@ -18,6 +18,7 @@ import ( runtimeEnv "github.com/gptscript-ai/gptscript/pkg/env" "github.com/gptscript-ai/gptscript/pkg/hash" "github.com/gptscript-ai/gptscript/pkg/repos/download" + "github.com/gptscript-ai/gptscript/pkg/types" ) //go:embed SHASUMS256.txt @@ -32,7 +33,7 @@ func (r *Runtime) ID() string { return "busybox" } -func (r *Runtime) Supports(cmd []string) bool { +func (r *Runtime) Supports(_ types.Tool, cmd []string) bool { if runtime.GOOS != "windows" { return false } @@ -44,7 +45,7 @@ func (r *Runtime) Supports(cmd []string) bool { return false } -func (r *Runtime) Setup(ctx context.Context, dataRoot, _ string, env []string) ([]string, error) { +func (r *Runtime) Setup(ctx context.Context, _ types.Tool, dataRoot, _ string, env []string) ([]string, error) { binPath, err := r.getRuntime(ctx, dataRoot) if err != nil { return nil, err diff --git a/pkg/repos/runtimes/busybox/busybox_test.go b/pkg/repos/runtimes/busybox/busybox_test.go index f3add18a..77bfae59 100644 --- a/pkg/repos/runtimes/busybox/busybox_test.go +++ b/pkg/repos/runtimes/busybox/busybox_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/adrg/xdg" + "github.com/gptscript-ai/gptscript/pkg/types" "github.com/samber/lo" "github.com/stretchr/testify/require" ) @@ -31,7 +32,7 @@ func TestRuntime(t *testing.T) { r := Runtime{} - s, err := r.Setup(context.Background(), testCacheHome, "testdata", os.Environ()) + s, err := r.Setup(context.Background(), types.Tool{}, testCacheHome, "testdata", os.Environ()) require.NoError(t, err) _, err = os.Stat(filepath.Join(firstPath(s), "busybox.exe")) if errors.Is(err, fs.ErrNotExist) { diff --git a/pkg/repos/runtimes/golang/golang.go b/pkg/repos/runtimes/golang/golang.go index 28300439..b19cfe90 100644 --- a/pkg/repos/runtimes/golang/golang.go +++ b/pkg/repos/runtimes/golang/golang.go @@ -18,6 +18,7 @@ import ( runtimeEnv "github.com/gptscript-ai/gptscript/pkg/env" "github.com/gptscript-ai/gptscript/pkg/hash" "github.com/gptscript-ai/gptscript/pkg/repos/download" + "github.com/gptscript-ai/gptscript/pkg/types" ) //go:embed digests.txt @@ -34,11 +35,12 @@ func (r *Runtime) ID() string { return "go" + r.Version } -func (r *Runtime) Supports(cmd []string) bool { - return len(cmd) > 0 && cmd[0] == "${GPTSCRIPT_TOOL_DIR}/bin/gptscript-go-tool" +func (r *Runtime) Supports(tool types.Tool, cmd []string) bool { + return tool.Source.IsGit() && + len(cmd) > 0 && cmd[0] == "${GPTSCRIPT_TOOL_DIR}/bin/gptscript-go-tool" } -func (r *Runtime) Setup(ctx context.Context, dataRoot, toolSource string, env []string) ([]string, error) { +func (r *Runtime) Setup(ctx context.Context, _ types.Tool, dataRoot, toolSource string, env []string) ([]string, error) { binPath, err := r.getRuntime(ctx, dataRoot) if err != nil { return nil, err diff --git a/pkg/repos/runtimes/golang/golang_test.go b/pkg/repos/runtimes/golang/golang_test.go index 5f71fb50..56098a51 100644 --- a/pkg/repos/runtimes/golang/golang_test.go +++ b/pkg/repos/runtimes/golang/golang_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/adrg/xdg" + "github.com/gptscript-ai/gptscript/pkg/types" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -27,7 +28,7 @@ func TestRuntime(t *testing.T) { Version: "1.22.1", } - s, err := r.Setup(context.Background(), testCacheHome, "testdata", os.Environ()) + s, err := r.Setup(context.Background(), types.Tool{}, testCacheHome, "testdata", os.Environ()) require.NoError(t, err) p, v, _ := strings.Cut(s[0], "=") v, _, _ = strings.Cut(v, string(filepath.ListSeparator)) diff --git a/pkg/repos/runtimes/node/node.go b/pkg/repos/runtimes/node/node.go index 575e3b23..fde5103d 100644 --- a/pkg/repos/runtimes/node/node.go +++ b/pkg/repos/runtimes/node/node.go @@ -17,12 +17,16 @@ import ( runtimeEnv "github.com/gptscript-ai/gptscript/pkg/env" "github.com/gptscript-ai/gptscript/pkg/hash" "github.com/gptscript-ai/gptscript/pkg/repos/download" + "github.com/gptscript-ai/gptscript/pkg/types" ) //go:embed SHASUMS256.txt.asc var releasesData []byte -const downloadURL = "https://nodejs.org/dist/%s/" +const ( + downloadURL = "https://nodejs.org/dist/%s/" + packageJSON = "package.json" +) type Runtime struct { // version something like "3.12" @@ -35,7 +39,10 @@ func (r *Runtime) ID() string { return "node" + r.Version } -func (r *Runtime) Supports(cmd []string) bool { +func (r *Runtime) Supports(tool types.Tool, cmd []string) bool { + if _, hasPackageJSON := tool.MetaData[packageJSON]; !hasPackageJSON && !tool.Source.IsGit() { + return false + } for _, testCmd := range []string{"node", "npx", "npm"} { if r.supports(testCmd, cmd) { return true @@ -54,17 +61,21 @@ func (r *Runtime) supports(testCmd string, cmd []string) bool { return runtimeEnv.Matches(cmd, testCmd) } -func (r *Runtime) Setup(ctx context.Context, dataRoot, toolSource string, env []string) ([]string, error) { +func (r *Runtime) Setup(ctx context.Context, tool types.Tool, dataRoot, toolSource string, env []string) ([]string, error) { binPath, err := r.getRuntime(ctx, dataRoot) if err != nil { return nil, err } newEnv := runtimeEnv.AppendPath(env, binPath) - if err := r.runNPM(ctx, toolSource, binPath, append(env, newEnv...)); err != nil { + if err := r.runNPM(ctx, tool, toolSource, binPath, append(env, newEnv...)); err != nil { return nil, err } + if _, ok := tool.MetaData[packageJSON]; ok { + newEnv = append(newEnv, "GPTSCRIPT_TMPDIR="+toolSource) + } + return newEnv, nil } @@ -100,11 +111,16 @@ func (r *Runtime) getReleaseAndDigest() (string, string, error) { return "", "", fmt.Errorf("failed to find %s release for os=%s arch=%s", r.ID(), osName(), arch()) } -func (r *Runtime) runNPM(ctx context.Context, toolSource, binDir string, env []string) error { +func (r *Runtime) runNPM(ctx context.Context, tool types.Tool, toolSource, binDir string, env []string) error { log.InfofCtx(ctx, "Running npm in %s", toolSource) cmd := debugcmd.New(ctx, filepath.Join(binDir, "npm"), "install") cmd.Env = env cmd.Dir = toolSource + if contents, ok := tool.MetaData[packageJSON]; ok { + if err := os.WriteFile(filepath.Join(toolSource, packageJSON), []byte(contents+"\n"), 0644); err != nil { + return err + } + } return cmd.Run() } diff --git a/pkg/repos/runtimes/node/node_test.go b/pkg/repos/runtimes/node/node_test.go index 50ef1e0a..014619c8 100644 --- a/pkg/repos/runtimes/node/node_test.go +++ b/pkg/repos/runtimes/node/node_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/adrg/xdg" + "github.com/gptscript-ai/gptscript/pkg/types" "github.com/samber/lo" "github.com/stretchr/testify/require" ) @@ -28,7 +29,7 @@ func TestRuntime(t *testing.T) { Version: "20", } - s, err := r.Setup(context.Background(), testCacheHome, "testdata", os.Environ()) + s, err := r.Setup(context.Background(), types.Tool{}, testCacheHome, "testdata", os.Environ()) require.NoError(t, err) _, err = os.Stat(filepath.Join(firstPath(s), "node.exe")) if errors.Is(err, fs.ErrNotExist) { @@ -42,7 +43,7 @@ func TestRuntime21(t *testing.T) { Version: "21", } - s, err := r.Setup(context.Background(), testCacheHome, "testdata", os.Environ()) + s, err := r.Setup(context.Background(), types.Tool{}, testCacheHome, "testdata", os.Environ()) require.NoError(t, err) _, err = os.Stat(filepath.Join(firstPath(s), "node.exe")) if errors.Is(err, fs.ErrNotExist) { diff --git a/pkg/repos/runtimes/python/python.go b/pkg/repos/runtimes/python/python.go index c031cb16..ae24f92a 100644 --- a/pkg/repos/runtimes/python/python.go +++ b/pkg/repos/runtimes/python/python.go @@ -17,12 +17,16 @@ import ( runtimeEnv "github.com/gptscript-ai/gptscript/pkg/env" "github.com/gptscript-ai/gptscript/pkg/hash" "github.com/gptscript-ai/gptscript/pkg/repos/download" + "github.com/gptscript-ai/gptscript/pkg/types" ) //go:embed python.json var releasesData []byte -const uvVersion = "uv==0.2.27" +const ( + uvVersion = "uv==0.2.33" + requirementsTxt = "requirements.txt" +) type Release struct { OS string `json:"os,omitempty"` @@ -43,7 +47,10 @@ func (r *Runtime) ID() string { return "python" + r.Version } -func (r *Runtime) Supports(cmd []string) bool { +func (r *Runtime) Supports(tool types.Tool, cmd []string) bool { + if _, hasRequirements := tool.MetaData[requirementsTxt]; !hasRequirements && !tool.Source.IsGit() { + return false + } if runtimeEnv.Matches(cmd, r.ID()) { return true } @@ -112,7 +119,7 @@ func (r *Runtime) copyPythonForWindows(binDir string) error { return nil } -func (r *Runtime) Setup(ctx context.Context, dataRoot, toolSource string, env []string) ([]string, error) { +func (r *Runtime) Setup(ctx context.Context, tool types.Tool, dataRoot, toolSource string, env []string) ([]string, error) { binPath, err := r.getRuntime(ctx, dataRoot) if err != nil { return nil, err @@ -145,7 +152,7 @@ func (r *Runtime) Setup(ctx context.Context, dataRoot, toolSource string, env [] } } - if err := r.runPip(ctx, toolSource, binPath, append(env, newEnv...)); err != nil { + if err := r.runPip(ctx, tool, toolSource, binPath, append(env, newEnv...)); err != nil { return nil, err } @@ -170,9 +177,19 @@ func (r *Runtime) getReleaseAndDigest() (string, string, error) { return "", "", fmt.Errorf("failed to find an python runtime for %s", r.Version) } -func (r *Runtime) runPip(ctx context.Context, toolSource, binDir string, env []string) error { +func (r *Runtime) runPip(ctx context.Context, tool types.Tool, toolSource, binDir string, env []string) error { log.InfofCtx(ctx, "Running pip in %s", toolSource) - for _, req := range []string{"requirements-gptscript.txt", "requirements.txt"} { + if content, ok := tool.MetaData[requirementsTxt]; ok { + reqFile := filepath.Join(toolSource, requirementsTxt) + if err := os.WriteFile(reqFile, []byte(content+"\n"), 0644); err != nil { + return err + } + cmd := debugcmd.New(ctx, uvBin(binDir), "pip", "install", "-r", reqFile) + cmd.Env = env + return cmd.Run() + } + + for _, req := range []string{"requirements-gptscript.txt", requirementsTxt} { reqFile := filepath.Join(toolSource, req) if s, err := os.Stat(reqFile); err == nil && !s.IsDir() { cmd := debugcmd.New(ctx, uvBin(binDir), "pip", "install", "-r", reqFile) diff --git a/pkg/repos/runtimes/python/python_test.go b/pkg/repos/runtimes/python/python_test.go index fc2ededc..0e483305 100644 --- a/pkg/repos/runtimes/python/python_test.go +++ b/pkg/repos/runtimes/python/python_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/adrg/xdg" + "github.com/gptscript-ai/gptscript/pkg/types" "github.com/samber/lo" "github.com/stretchr/testify/require" ) @@ -27,7 +28,7 @@ func TestRuntime(t *testing.T) { Version: "3.12", } - s, err := r.Setup(context.Background(), testCacheHome, "testdata", os.Environ()) + s, err := r.Setup(context.Background(), types.Tool{}, testCacheHome, "testdata", os.Environ()) require.NoError(t, err) _, err = os.Stat(filepath.Join(firstPath(s), "python.exe")) if errors.Is(err, os.ErrNotExist) { diff --git a/pkg/tests/runner_test.go b/pkg/tests/runner_test.go index 60f1cfc3..424c84c1 100644 --- a/pkg/tests/runner_test.go +++ b/pkg/tests/runner_test.go @@ -997,3 +997,24 @@ func TestToolRefAll(t *testing.T) { r := tester.NewRunner(t) r.RunDefault() } + +func TestRuntimes(t *testing.T) { + r := tester.NewRunner(t) + r.RespondWith(tester.Result{ + Func: types.CompletionFunctionCall{ + Name: "py", + Arguments: "{}", + }, + }, tester.Result{ + Func: types.CompletionFunctionCall{ + Name: "node", + Arguments: "{}", + }, + }, tester.Result{ + Func: types.CompletionFunctionCall{ + Name: "bash", + Arguments: "{}", + }, + }) + r.RunDefault() +} diff --git a/pkg/tests/testdata/TestRuntimes/call1-resp.golden b/pkg/tests/testdata/TestRuntimes/call1-resp.golden new file mode 100644 index 00000000..1d53670a --- /dev/null +++ b/pkg/tests/testdata/TestRuntimes/call1-resp.golden @@ -0,0 +1,16 @@ +`{ + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + } + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestRuntimes/call1.golden b/pkg/tests/testdata/TestRuntimes/call1.golden new file mode 100644 index 00000000..67c7d9f7 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimes/call1.golden @@ -0,0 +1,37 @@ +`{ + "model": "gpt-4o", + "tools": [ + { + "function": { + "toolID": "testdata/TestRuntimes/test.gpt:py", + "name": "py", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimes/test.gpt:node", + "name": "node", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimes/test.gpt:bash", + "name": "bash", + "parameters": null + } + } + ], + "messages": [ + { + "role": "system", + "content": [ + { + "text": "Dummy" + } + ], + "usage": {} + } + ] +}` diff --git a/pkg/tests/testdata/TestRuntimes/call2-resp.golden b/pkg/tests/testdata/TestRuntimes/call2-resp.golden new file mode 100644 index 00000000..4806793c --- /dev/null +++ b/pkg/tests/testdata/TestRuntimes/call2-resp.golden @@ -0,0 +1,16 @@ +`{ + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "node", + "arguments": "{}" + } + } + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestRuntimes/call2.golden b/pkg/tests/testdata/TestRuntimes/call2.golden new file mode 100644 index 00000000..da456a5d --- /dev/null +++ b/pkg/tests/testdata/TestRuntimes/call2.golden @@ -0,0 +1,70 @@ +`{ + "model": "gpt-4o", + "tools": [ + { + "function": { + "toolID": "testdata/TestRuntimes/test.gpt:py", + "name": "py", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimes/test.gpt:node", + "name": "node", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimes/test.gpt:bash", + "name": "bash", + "parameters": null + } + } + ], + "messages": [ + { + "role": "system", + "content": [ + { + "text": "Dummy" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + } + } + ], + "usage": {} + }, + { + "role": "tool", + "content": [ + { + "text": "py worked\r\n" + } + ], + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + }, + "usage": {} + } + ] +}` diff --git a/pkg/tests/testdata/TestRuntimes/call3-resp.golden b/pkg/tests/testdata/TestRuntimes/call3-resp.golden new file mode 100644 index 00000000..1103f824 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimes/call3-resp.golden @@ -0,0 +1,16 @@ +`{ + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 2, + "id": "call_3", + "function": { + "name": "bash", + "arguments": "{}" + } + } + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestRuntimes/call3.golden b/pkg/tests/testdata/TestRuntimes/call3.golden new file mode 100644 index 00000000..f0792540 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimes/call3.golden @@ -0,0 +1,103 @@ +`{ + "model": "gpt-4o", + "tools": [ + { + "function": { + "toolID": "testdata/TestRuntimes/test.gpt:py", + "name": "py", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimes/test.gpt:node", + "name": "node", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimes/test.gpt:bash", + "name": "bash", + "parameters": null + } + } + ], + "messages": [ + { + "role": "system", + "content": [ + { + "text": "Dummy" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + } + } + ], + "usage": {} + }, + { + "role": "tool", + "content": [ + { + "text": "py worked\r\n" + } + ], + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + }, + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "node", + "arguments": "{}" + } + } + } + ], + "usage": {} + }, + { + "role": "tool", + "content": [ + { + "text": "node worked\n" + } + ], + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "node", + "arguments": "{}" + } + }, + "usage": {} + } + ] +}` diff --git a/pkg/tests/testdata/TestRuntimes/call4-resp.golden b/pkg/tests/testdata/TestRuntimes/call4-resp.golden new file mode 100644 index 00000000..8135a8c9 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimes/call4-resp.golden @@ -0,0 +1,9 @@ +`{ + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 4" + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestRuntimes/call4.golden b/pkg/tests/testdata/TestRuntimes/call4.golden new file mode 100644 index 00000000..04ac31f5 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimes/call4.golden @@ -0,0 +1,136 @@ +`{ + "model": "gpt-4o", + "tools": [ + { + "function": { + "toolID": "testdata/TestRuntimes/test.gpt:py", + "name": "py", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimes/test.gpt:node", + "name": "node", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimes/test.gpt:bash", + "name": "bash", + "parameters": null + } + } + ], + "messages": [ + { + "role": "system", + "content": [ + { + "text": "Dummy" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + } + } + ], + "usage": {} + }, + { + "role": "tool", + "content": [ + { + "text": "py worked\r\n" + } + ], + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + }, + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "node", + "arguments": "{}" + } + } + } + ], + "usage": {} + }, + { + "role": "tool", + "content": [ + { + "text": "node worked\n" + } + ], + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "node", + "arguments": "{}" + } + }, + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 2, + "id": "call_3", + "function": { + "name": "bash", + "arguments": "{}" + } + } + } + ], + "usage": {} + }, + { + "role": "tool", + "content": [ + { + "text": "bash works\n" + } + ], + "toolCall": { + "index": 2, + "id": "call_3", + "function": { + "name": "bash", + "arguments": "{}" + } + }, + "usage": {} + } + ] +}` diff --git a/pkg/tests/testdata/TestRuntimes/test.gpt b/pkg/tests/testdata/TestRuntimes/test.gpt new file mode 100644 index 00000000..db3ede64 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimes/test.gpt @@ -0,0 +1,58 @@ +name: first +tools: py, node, bash + +Dummy + +--- +name: py + +#!/usr/bin/env python3 + +import requests +import platform + +# this is dumb hack to get the line endings to always be \r\n so the golden files match +# on both linux and windows +if platform.system() == 'Windows': + print('py worked') +else: + print('py worked\r') + +--- +!metadata:py:requirements.txt + +requests + +--- +name: node + +#!/usr/bin/env node + +import chalk from 'chalk'; +console.log("node worked") + +--- +!metadata:node:package.json + +{ + "name": "chalk-example", + "version": "1.0.0", + "type": "module", + "description": "A simple example project to demonstrate the use of chalk", + "main": "example.js", + "scripts": { + "start": "node example.js" + }, + "author": "Your Name", + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0" + } + } + +--- +name: bash + +#!/bin/bash + +echo bash works \ No newline at end of file diff --git a/pkg/tests/tester/runner.go b/pkg/tests/tester/runner.go index ef75c0f5..a36c5e91 100644 --- a/pkg/tests/tester/runner.go +++ b/pkg/tests/tester/runner.go @@ -11,7 +11,6 @@ import ( "github.com/adrg/xdg" "github.com/gptscript-ai/gptscript/pkg/credentials" "github.com/gptscript-ai/gptscript/pkg/loader" - "github.com/gptscript-ai/gptscript/pkg/repos" "github.com/gptscript-ai/gptscript/pkg/repos/runtimes" "github.com/gptscript-ai/gptscript/pkg/runner" "github.com/gptscript-ai/gptscript/pkg/types" @@ -178,7 +177,6 @@ func NewRunner(t *testing.T) *Runner { require.NoError(t, err) rm := runtimes.Default(cacheDir) - rm.(*repos.Manager).SetSupportLocal() run, err := runner.New(c, credentials.NoopStore{}, runner.Options{ Sequential: true, diff --git a/pkg/types/tool.go b/pkg/types/tool.go index 54d5d817..61a67fa4 100644 --- a/pkg/types/tool.go +++ b/pkg/types/tool.go @@ -166,6 +166,7 @@ type Tool struct { ID string `json:"id,omitempty"` ToolMapping map[string][]ToolReference `json:"toolMapping,omitempty"` + MetaData map[string]string `json:"metaData,omitempty"` LocalTools map[string]string `json:"localTools,omitempty"` Source ToolSource `json:"source,omitempty"` WorkingDir string `json:"workingDir,omitempty"` @@ -793,6 +794,10 @@ type ToolSource struct { Repo *Repo `json:"repo,omitempty"` } +func (t ToolSource) IsGit() bool { + return t.Repo != nil && t.Repo.VCS == "git" +} + func (t ToolSource) String() string { return fmt.Sprintf("%s:%d", t.Location, t.LineNo) } From d8b1ea856423ce9f8b8b8e98f039a24c0ea24b79 Mon Sep 17 00:00:00 2001 From: Taylor Price Date: Tue, 6 Aug 2024 13:34:30 -0700 Subject: [PATCH 07/83] fix: windows absolute path logic --- pkg/loader/loader.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/loader/loader.go b/pkg/loader/loader.go index 3d2ae8ed..1dfaa0e2 100644 --- a/pkg/loader/loader.go +++ b/pkg/loader/loader.go @@ -68,10 +68,8 @@ func openFile(path string) (io.ReadCloser, bool, error) { } func loadLocal(base *source, name string) (*source, bool, error) { - // We want to keep all strings in / format, and only convert to platform specific when reading - // This is why we use path instead of filepath. filePath := name - if !path.IsAbs(name) { + if !filepath.IsAbs(name) { filePath = path.Join(base.Path, name) } From 4fd8e8a2ad24f48e48f50c65368488c1ea48046b Mon Sep 17 00:00:00 2001 From: Taylor Price Date: Tue, 6 Aug 2024 15:28:08 -0700 Subject: [PATCH 08/83] fix: add comment back in correct place, right above the place we mutate path Signed-off-by: Taylor Price --- pkg/loader/loader.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/loader/loader.go b/pkg/loader/loader.go index 1dfaa0e2..80342f2b 100644 --- a/pkg/loader/loader.go +++ b/pkg/loader/loader.go @@ -70,6 +70,8 @@ func openFile(path string) (io.ReadCloser, bool, error) { func loadLocal(base *source, name string) (*source, bool, error) { filePath := name if !filepath.IsAbs(name) { + // We want to keep all strings in / format, and only convert to platform specific when reading + // This is why we use path instead of filepath. filePath = path.Join(base.Path, name) } From c0507a2c32dd543a43e46526710fab12b81c04f8 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Tue, 6 Aug 2024 21:02:19 -0400 Subject: [PATCH 09/83] feat: allow providers to be restarted if they stop By not caching the client, gptscript is able to restart the provider daemon if it stops. If the daemon is still running, then there is little overhead because the daemon URL is cached and the tool will not be completely reprocessed. The model to provider mapping is still cached so that the client can be recreated when necessary. Signed-off-by: Donnie Adams --- pkg/remote/remote.go | 52 +++++++++++++++----------------------------- 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 6d83e6cc..89863529 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -22,10 +22,9 @@ import ( ) type Client struct { - clientsLock sync.Mutex + modelsLock sync.Mutex cache *cache.Client - clients map[string]*openai.Client - models map[string]*openai.Client + modelToProvider map[string]string runner *runner.Runner envs []string credStore credentials.CredentialStore @@ -43,14 +42,19 @@ func New(r *runner.Runner, envs []string, cache *cache.Client, credStore credent } func (c *Client) Call(ctx context.Context, messageRequest types.CompletionRequest, status chan<- types.CompletionStatus) (*types.CompletionMessage, error) { - c.clientsLock.Lock() - client, ok := c.models[messageRequest.Model] - c.clientsLock.Unlock() + c.modelsLock.Lock() + provider, ok := c.modelToProvider[messageRequest.Model] + c.modelsLock.Unlock() if !ok { return nil, fmt.Errorf("failed to find remote model %s", messageRequest.Model) } + client, err := c.load(ctx, provider) + if err != nil { + return nil, err + } + toolName, modelName := types.SplitToolRef(messageRequest.Model) if modelName == "" { // modelName is empty, then the messageRequest.Model is not of the form 'modelName from provider' @@ -96,19 +100,19 @@ func (c *Client) Supports(ctx context.Context, modelString string) (bool, error) return false, nil } - client, err := c.load(ctx, providerName) + _, err := c.load(ctx, providerName) if err != nil { return false, err } - c.clientsLock.Lock() - defer c.clientsLock.Unlock() + c.modelsLock.Lock() + defer c.modelsLock.Unlock() - if c.models == nil { - c.models = map[string]*openai.Client{} + if c.modelToProvider == nil { + c.modelToProvider = map[string]string{} } - c.models[modelString] = client + c.modelToProvider[modelString] = providerName return true, nil } @@ -141,24 +145,11 @@ func (c *Client) clientFromURL(ctx context.Context, apiURL string) (*openai.Clie } func (c *Client) load(ctx context.Context, toolName string) (*openai.Client, error) { - c.clientsLock.Lock() - defer c.clientsLock.Unlock() - - client, ok := c.clients[toolName] - if ok { - return client, nil - } - - if c.clients == nil { - c.clients = make(map[string]*openai.Client) - } - if isHTTPURL(toolName) { remoteClient, err := c.clientFromURL(ctx, toolName) if err != nil { return nil, err } - c.clients[toolName] = remoteClient return remoteClient, nil } @@ -174,14 +165,8 @@ func (c *Client) load(ctx context.Context, toolName string) (*openai.Client, err return nil, err } - if strings.HasSuffix(url, "/") { - url += "v1" - } else { - url += "/v1" - } - - client, err = openai.NewClient(ctx, c.credStore, openai.Options{ - BaseURL: url, + client, err := openai.NewClient(ctx, c.credStore, openai.Options{ + BaseURL: strings.TrimSuffix(url, "/") + "/v1", Cache: c.cache, CacheKey: prg.EntryToolID, }) @@ -189,7 +174,6 @@ func (c *Client) load(ctx context.Context, toolName string) (*openai.Client, err return nil, err } - c.clients[toolName] = client return client, nil } From 8fc57e02c84adf3a1f6e68cd9d7bfd7d9023023a Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Wed, 7 Aug 2024 11:19:58 -0400 Subject: [PATCH 10/83] feat: allow disabling of the cache when parsing tools Signed-off-by: Donnie Adams --- Makefile | 6 ++++-- pkg/cli/gptscript.go | 2 +- pkg/cli/parse.go | 3 ++- pkg/input/input.go | 4 ++-- pkg/loader/url.go | 6 ++++-- pkg/sdkserver/routes.go | 2 +- pkg/sdkserver/types.go | 3 ++- 7 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index e0d93a1d..5b1b6309 100644 --- a/Makefile +++ b/Makefile @@ -52,12 +52,14 @@ init-docs: docker run --rm --workdir=/docs -v $${PWD}/docs:/docs node:18-buster yarn install # Ensure docs build without errors. Makes sure generated docs are in-sync with CLI. -validate-docs: +validate-docs: gen-docs docker run --rm --workdir=/docs -v $${PWD}/docs:/docs node:18-buster yarn build - go run tools/gendocs/main.go if [ -n "$$(git status --porcelain --untracked-files=no)" ]; then \ git status --porcelain --untracked-files=no; \ echo "Encountered dirty repo!"; \ git diff; \ exit 1 \ ;fi + +gen-docs: + go run tools/gendocs/main.go \ No newline at end of file diff --git a/pkg/cli/gptscript.go b/pkg/cli/gptscript.go index 4458d87b..536e339b 100644 --- a/pkg/cli/gptscript.go +++ b/pkg/cli/gptscript.go @@ -83,7 +83,7 @@ func New() *cobra.Command { root, &Eval{gptscript: root}, &Credential{root: root}, - &Parse{}, + &Parse{gptscript: root}, &Fmt{}, &Getenv{}, &SDKServer{ diff --git a/pkg/cli/parse.go b/pkg/cli/parse.go index 8599bd97..081116c1 100644 --- a/pkg/cli/parse.go +++ b/pkg/cli/parse.go @@ -12,6 +12,7 @@ import ( type Parse struct { PrettyPrint bool `usage:"Indent the json output" short:"p"` + gptscript *GPTScript } func (e *Parse) Customize(cmd *cobra.Command) { @@ -26,7 +27,7 @@ func locationName(l string) string { } func (e *Parse) Run(_ *cobra.Command, args []string) error { - content, err := input.FromLocation(args[0]) + content, err := input.FromLocation(args[0], e.gptscript.DisableCache) if err != nil { return err } diff --git a/pkg/input/input.go b/pkg/input/input.go index 3d480431..f930753f 100644 --- a/pkg/input/input.go +++ b/pkg/input/input.go @@ -55,13 +55,13 @@ func FromFile(file string) (string, error) { } // FromLocation takes a string that can be a file path or a URL to a file and returns the content of that file. -func FromLocation(s string) (string, error) { +func FromLocation(s string, disableCache bool) (string, error) { // Attempt to read the file first, if that fails, try to load the URL. Finally, // return an error if both fail. content, err := FromFile(s) if err != nil { log.Debugf("failed to read file %s (due to %v) attempting to load the URL...", s, err) - content, err = loader.ContentFromURL(s) + content, err = loader.ContentFromURL(s, disableCache) if err != nil { return "", err } diff --git a/pkg/loader/url.go b/pkg/loader/url.go index 41400790..72970546 100644 --- a/pkg/loader/url.go +++ b/pkg/loader/url.go @@ -207,8 +207,10 @@ func getWithDefaults(req *http.Request) ([]byte, string, error) { panic("unreachable") } -func ContentFromURL(url string) (string, error) { - cache, err := cache.New() +func ContentFromURL(url string, disableCache bool) (string, error) { + cache, err := cache.New(cache.Options{ + DisableCache: disableCache, + }) if err != nil { return "", fmt.Errorf("failed to create cache: %w", err) } diff --git a/pkg/sdkserver/routes.go b/pkg/sdkserver/routes.go index 2e709e3f..c0d7a41b 100644 --- a/pkg/sdkserver/routes.go +++ b/pkg/sdkserver/routes.go @@ -227,7 +227,7 @@ func (s *server) parse(w http.ResponseWriter, r *http.Request) { if reqObject.Content != "" { out, err = parser.Parse(strings.NewReader(reqObject.Content), reqObject.Options) } else { - content, loadErr := input.FromLocation(reqObject.File) + content, loadErr := input.FromLocation(reqObject.File, reqObject.DisableCache) if loadErr != nil { logger.Errorf(loadErr.Error()) writeError(logger, w, http.StatusInternalServerError, loadErr) diff --git a/pkg/sdkserver/types.go b/pkg/sdkserver/types.go index 9736f045..ade035b2 100644 --- a/pkg/sdkserver/types.go +++ b/pkg/sdkserver/types.go @@ -86,7 +86,8 @@ type parseRequest struct { parser.Options `json:",inline"` content `json:",inline"` - File string `json:"file"` + DisableCache bool `json:"disableCache"` + File string `json:"file"` } type modelsRequest struct { From dc4dcfce04772d5dae360b5602d3670e9cc5217d Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Wed, 7 Aug 2024 12:37:51 -0400 Subject: [PATCH 11/83] fix: always set GPTSCRIPT_CREDENTIAL_EXPIRATION env var when credentials are used (#727) Signed-off-by: Grant Linville --- pkg/runner/runner.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index c2137bea..a8d88fee 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -968,19 +968,19 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env } else { log.Warnf("Not saving credential for tool %s - credentials will only be saved for tools from GitHub, or tools that use aliases.", toolName) } + } - if c.ExpiresAt != nil && (nearestExpiration == nil || nearestExpiration.After(*c.ExpiresAt)) { - nearestExpiration = c.ExpiresAt - } + if c.ExpiresAt != nil && (nearestExpiration == nil || nearestExpiration.After(*c.ExpiresAt)) { + nearestExpiration = c.ExpiresAt } for k, v := range c.Env { env = append(env, fmt.Sprintf("%s=%s", k, v)) } + } - if nearestExpiration != nil { - env = append(env, fmt.Sprintf("%s=%s", credentials.CredentialExpiration, nearestExpiration.Format(time.RFC3339))) - } + if nearestExpiration != nil { + env = append(env, fmt.Sprintf("%s=%s", credentials.CredentialExpiration, nearestExpiration.Format(time.RFC3339))) } return env, nil From 40cc9785f3e19e7998e3f4fa001c246372917064 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Wed, 7 Aug 2024 09:43:54 -0700 Subject: [PATCH 12/83] bug: change quoting behavior When reading the #! interpreter line variables in quotes "${FOO}" will be evaluated and passed as a single argument to the command. Unquoted variables like ${FOO} may result in multiple argument. For example, if FOO="Hello World" that will result in two arguments "Hello" and "World", where "${FOO}" will result in one argument "Hello World" --- pkg/engine/cmd.go | 93 ++++++++++++++++++++++++++++ pkg/engine/cmd_test.go | 135 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 pkg/engine/cmd_test.go diff --git a/pkg/engine/cmd.go b/pkg/engine/cmd.go index 57dfd477..311c743a 100644 --- a/pkg/engine/cmd.go +++ b/pkg/engine/cmd.go @@ -123,6 +123,9 @@ func (e *Engine) runCommand(ctx Context, tool types.Tool, input string, toolCate } cmd, stop, err := e.newCommand(ctx.Ctx, extraEnv, tool, input) if err != nil { + if toolCategory == NoCategory { + return fmt.Sprintf("ERROR: got (%v) while parsing command", err), nil + } return "", err } defer stop() @@ -268,6 +271,12 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T }) } + // After we determined the interpreter we again interpret the args by env vars + args, err = replaceVariablesForInterpreter(interpreter, envMap) + if err != nil { + return nil, nil, err + } + if runtime.GOOS == "windows" && (args[0] == "/bin/bash" || args[0] == "/bin/sh") { args[0] = path.Base(args[0]) } @@ -314,3 +323,87 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T cmd.Env = compressEnv(envvars) return cmd, stop, nil } + +func replaceVariablesForInterpreter(interpreter string, envMap map[string]string) ([]string, error) { + var parts []string + for i, part := range splitByQuotes(interpreter) { + if i%2 == 0 { + part = os.Expand(part, func(s string) string { + return envMap[s] + }) + // We protect newly resolved env vars from getting replaced when we do the second Expand + // after shlex. Yeah, crazy. I'm guessing this isn't secure, but just trying to avoid a foot gun. + part = os.Expand(part, func(s string) string { + return "${__" + s + "}" + }) + } + parts = append(parts, part) + } + + parts, err := shlex.Split(strings.Join(parts, "")) + if err != nil { + return nil, err + } + + for i, part := range parts { + parts[i] = os.Expand(part, func(s string) string { + if strings.HasPrefix(s, "__") { + return "${" + s[2:] + "}" + } + return envMap[s] + }) + } + + return parts, nil +} + +// splitByQuotes will split a string by parsing matching double quotes (with \ as the escape character). +// The return value conforms to the following properties +// 1. s == strings.Join(result, "") +// 2. Even indexes are strings that were not in quotes. +// 3. Odd indexes are strings that were quoted. +// +// Example: s = `In a "quoted string" quotes can be escaped with \"` +// +// result = [`In a `, `"quoted string"`, ` quotes can be escaped with \"`] +func splitByQuotes(s string) (result []string) { + var ( + buf strings.Builder + inEscape, inQuote bool + ) + + for _, c := range s { + if inEscape { + buf.WriteRune(c) + inEscape = false + continue + } + + switch c { + case '"': + if inQuote { + buf.WriteRune(c) + } + result = append(result, buf.String()) + buf.Reset() + if !inQuote { + buf.WriteRune(c) + } + inQuote = !inQuote + case '\\': + inEscape = true + buf.WriteRune(c) + default: + buf.WriteRune(c) + } + } + + if buf.Len() > 0 { + if inQuote { + result = append(result, "") + } + result = append(result, buf.String()) + } + + return +} diff --git a/pkg/engine/cmd_test.go b/pkg/engine/cmd_test.go new file mode 100644 index 00000000..15f72036 --- /dev/null +++ b/pkg/engine/cmd_test.go @@ -0,0 +1,135 @@ +// File: cmd_test.go +package engine + +import "testing" + +func TestSplitByQuotes(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "NoQuotes", + input: "Hello World", + expected: []string{"Hello World"}, + }, + { + name: "ValidQuote", + input: `"Hello" "World"`, + expected: []string{``, `"Hello"`, ` `, `"World"`}, + }, + { + name: "ValidQuoteWithEscape", + input: `"Hello\" World"`, + expected: []string{``, `"Hello\" World"`}, + }, + { + name: "Nothing", + input: "", + expected: []string{}, + }, + { + name: "SpaceInsideQuote", + input: `"Hello World"`, + expected: []string{``, `"Hello World"`}, + }, + { + name: "SingleChar", + input: "H", + expected: []string{"H"}, + }, + { + name: "SingleQuote", + input: `"Hello`, + expected: []string{``, ``, `"Hello`}, + }, + { + name: "ThreeQuotes", + input: `Test "Hello "World" End\"`, + expected: []string{`Test `, `"Hello "`, `World`, ``, `" End\"`}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := splitByQuotes(tt.input) + if !equal(got, tt.expected) { + t.Errorf("splitByQuotes() = %v, want %v", got, tt.expected) + } + }) + } +} + +// Helper function to assert equality of two string slices. +func equal(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} + +// Testing for replaceVariablesForInterpreter +func TestReplaceVariablesForInterpreter(t *testing.T) { + tests := []struct { + name string + interpreter string + envMap map[string]string + expected []string + shouldFail bool + }{ + { + name: "No quotes", + interpreter: "/bin/bash -c ${COMMAND} tail", + envMap: map[string]string{"COMMAND": "echo Hello!"}, + expected: []string{"/bin/bash", "-c", "echo", "Hello!", "tail"}, + }, + { + name: "Quotes Variables", + interpreter: `/bin/bash -c "${COMMAND}" tail`, + envMap: map[string]string{"COMMAND": "Hello, World!"}, + expected: []string{"/bin/bash", "-c", "Hello, World!", "tail"}, + }, + { + name: "Double escape", + interpreter: `/bin/bash -c "${COMMAND}" ${TWO} tail`, + envMap: map[string]string{ + "COMMAND": "Hello, World!", + "TWO": "${COMMAND}", + }, + expected: []string{"/bin/bash", "-c", "Hello, World!", "${COMMAND}", "tail"}, + }, + { + name: "aws cli issue", + interpreter: "aws ${ARGS}", + envMap: map[string]string{ + "ARGS": `ec2 describe-instances --region us-east-1 --query 'Reservations[*].Instances[*].{Instance:InstanceId,State:State.Name}'`, + }, + expected: []string{ + `aws`, + `ec2`, + `describe-instances`, + `--region`, `us-east-1`, + `--query`, `Reservations[*].Instances[*].{Instance:InstanceId,State:State.Name}`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := replaceVariablesForInterpreter(tt.interpreter, tt.envMap) + if (err != nil) != tt.shouldFail { + t.Errorf("replaceVariablesForInterpreter() error = %v, want %v", err, tt.shouldFail) + return + } + if !equal(got, tt.expected) { + t.Errorf("replaceVariablesForInterpreter() = %v, want %v", got, tt.expected) + } + }) + } +} From ab1768a2177282a1a9b4bd4f2b62066ce42c2087 Mon Sep 17 00:00:00 2001 From: Daishan Peng Date: Wed, 7 Aug 2024 11:20:38 -0700 Subject: [PATCH 13/83] Fix: Fix query not added if there are multple entries Signed-off-by: Daishan Peng --- pkg/openapi/run.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/openapi/run.go b/pkg/openapi/run.go index 17199851..2efc2309 100644 --- a/pkg/openapi/run.go +++ b/pkg/openapi/run.go @@ -177,6 +177,7 @@ func HandleAuths(req *http.Request, envMap map[string]string, infoSets [][]Secur // We're using this info set, because no environment variables were missing. // Set up the request as needed. + v := url.Values{} for _, info := range infoSet { envNames := maps.Values(info.getCredentialNamesAndEnvVars(req.URL.Hostname())) switch info.Type { @@ -185,9 +186,7 @@ func HandleAuths(req *http.Request, envMap map[string]string, infoSets [][]Secur case "header": req.Header.Set(info.APIKeyName, envMap[envNames[0]]) case "query": - v := url.Values{} v.Add(info.APIKeyName, envMap[envNames[0]]) - req.URL.RawQuery = v.Encode() case "cookie": req.AddCookie(&http.Cookie{ Name: info.APIKeyName, @@ -203,6 +202,9 @@ func HandleAuths(req *http.Request, envMap map[string]string, infoSets [][]Secur } } } + if len(v) > 0 { + req.URL.RawQuery = v.Encode() + } return nil } From bb2340bd53e483c7cb9c2ae2b8f6c4919a7fcd5c Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Wed, 7 Aug 2024 16:14:24 -0400 Subject: [PATCH 14/83] fix: make vars capital in smoke tests A recent change in gptscript got rid of the lowercase env vars because they don't work on Windows. This change updates the smoke tests. Signed-off-by: Donnie Adams --- pkg/tests/smoke/testdata/Bob/test.gpt | 2 +- pkg/tests/smoke/testdata/BobAsShell/test.gpt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tests/smoke/testdata/Bob/test.gpt b/pkg/tests/smoke/testdata/Bob/test.gpt index fe8ffb62..20f533e2 100644 --- a/pkg/tests/smoke/testdata/Bob/test.gpt +++ b/pkg/tests/smoke/testdata/Bob/test.gpt @@ -7,4 +7,4 @@ name: bob description: I'm Bob, a friendly guy. args: question: The question to ask Bob. -When asked how I am doing, respond with exactly "Thanks for asking "${question}", I'm doing great fellow friendly AI tool!" +When asked how I am doing, respond with exactly "Thanks for asking "${QUESTION}", I'm doing great fellow friendly AI tool!" diff --git a/pkg/tests/smoke/testdata/BobAsShell/test.gpt b/pkg/tests/smoke/testdata/BobAsShell/test.gpt index f04920bf..a0edb9c4 100644 --- a/pkg/tests/smoke/testdata/BobAsShell/test.gpt +++ b/pkg/tests/smoke/testdata/BobAsShell/test.gpt @@ -10,4 +10,4 @@ args: question: The question to ask Bob. #!/bin/bash -echo "Thanks for asking ${question}, I'm doing great fellow friendly AI tool!" +echo "Thanks for asking ${QUESTION}, I'm doing great fellow friendly AI tool!" From 04f42cdc4b1aacf82241eb62b1e545bba014269c Mon Sep 17 00:00:00 2001 From: Nick Hale <4175918+njhale@users.noreply.github.com> Date: Wed, 7 Aug 2024 23:25:33 -0400 Subject: [PATCH 15/83] chore: pin ui tool version to v0.9.4 Signed-off-by: Nick Hale <4175918+njhale@users.noreply.github.com> --- pkg/cli/gptscript.go | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/pkg/cli/gptscript.go b/pkg/cli/gptscript.go index 536e339b..d7221715 100644 --- a/pkg/cli/gptscript.go +++ b/pkg/cli/gptscript.go @@ -386,7 +386,9 @@ func (r *GPTScript) Run(cmd *cobra.Command, args []string) (retErr error) { // The UI must run in daemon mode. r.Daemon = true // Use the UI tool as the first argument. - args = append([]string{uiTool()}, args...) + args = append([]string{ + env.VarOrDefault("GPTSCRIPT_CHAT_UI_TOOL", "github.com/gptscript-ai/ui@v0.9.4"), + }, args...) } ctx := cmd.Context() @@ -503,15 +505,3 @@ func (r *GPTScript) Run(cmd *cobra.Command, args []string) (retErr error) { return r.PrintOutput(toolInput, s) } - -// uiTool returns the versioned UI tool reference for the current GPTScript version. -// For release versions, a reference with a matching release tag is returned. -// For all other versions, a reference to main is returned. -func uiTool() string { - ref := "github.com/gptscript-ai/ui" - if tag := version.Tag; !strings.Contains(tag, "v0.0.0-dev") { - ref = fmt.Sprintf("%s@%s", ref, tag) - } - - return env.VarOrDefault("GPTSCRIPT_CHAT_UI_TOOL", ref) -} From 26476d27a34513b123301ba382e07372d1bc6a18 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Thu, 8 Aug 2024 12:43:50 -0700 Subject: [PATCH 16/83] chore: move metadata field to tooldef struct --- pkg/types/tool.go | 21 ++++++++++++++++++--- pkg/types/tool_test.go | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/pkg/types/tool.go b/pkg/types/tool.go index 61a67fa4..bb49e6f1 100644 --- a/pkg/types/tool.go +++ b/pkg/types/tool.go @@ -157,8 +157,9 @@ func (p Parameters) ToolRefNames() []string { type ToolDef struct { Parameters `json:",inline"` - Instructions string `json:"instructions,omitempty"` - BuiltinFunc BuiltinFunc `json:"-"` + Instructions string `json:"instructions,omitempty"` + BuiltinFunc BuiltinFunc `json:"-"` + MetaData map[string]string `json:"metaData,omitempty"` } type Tool struct { @@ -166,7 +167,6 @@ type Tool struct { ID string `json:"id,omitempty"` ToolMapping map[string][]ToolReference `json:"toolMapping,omitempty"` - MetaData map[string]string `json:"metaData,omitempty"` LocalTools map[string]string `json:"localTools,omitempty"` Source ToolSource `json:"source,omitempty"` WorkingDir string `json:"workingDir,omitempty"` @@ -489,6 +489,21 @@ func (t ToolDef) String() string { _, _ = fmt.Fprintln(buf, t.Instructions) } + if t.Name != "" { + keys := maps.Keys(t.MetaData) + sort.Strings(keys) + for _, key := range keys { + buf.WriteString("---\n") + buf.WriteString("!metadata:") + buf.WriteString(t.Name) + buf.WriteString(":") + buf.WriteString(key) + buf.WriteString("\n") + buf.WriteString(t.MetaData[key]) + buf.WriteString("\n") + } + } + return buf.String() } diff --git a/pkg/types/tool_test.go b/pkg/types/tool_test.go index a47014a1..e95c2248 100644 --- a/pkg/types/tool_test.go +++ b/pkg/types/tool_test.go @@ -36,6 +36,13 @@ func TestToolDef_String(t *testing.T) { ExportCredentials: []string{"ExportCredential1", "ExportCredential2"}, Type: ToolTypeContext, }, + MetaData: map[string]string{ + "package.json": `{ +// blah blah some ugly JSON +} +`, + "requirements.txt": `requests=5`, + }, Instructions: "This is a sample instruction", } @@ -68,6 +75,15 @@ Share Credential: ExportCredential2 Chat: true This is a sample instruction +--- +!metadata:Tool Sample:package.json +{ +// blah blah some ugly JSON +} + +--- +!metadata:Tool Sample:requirements.txt +requests=5 `).Equal(t, tool.String()) } From 227d8530ad3a4490145a61eea9eeeefb6043f625 Mon Sep 17 00:00:00 2001 From: Nick Hale <4175918+njhale@users.noreply.github.com> Date: Thu, 8 Aug 2024 21:13:25 -0400 Subject: [PATCH 17/83] chore: remove ui dispatch from release workflow The UI must be updated independently from now on for electron releases to work properly. Signed-off-by: Nick Hale <4175918+njhale@users.noreply.github.com> --- .github/workflows/release.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4c1c22bf..f710e953 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,13 +12,6 @@ jobs: release-tag: runs-on: ubuntu-22.04 steps: - - name: trigger ui repo tag workflow - uses: peter-evans/repository-dispatch@v3 - with: - token: ${{ secrets.DISPATCH_PAT }} - repository: gptscript-ai/ui - event-type: release - client-payload: '{"tag": "${{ github.ref_name }}"}' - name: Checkout uses: actions/checkout@v4 with: From 2e51afe0ac4ee2875c7693c1d8d5c439e29abf59 Mon Sep 17 00:00:00 2001 From: John Engelman Date: Mon, 24 Jun 2024 13:57:26 +0530 Subject: [PATCH 18/83] feat: Add support for tools from github enterprise. --- .../04-command-line-reference/gptscript.md | 65 +++++++------- pkg/cli/gptscript.go | 44 ++++++---- pkg/loader/github/github.go | 87 ++++++++++++++----- pkg/loader/github/github_test.go | 57 ++++++++++++ 4 files changed, 178 insertions(+), 75 deletions(-) diff --git a/docs/docs/04-command-line-reference/gptscript.md b/docs/docs/04-command-line-reference/gptscript.md index de29a97f..b7de5e86 100644 --- a/docs/docs/04-command-line-reference/gptscript.md +++ b/docs/docs/04-command-line-reference/gptscript.md @@ -12,38 +12,39 @@ gptscript [flags] PROGRAM_FILE [INPUT...] ### Options ``` - --cache-dir string Directory to store cache (default: $XDG_CACHE_HOME/gptscript) ($GPTSCRIPT_CACHE_DIR) - --chat-state string The chat state to continue, or null to start a new chat and return the state ($GPTSCRIPT_CHAT_STATE) - -C, --chdir string Change current working directory ($GPTSCRIPT_CHDIR) - --color Use color in output (default true) ($GPTSCRIPT_COLOR) - --config string Path to GPTScript config file ($GPTSCRIPT_CONFIG) - --confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM) - --credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default") - --credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE) - --debug Enable debug logging ($GPTSCRIPT_DEBUG) - --debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES) - --default-model string Default LLM model to use ($GPTSCRIPT_DEFAULT_MODEL) (default "gpt-4o") - --default-model-provider string Default LLM model provider to use, this will override OpenAI settings ($GPTSCRIPT_DEFAULT_MODEL_PROVIDER) - --disable-cache Disable caching of LLM API responses ($GPTSCRIPT_DISABLE_CACHE) - --disable-tui Don't use chat TUI but instead verbose output ($GPTSCRIPT_DISABLE_TUI) - --dump-state string Dump the internal execution state to a file ($GPTSCRIPT_DUMP_STATE) - --events-stream-to string Stream events to this location, could be a file descriptor/handle (e.g. fd://2), filename, or named pipe (e.g. \\.\pipe\my-pipe) ($GPTSCRIPT_EVENTS_STREAM_TO) - --force-chat Force an interactive chat session if even the top level tool is not a chat tool ($GPTSCRIPT_FORCE_CHAT) - --force-sequential Force parallel calls to run sequentially ($GPTSCRIPT_FORCE_SEQUENTIAL) - -h, --help help for gptscript - -f, --input string Read input from a file ("-" for stdin) ($GPTSCRIPT_INPUT_FILE) - --list-models List the models available and exit ($GPTSCRIPT_LIST_MODELS) - --list-tools List built-in tools and exit ($GPTSCRIPT_LIST_TOOLS) - --no-trunc Do not truncate long log messages ($GPTSCRIPT_NO_TRUNC) - --openai-api-key string OpenAI API KEY ($OPENAI_API_KEY) - --openai-base-url string OpenAI base URL ($OPENAI_BASE_URL) - --openai-org-id string OpenAI organization ID ($OPENAI_ORG_ID) - -o, --output string Save output to a file, or - for stdout ($GPTSCRIPT_OUTPUT) - -q, --quiet No output logging (set --quiet=false to force on even when there is no TTY) ($GPTSCRIPT_QUIET) - --save-chat-state-file string A file to save the chat state to so that a conversation can be resumed with --chat-state ($GPTSCRIPT_SAVE_CHAT_STATE_FILE) - --sub-tool string Use tool of this name, not the first tool in file ($GPTSCRIPT_SUB_TOOL) - --ui Launch the UI ($GPTSCRIPT_UI) - --workspace string Directory to use for the workspace, if specified it will not be deleted on exit ($GPTSCRIPT_WORKSPACE) + --cache-dir string Directory to store cache (default: $XDG_CACHE_HOME/gptscript) ($GPTSCRIPT_CACHE_DIR) + --chat-state string The chat state to continue, or null to start a new chat and return the state ($GPTSCRIPT_CHAT_STATE) + -C, --chdir string Change current working directory ($GPTSCRIPT_CHDIR) + --color Use color in output (default true) ($GPTSCRIPT_COLOR) + --config string Path to GPTScript config file ($GPTSCRIPT_CONFIG) + --confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM) + --credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default") + --credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE) + --debug Enable debug logging ($GPTSCRIPT_DEBUG) + --debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES) + --default-model string Default LLM model to use ($GPTSCRIPT_DEFAULT_MODEL) (default "gpt-4o") + --default-model-provider string Default LLM model provider to use, this will override OpenAI settings ($GPTSCRIPT_DEFAULT_MODEL_PROVIDER) + --disable-cache Disable caching of LLM API responses ($GPTSCRIPT_DISABLE_CACHE) + --disable-tui Don't use chat TUI but instead verbose output ($GPTSCRIPT_DISABLE_TUI) + --dump-state string Dump the internal execution state to a file ($GPTSCRIPT_DUMP_STATE) + --events-stream-to string Stream events to this location, could be a file descriptor/handle (e.g. fd://2), filename, or named pipe (e.g. \\.\pipe\my-pipe) ($GPTSCRIPT_EVENTS_STREAM_TO) + --force-chat Force an interactive chat session if even the top level tool is not a chat tool ($GPTSCRIPT_FORCE_CHAT) + --force-sequential Force parallel calls to run sequentially ($GPTSCRIPT_FORCE_SEQUENTIAL) + --github-enterprise-hostname string The host name for a Github Enterprise instance to enable for remote loading ($GPTSCRIPT_GITHUB_ENTERPRISE_HOSTNAME) + -h, --help help for gptscript + -f, --input string Read input from a file ("-" for stdin) ($GPTSCRIPT_INPUT_FILE) + --list-models List the models available and exit ($GPTSCRIPT_LIST_MODELS) + --list-tools List built-in tools and exit ($GPTSCRIPT_LIST_TOOLS) + --no-trunc Do not truncate long log messages ($GPTSCRIPT_NO_TRUNC) + --openai-api-key string OpenAI API KEY ($OPENAI_API_KEY) + --openai-base-url string OpenAI base URL ($OPENAI_BASE_URL) + --openai-org-id string OpenAI organization ID ($OPENAI_ORG_ID) + -o, --output string Save output to a file, or - for stdout ($GPTSCRIPT_OUTPUT) + -q, --quiet No output logging (set --quiet=false to force on even when there is no TTY) ($GPTSCRIPT_QUIET) + --save-chat-state-file string A file to save the chat state to so that a conversation can be resumed with --chat-state ($GPTSCRIPT_SAVE_CHAT_STATE_FILE) + --sub-tool string Use tool of this name, not the first tool in file ($GPTSCRIPT_SUB_TOOL) + --ui Launch the UI ($GPTSCRIPT_UI) + --workspace string Directory to use for the workspace, if specified it will not be deleted on exit ($GPTSCRIPT_WORKSPACE) ``` ### SEE ALSO diff --git a/pkg/cli/gptscript.go b/pkg/cli/gptscript.go index 4458d87b..aafdeacf 100644 --- a/pkg/cli/gptscript.go +++ b/pkg/cli/gptscript.go @@ -23,6 +23,7 @@ import ( "github.com/gptscript-ai/gptscript/pkg/gptscript" "github.com/gptscript-ai/gptscript/pkg/input" "github.com/gptscript-ai/gptscript/pkg/loader" + "github.com/gptscript-ai/gptscript/pkg/loader/github" "github.com/gptscript-ai/gptscript/pkg/monitor" "github.com/gptscript-ai/gptscript/pkg/mvl" "github.com/gptscript-ai/gptscript/pkg/openai" @@ -54,25 +55,26 @@ type GPTScript struct { Output string `usage:"Save output to a file, or - for stdout" short:"o"` EventsStreamTo string `usage:"Stream events to this location, could be a file descriptor/handle (e.g. fd://2), filename, or named pipe (e.g. \\\\.\\pipe\\my-pipe)" name:"events-stream-to"` // Input should not be using GPTSCRIPT_INPUT env var because that is the same value that is set in tool executions - Input string `usage:"Read input from a file (\"-\" for stdin)" short:"f" env:"GPTSCRIPT_INPUT_FILE"` - SubTool string `usage:"Use tool of this name, not the first tool in file" local:"true"` - Assemble bool `usage:"Assemble tool to a single artifact, saved to --output" hidden:"true" local:"true"` - ListModels bool `usage:"List the models available and exit" local:"true"` - ListTools bool `usage:"List built-in tools and exit" local:"true"` - ListenAddress string `usage:"Server listen address" default:"127.0.0.1:0" hidden:"true"` - Chdir string `usage:"Change current working directory" short:"C"` - Daemon bool `usage:"Run tool as a daemon" local:"true" hidden:"true"` - Ports string `usage:"The port range to use for ephemeral daemon ports (ex: 11000-12000)" hidden:"true"` - CredentialContext string `usage:"Context name in which to store credentials" default:"default"` - CredentialOverride []string `usage:"Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234)"` - ChatState string `usage:"The chat state to continue, or null to start a new chat and return the state" local:"true"` - ForceChat bool `usage:"Force an interactive chat session if even the top level tool is not a chat tool" local:"true"` - ForceSequential bool `usage:"Force parallel calls to run sequentially" local:"true"` - Workspace string `usage:"Directory to use for the workspace, if specified it will not be deleted on exit"` - UI bool `usage:"Launch the UI" local:"true" name:"ui"` - DisableTUI bool `usage:"Don't use chat TUI but instead verbose output" local:"true" name:"disable-tui"` - SaveChatStateFile string `usage:"A file to save the chat state to so that a conversation can be resumed with --chat-state" local:"true"` - DefaultModelProvider string `usage:"Default LLM model provider to use, this will override OpenAI settings"` + Input string `usage:"Read input from a file (\"-\" for stdin)" short:"f" env:"GPTSCRIPT_INPUT_FILE"` + SubTool string `usage:"Use tool of this name, not the first tool in file" local:"true"` + Assemble bool `usage:"Assemble tool to a single artifact, saved to --output" hidden:"true" local:"true"` + ListModels bool `usage:"List the models available and exit" local:"true"` + ListTools bool `usage:"List built-in tools and exit" local:"true"` + ListenAddress string `usage:"Server listen address" default:"127.0.0.1:0" hidden:"true"` + Chdir string `usage:"Change current working directory" short:"C"` + Daemon bool `usage:"Run tool as a daemon" local:"true" hidden:"true"` + Ports string `usage:"The port range to use for ephemeral daemon ports (ex: 11000-12000)" hidden:"true"` + CredentialContext string `usage:"Context name in which to store credentials" default:"default"` + CredentialOverride []string `usage:"Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234)"` + ChatState string `usage:"The chat state to continue, or null to start a new chat and return the state" local:"true"` + ForceChat bool `usage:"Force an interactive chat session if even the top level tool is not a chat tool" local:"true"` + ForceSequential bool `usage:"Force parallel calls to run sequentially" local:"true"` + Workspace string `usage:"Directory to use for the workspace, if specified it will not be deleted on exit"` + UI bool `usage:"Launch the UI" local:"true" name:"ui"` + DisableTUI bool `usage:"Don't use chat TUI but instead verbose output" local:"true" name:"disable-tui"` + SaveChatStateFile string `usage:"A file to save the chat state to so that a conversation can be resumed with --chat-state" local:"true"` + DefaultModelProvider string `usage:"Default LLM model provider to use, this will override OpenAI settings"` + GithubEnterpriseHostname string `usage:"The host name for a Github Enterprise instance to enable for remote loading" local:"true"` readData []byte } @@ -334,6 +336,10 @@ func (r *GPTScript) Run(cmd *cobra.Command, args []string) (retErr error) { return err } + if r.GithubEnterpriseHostname != "" { + loader.AddVSC(github.LoaderForPrefix(r.GithubEnterpriseHostname)) + } + // If the user is trying to launch the chat-builder UI, then set up the tool and options here. if r.UI { if os.Getenv(system.BinEnvVar) == "" { diff --git a/pkg/loader/github/github.go b/pkg/loader/github/github.go index 2fb01c3d..7b6e79ec 100644 --- a/pkg/loader/github/github.go +++ b/pkg/loader/github/github.go @@ -2,6 +2,7 @@ package github import ( "context" + "crypto/tls" "encoding/json" "fmt" "io" @@ -18,52 +19,63 @@ import ( "github.com/gptscript-ai/gptscript/pkg/types" ) -const ( - GithubPrefix = "github.com/" - githubRepoURL = "https://github.com/%s/%s.git" - githubDownloadURL = "https://raw.githubusercontent.com/%s/%s/%s/%s" - githubCommitURL = "https://api.github.com/repos/%s/%s/commits/%s" -) +type Config struct { + Prefix string + RepoURL string + DownloadURL string + CommitURL string + AuthToken string +} var ( - githubAuthToken = os.Getenv("GITHUB_AUTH_TOKEN") - log = mvl.Package() + log = mvl.Package() + defaultGithubConfig = &Config{ + Prefix: "github.com/", + RepoURL: "https://github.com/%s/%s.git", + DownloadURL: "https://raw.githubusercontent.com/%s/%s/%s/%s", + CommitURL: "https://api.github.com/repos/%s/%s/commits/%s", + AuthToken: os.Getenv("GITHUB_AUTH_TOKEN"), + } ) func init() { loader.AddVSC(Load) } -func getCommitLsRemote(ctx context.Context, account, repo, ref string) (string, error) { - url := fmt.Sprintf(githubRepoURL, account, repo) +func getCommitLsRemote(ctx context.Context, account, repo, ref string, config *Config) (string, error) { + url := fmt.Sprintf(config.RepoURL, account, repo) return git.LsRemote(ctx, url, ref) } // regexp to match a git commit id var commitRegexp = regexp.MustCompile("^[a-f0-9]{40}$") -func getCommit(ctx context.Context, account, repo, ref string) (string, error) { +func getCommit(ctx context.Context, account, repo, ref string, config *Config) (string, error) { if commitRegexp.MatchString(ref) { return ref, nil } - url := fmt.Sprintf(githubCommitURL, account, repo, ref) + url := fmt.Sprintf(config.CommitURL, account, repo, ref) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return "", fmt.Errorf("failed to create request of %s/%s at %s: %w", account, repo, url, err) } - if githubAuthToken != "" { - req.Header.Add("Authorization", "Bearer "+githubAuthToken) + if config.AuthToken != "" { + req.Header.Add("Authorization", "Bearer "+config.AuthToken) } - resp, err := http.DefaultClient.Do(req) + client := http.DefaultClient + if req.Host == config.Prefix && strings.ToLower(os.Getenv("GH_ENTERPRISE_SKIP_VERIFY")) == "true" { + client = &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} + } + resp, err := client.Do(req) if err != nil { return "", err } else if resp.StatusCode != http.StatusOK { c, _ := io.ReadAll(resp.Body) resp.Body.Close() - commit, fallBackErr := getCommitLsRemote(ctx, account, repo, ref) + commit, fallBackErr := getCommitLsRemote(ctx, account, repo, ref, config) if fallBackErr == nil { return commit, nil } @@ -88,8 +100,28 @@ func getCommit(ctx context.Context, account, repo, ref string) (string, error) { return commit.SHA, nil } -func Load(ctx context.Context, _ *cache.Client, urlName string) (string, string, *types.Repo, bool, error) { - if !strings.HasPrefix(urlName, GithubPrefix) { +func LoaderForPrefix(prefix string) func(context.Context, *cache.Client, string) (string, string, *types.Repo, bool, error) { + return func(ctx context.Context, c *cache.Client, urlName string) (string, string, *types.Repo, bool, error) { + return LoadWithConfig(ctx, c, urlName, NewGithubEnterpriseConfig(prefix)) + } +} + +func Load(ctx context.Context, c *cache.Client, urlName string) (string, string, *types.Repo, bool, error) { + return LoadWithConfig(ctx, c, urlName, defaultGithubConfig) +} + +func NewGithubEnterpriseConfig(prefix string) *Config { + return &Config{ + Prefix: prefix, + RepoURL: fmt.Sprintf("https://%s/%%s/%%s.git", prefix), + DownloadURL: fmt.Sprintf("https://raw.%s/%%s/%%s/%%s/%%s", prefix), + CommitURL: fmt.Sprintf("https://%s/api/v3/repos/%%s/%%s/commits/%%s", prefix), + AuthToken: os.Getenv("GH_ENTERPRISE_TOKEN"), + } +} + +func LoadWithConfig(ctx context.Context, _ *cache.Client, urlName string, config *Config) (string, string, *types.Repo, bool, error) { + if !strings.HasPrefix(urlName, config.Prefix) { return "", "", nil, false, nil } @@ -107,12 +139,12 @@ func Load(ctx context.Context, _ *cache.Client, urlName string) (string, string, account, repo := parts[1], parts[2] path := strings.Join(parts[3:], "/") - ref, err := getCommit(ctx, account, repo, ref) + ref, err := getCommit(ctx, account, repo, ref, config) if err != nil { return "", "", nil, false, err } - downloadURL := fmt.Sprintf(githubDownloadURL, account, repo, ref, path) + downloadURL := fmt.Sprintf(config.DownloadURL, account, repo, ref, path) if path == "" || path == "/" || !strings.Contains(parts[len(parts)-1], ".") { var ( testPath string @@ -124,13 +156,20 @@ func Load(ctx context.Context, _ *cache.Client, urlName string) (string, string, } else { testPath = path + "/" + ext } - testURL = fmt.Sprintf(githubDownloadURL, account, repo, ref, testPath) + testURL = fmt.Sprintf(config.DownloadURL, account, repo, ref, testPath) if i == len(types.DefaultFiles)-1 { // no reason to test the last one, we are just going to use it. Being that the default list is only // two elements this loop could have been one check, but hey over-engineered code ftw. break } - if resp, err := http.Head(testURL); err == nil { + headReq, err := http.NewRequest("HEAD", testURL, nil) + if err != nil { + break + } + if config.AuthToken != "" { + headReq.Header.Add("Authorization", "Bearer "+config.AuthToken) + } + if resp, err := http.DefaultClient.Do(headReq); err == nil { _ = resp.Body.Close() if resp.StatusCode == 200 { break @@ -141,9 +180,9 @@ func Load(ctx context.Context, _ *cache.Client, urlName string) (string, string, path = testPath } - return downloadURL, githubAuthToken, &types.Repo{ + return downloadURL, config.AuthToken, &types.Repo{ VCS: "git", - Root: fmt.Sprintf(githubRepoURL, account, repo), + Root: fmt.Sprintf(config.RepoURL, account, repo), Path: gpath.Dir(path), Name: gpath.Base(path), Revision: ref, diff --git a/pkg/loader/github/github_test.go b/pkg/loader/github/github_test.go index d627ee5e..483722bc 100644 --- a/pkg/loader/github/github_test.go +++ b/pkg/loader/github/github_test.go @@ -2,6 +2,10 @@ package github import ( "context" + "fmt" + "net/http" + "net/http/httptest" + "os" "testing" "github.com/gptscript-ai/gptscript/pkg/types" @@ -44,3 +48,56 @@ func TestLoad(t *testing.T) { Revision: "172dfb00b48c6adbbaa7e99270933f95887d1b91", }).Equal(t, repo) } + +func TestLoad_GithubEnterprise(t *testing.T) { + gheToken := "mytoken" + os.Setenv("GH_ENTERPRISE_SKIP_VERIFY", "true") + os.Setenv("GH_ENTERPRISE_TOKEN", gheToken) + s := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v3/repos/gptscript-ai/gptscript/commits/172dfb0": + _, _ = w.Write([]byte(`{"sha": "172dfb00b48c6adbbaa7e99270933f95887d1b91"}`)) + default: + w.WriteHeader(404) + } + })) + defer s.Close() + + serverAddr := s.Listener.Addr().String() + + url, token, repo, ok, err := LoadWithConfig(context.Background(), nil, fmt.Sprintf("%s/gptscript-ai/gptscript/pkg/loader/testdata/tool@172dfb0", serverAddr), NewGithubEnterpriseConfig(serverAddr)) + require.NoError(t, err) + assert.True(t, ok) + autogold.Expect(fmt.Sprintf("https://raw.%s/gptscript-ai/gptscript/172dfb00b48c6adbbaa7e99270933f95887d1b91/pkg/loader/testdata/tool/tool.gpt", serverAddr)).Equal(t, url) + autogold.Expect(&types.Repo{ + VCS: "git", Root: fmt.Sprintf("https://%s/gptscript-ai/gptscript.git", serverAddr), + Path: "pkg/loader/testdata/tool", + Name: "tool.gpt", + Revision: "172dfb00b48c6adbbaa7e99270933f95887d1b91", + }).Equal(t, repo) + autogold.Expect(gheToken).Equal(t, token) + + url, token, repo, ok, err = Load(context.Background(), nil, "github.com/gptscript-ai/gptscript/pkg/loader/testdata/agent@172dfb0") + require.NoError(t, err) + assert.True(t, ok) + autogold.Expect("https://raw.githubusercontent.com/gptscript-ai/gptscript/172dfb00b48c6adbbaa7e99270933f95887d1b91/pkg/loader/testdata/agent/agent.gpt").Equal(t, url) + autogold.Expect(&types.Repo{ + VCS: "git", Root: "https://github.com/gptscript-ai/gptscript.git", + Path: "pkg/loader/testdata/agent", + Name: "agent.gpt", + Revision: "172dfb00b48c6adbbaa7e99270933f95887d1b91", + }).Equal(t, repo) + autogold.Expect("").Equal(t, token) + + url, token, repo, ok, err = Load(context.Background(), nil, "github.com/gptscript-ai/gptscript/pkg/loader/testdata/bothtoolagent@172dfb0") + require.NoError(t, err) + assert.True(t, ok) + autogold.Expect("https://raw.githubusercontent.com/gptscript-ai/gptscript/172dfb00b48c6adbbaa7e99270933f95887d1b91/pkg/loader/testdata/bothtoolagent/agent.gpt").Equal(t, url) + autogold.Expect(&types.Repo{ + VCS: "git", Root: "https://github.com/gptscript-ai/gptscript.git", + Path: "pkg/loader/testdata/bothtoolagent", + Name: "agent.gpt", + Revision: "172dfb00b48c6adbbaa7e99270933f95887d1b91", + }).Equal(t, repo) + autogold.Expect("").Equal(t, token) +} From b8071a888f1bd376da00ffc911db5ec5fce89f2d Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Fri, 9 Aug 2024 21:45:38 -0400 Subject: [PATCH 19/83] fix: use the default model provider when listing models Signed-off-by: Donnie Adams --- pkg/cli/gptscript.go | 3 +++ pkg/sdkserver/routes.go | 4 ++++ pkg/server/server.go | 3 ++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/cli/gptscript.go b/pkg/cli/gptscript.go index 4926c6fd..2d7e90d9 100644 --- a/pkg/cli/gptscript.go +++ b/pkg/cli/gptscript.go @@ -406,6 +406,9 @@ func (r *GPTScript) Run(cmd *cobra.Command, args []string) (retErr error) { defer gptScript.Close(true) if r.ListModels { + if r.DefaultModelProvider != "" { + args = append(args, r.DefaultModelProvider) + } return r.listModels(ctx, gptScript, args) } diff --git a/pkg/sdkserver/routes.go b/pkg/sdkserver/routes.go index c0d7a41b..98957624 100644 --- a/pkg/sdkserver/routes.go +++ b/pkg/sdkserver/routes.go @@ -127,6 +127,10 @@ func (s *server) listModels(w http.ResponseWriter, r *http.Request) { providers = reqObject.Providers } + if s.gptscriptOpts.DefaultModelProvider != "" { + providers = append(providers, s.gptscriptOpts.DefaultModelProvider) + } + out, err := s.client.ListModels(r.Context(), providers...) if err != nil { writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to list models: %w", err)) diff --git a/pkg/server/server.go b/pkg/server/server.go index d1c57100..00734c38 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -24,5 +24,6 @@ func ContextWithNewRunID(ctx context.Context) context.Context { } func RunIDFromContext(ctx context.Context) string { - return ctx.Value(execKey{}).(string) + runID, _ := ctx.Value(execKey{}).(string) + return runID } From a383d4f513400b5af0ce69217373dafe72ef058a Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Fri, 9 Aug 2024 12:09:21 -0700 Subject: [PATCH 20/83] bug: fix more path issues on windows Stop evaluating env vars locally, but instead use the shell/cmd.exe. --- pkg/engine/cmd.go | 145 ++++++++++------------------------------- pkg/engine/cmd_test.go | 135 -------------------------------------- pkg/engine/daemon.go | 1 + 3 files changed, 37 insertions(+), 244 deletions(-) delete mode 100644 pkg/engine/cmd_test.go diff --git a/pkg/engine/cmd.go b/pkg/engine/cmd.go index 311c743a..14b41183 100644 --- a/pkg/engine/cmd.go +++ b/pkg/engine/cmd.go @@ -11,7 +11,6 @@ import ( "os" "os/exec" "path" - "path/filepath" "runtime" "sort" "strings" @@ -121,7 +120,7 @@ func (e *Engine) runCommand(ctx Context, tool types.Tool, input string, toolCate var extraEnv = []string{ strings.TrimSpace("GPTSCRIPT_CONTEXT=" + strings.Join(instructions, "\n")), } - cmd, stop, err := e.newCommand(ctx.Ctx, extraEnv, tool, input) + cmd, stop, err := e.newCommand(ctx.Ctx, extraEnv, tool, input, true) if err != nil { if toolCategory == NoCategory { return fmt.Sprintf("ERROR: got (%v) while parsing command", err), nil @@ -244,7 +243,11 @@ func appendInputAsEnv(env []string, input string) []string { return env } -func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.Tool, input string) (*exec.Cmd, func(), error) { +func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.Tool, input string, useShell bool) (*exec.Cmd, func(), error) { + if runtime.GOOS == "windows" { + useShell = false + } + envvars := append(e.Env[:], extraEnv...) envvars = appendInputAsEnv(envvars, input) if log.IsDebug() { @@ -254,9 +257,17 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T interpreter, rest, _ := strings.Cut(tool.Instructions, "\n") interpreter = strings.TrimSpace(interpreter)[2:] - args, err := shlex.Split(interpreter) - if err != nil { - return nil, nil, err + var ( + args []string + err error + ) + if useShell { + args = strings.Fields(interpreter) + } else { + args, err = shlex.Split(interpreter) + if err != nil { + return nil, nil, err + } } envvars, err = e.getRuntimeEnv(ctx, tool, args, envvars) @@ -265,17 +276,6 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T } envvars, envMap := envAsMapAndDeDup(envvars) - for i, arg := range args { - args[i] = os.Expand(arg, func(s string) string { - return envMap[s] - }) - } - - // After we determined the interpreter we again interpret the args by env vars - args, err = replaceVariablesForInterpreter(interpreter, envMap) - if err != nil { - return nil, nil, err - } if runtime.GOOS == "windows" && (args[0] == "/bin/bash" || args[0] == "/bin/sh") { args[0] = path.Base(args[0]) @@ -286,8 +286,7 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T } var ( - cmdArgs = args[1:] - stop = func() {} + stop = func() {} ) if strings.TrimSpace(rest) != "" { @@ -305,105 +304,33 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T stop() return nil, nil, err } - cmdArgs = append(cmdArgs, f.Name()) - } - - // This is a workaround for Windows, where the command interpreter is constructed with unix style paths - // It converts unix style paths to windows style paths - if runtime.GOOS == "windows" { - parts := strings.Split(args[0], "/") - if parts[len(parts)-1] == "gptscript-go-tool" { - parts[len(parts)-1] = "gptscript-go-tool.exe" - } - - args[0] = filepath.Join(parts...) + args = append(args, f.Name()) } - cmd := exec.CommandContext(ctx, env.Lookup(envvars, args[0]), cmdArgs...) - cmd.Env = compressEnv(envvars) - return cmd, stop, nil -} - -func replaceVariablesForInterpreter(interpreter string, envMap map[string]string) ([]string, error) { - var parts []string - for i, part := range splitByQuotes(interpreter) { - if i%2 == 0 { - part = os.Expand(part, func(s string) string { + // Expand and/or normalize env references + for i, arg := range args { + args[i] = os.Expand(arg, func(s string) string { + if strings.HasPrefix(s, "!") { + return envMap[s[1:]] + } + if !useShell { return envMap[s] - }) - // We protect newly resolved env vars from getting replaced when we do the second Expand - // after shlex. Yeah, crazy. I'm guessing this isn't secure, but just trying to avoid a foot gun. - part = os.Expand(part, func(s string) string { - return "${__" + s + "}" - }) - } - parts = append(parts, part) - } - - parts, err := shlex.Split(strings.Join(parts, "")) - if err != nil { - return nil, err - } - - for i, part := range parts { - parts[i] = os.Expand(part, func(s string) string { - if strings.HasPrefix(s, "__") { - return "${" + s[2:] + "}" } - return envMap[s] + return "${" + s + "}" }) } - return parts, nil -} - -// splitByQuotes will split a string by parsing matching double quotes (with \ as the escape character). -// The return value conforms to the following properties -// 1. s == strings.Join(result, "") -// 2. Even indexes are strings that were not in quotes. -// 3. Odd indexes are strings that were quoted. -// -// Example: s = `In a "quoted string" quotes can be escaped with \"` -// -// result = [`In a `, `"quoted string"`, ` quotes can be escaped with \"`] -func splitByQuotes(s string) (result []string) { - var ( - buf strings.Builder - inEscape, inQuote bool - ) - - for _, c := range s { - if inEscape { - buf.WriteRune(c) - inEscape = false - continue - } - - switch c { - case '"': - if inQuote { - buf.WriteRune(c) - } - result = append(result, buf.String()) - buf.Reset() - if !inQuote { - buf.WriteRune(c) - } - inQuote = !inQuote - case '\\': - inEscape = true - buf.WriteRune(c) - default: - buf.WriteRune(c) - } + if runtime.GOOS == "windows" { + args[0] = strings.ReplaceAll(args[0], "/", "\\") } - if buf.Len() > 0 { - if inQuote { - result = append(result, "") - } - result = append(result, buf.String()) + if useShell { + args = append([]string{"/bin/sh", "-c"}, strings.Join(args, " ")) + } else { + args[0] = env.Lookup(envvars, args[0]) } - return + cmd := exec.CommandContext(ctx, args[0], args[1:]...) + cmd.Env = compressEnv(envvars) + return cmd, stop, nil } diff --git a/pkg/engine/cmd_test.go b/pkg/engine/cmd_test.go deleted file mode 100644 index 15f72036..00000000 --- a/pkg/engine/cmd_test.go +++ /dev/null @@ -1,135 +0,0 @@ -// File: cmd_test.go -package engine - -import "testing" - -func TestSplitByQuotes(t *testing.T) { - tests := []struct { - name string - input string - expected []string - }{ - { - name: "NoQuotes", - input: "Hello World", - expected: []string{"Hello World"}, - }, - { - name: "ValidQuote", - input: `"Hello" "World"`, - expected: []string{``, `"Hello"`, ` `, `"World"`}, - }, - { - name: "ValidQuoteWithEscape", - input: `"Hello\" World"`, - expected: []string{``, `"Hello\" World"`}, - }, - { - name: "Nothing", - input: "", - expected: []string{}, - }, - { - name: "SpaceInsideQuote", - input: `"Hello World"`, - expected: []string{``, `"Hello World"`}, - }, - { - name: "SingleChar", - input: "H", - expected: []string{"H"}, - }, - { - name: "SingleQuote", - input: `"Hello`, - expected: []string{``, ``, `"Hello`}, - }, - { - name: "ThreeQuotes", - input: `Test "Hello "World" End\"`, - expected: []string{`Test `, `"Hello "`, `World`, ``, `" End\"`}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := splitByQuotes(tt.input) - if !equal(got, tt.expected) { - t.Errorf("splitByQuotes() = %v, want %v", got, tt.expected) - } - }) - } -} - -// Helper function to assert equality of two string slices. -func equal(a, b []string) bool { - if len(a) != len(b) { - return false - } - for i, v := range a { - if v != b[i] { - return false - } - } - return true -} - -// Testing for replaceVariablesForInterpreter -func TestReplaceVariablesForInterpreter(t *testing.T) { - tests := []struct { - name string - interpreter string - envMap map[string]string - expected []string - shouldFail bool - }{ - { - name: "No quotes", - interpreter: "/bin/bash -c ${COMMAND} tail", - envMap: map[string]string{"COMMAND": "echo Hello!"}, - expected: []string{"/bin/bash", "-c", "echo", "Hello!", "tail"}, - }, - { - name: "Quotes Variables", - interpreter: `/bin/bash -c "${COMMAND}" tail`, - envMap: map[string]string{"COMMAND": "Hello, World!"}, - expected: []string{"/bin/bash", "-c", "Hello, World!", "tail"}, - }, - { - name: "Double escape", - interpreter: `/bin/bash -c "${COMMAND}" ${TWO} tail`, - envMap: map[string]string{ - "COMMAND": "Hello, World!", - "TWO": "${COMMAND}", - }, - expected: []string{"/bin/bash", "-c", "Hello, World!", "${COMMAND}", "tail"}, - }, - { - name: "aws cli issue", - interpreter: "aws ${ARGS}", - envMap: map[string]string{ - "ARGS": `ec2 describe-instances --region us-east-1 --query 'Reservations[*].Instances[*].{Instance:InstanceId,State:State.Name}'`, - }, - expected: []string{ - `aws`, - `ec2`, - `describe-instances`, - `--region`, `us-east-1`, - `--query`, `Reservations[*].Instances[*].{Instance:InstanceId,State:State.Name}`, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := replaceVariablesForInterpreter(tt.interpreter, tt.envMap) - if (err != nil) != tt.shouldFail { - t.Errorf("replaceVariablesForInterpreter() error = %v, want %v", err, tt.shouldFail) - return - } - if !equal(got, tt.expected) { - t.Errorf("replaceVariablesForInterpreter() = %v, want %v", got, tt.expected) - } - }) - } -} diff --git a/pkg/engine/daemon.go b/pkg/engine/daemon.go index 4cdab995..113aa1ba 100644 --- a/pkg/engine/daemon.go +++ b/pkg/engine/daemon.go @@ -133,6 +133,7 @@ func (e *Engine) startDaemon(tool types.Tool) (string, error) { }, tool, "{}", + false, ) if err != nil { return url, err From cb46358ec8b92cab677856f4592bbe2010563ab8 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Fri, 9 Aug 2024 11:44:39 -0700 Subject: [PATCH 21/83] feat: manage node/python runtime for local files in dev --- pkg/repos/get.go | 18 ++- pkg/repos/runtimes/busybox/busybox.go | 4 + pkg/repos/runtimes/golang/golang.go | 4 + pkg/repos/runtimes/node/node.go | 26 +++- pkg/repos/runtimes/python/python.go | 38 +++-- pkg/tests/runner_test.go | 23 +++ .../TestRuntimesLocalDev/call1-resp.golden | 16 +++ .../TestRuntimesLocalDev/call1.golden | 37 +++++ .../TestRuntimesLocalDev/call2-resp.golden | 16 +++ .../TestRuntimesLocalDev/call2.golden | 70 +++++++++ .../TestRuntimesLocalDev/call3-resp.golden | 16 +++ .../TestRuntimesLocalDev/call3.golden | 103 +++++++++++++ .../TestRuntimesLocalDev/call4-resp.golden | 9 ++ .../TestRuntimesLocalDev/call4.golden | 136 ++++++++++++++++++ .../TestRuntimesLocalDev/package.json | 15 ++ .../TestRuntimesLocalDev/requirements.txt | 1 + .../testdata/TestRuntimesLocalDev/test.gpt | 34 +++++ 17 files changed, 552 insertions(+), 14 deletions(-) create mode 100644 pkg/tests/testdata/TestRuntimesLocalDev/call1-resp.golden create mode 100644 pkg/tests/testdata/TestRuntimesLocalDev/call1.golden create mode 100644 pkg/tests/testdata/TestRuntimesLocalDev/call2-resp.golden create mode 100644 pkg/tests/testdata/TestRuntimesLocalDev/call2.golden create mode 100644 pkg/tests/testdata/TestRuntimesLocalDev/call3-resp.golden create mode 100644 pkg/tests/testdata/TestRuntimesLocalDev/call3.golden create mode 100644 pkg/tests/testdata/TestRuntimesLocalDev/call4-resp.golden create mode 100644 pkg/tests/testdata/TestRuntimesLocalDev/call4.golden create mode 100644 pkg/tests/testdata/TestRuntimesLocalDev/package.json create mode 100644 pkg/tests/testdata/TestRuntimesLocalDev/requirements.txt create mode 100644 pkg/tests/testdata/TestRuntimesLocalDev/test.gpt diff --git a/pkg/repos/get.go b/pkg/repos/get.go index fc675c58..b43bc63b 100644 --- a/pkg/repos/get.go +++ b/pkg/repos/get.go @@ -28,6 +28,7 @@ type Runtime interface { ID() string Supports(tool types.Tool, cmd []string) bool Setup(ctx context.Context, tool types.Tool, dataRoot, toolSource string, env []string) ([]string, error) + GetHash(tool types.Tool) (string, error) } type noopRuntime struct { @@ -37,6 +38,10 @@ func (n noopRuntime) ID() string { return "none" } +func (n noopRuntime) GetHash(_ types.Tool) (string, error) { + return "", nil +} + func (n noopRuntime) Supports(_ types.Tool, _ []string) bool { return false } @@ -183,8 +188,13 @@ func (m *Manager) setup(ctx context.Context, runtime Runtime, tool types.Tool, e locker.Lock(tool.ID) defer locker.Unlock(tool.ID) + runtimeHash, err := runtime.GetHash(tool) + if err != nil { + return "", nil, err + } + target := filepath.Join(m.storageDir, tool.Source.Repo.Revision, tool.Source.Repo.Path, tool.Source.Repo.Name, runtime.ID()) - targetFinal := filepath.Join(target, tool.Source.Repo.Path) + targetFinal := filepath.Join(target, tool.Source.Repo.Path+runtimeHash) doneFile := targetFinal + ".done" envData, err := os.ReadFile(doneFile) if err == nil { @@ -251,7 +261,11 @@ func (m *Manager) GetContext(ctx context.Context, tool types.Tool, cmd, env []st for _, runtime := range m.runtimes { if runtime.Supports(tool, cmd) { log.Debugf("Runtime %s supports %v", runtime.ID(), cmd) - return m.setup(ctx, runtime, tool, env) + wd, env, err := m.setup(ctx, runtime, tool, env) + if isLocal { + wd = tool.WorkingDir + } + return wd, env, err } } diff --git a/pkg/repos/runtimes/busybox/busybox.go b/pkg/repos/runtimes/busybox/busybox.go index 542ba94a..481ed1fe 100644 --- a/pkg/repos/runtimes/busybox/busybox.go +++ b/pkg/repos/runtimes/busybox/busybox.go @@ -33,6 +33,10 @@ func (r *Runtime) ID() string { return "busybox" } +func (r *Runtime) GetHash(_ types.Tool) (string, error) { + return "", nil +} + func (r *Runtime) Supports(_ types.Tool, cmd []string) bool { if runtime.GOOS != "windows" { return false diff --git a/pkg/repos/runtimes/golang/golang.go b/pkg/repos/runtimes/golang/golang.go index b19cfe90..882e8a0b 100644 --- a/pkg/repos/runtimes/golang/golang.go +++ b/pkg/repos/runtimes/golang/golang.go @@ -35,6 +35,10 @@ func (r *Runtime) ID() string { return "go" + r.Version } +func (r *Runtime) GetHash(_ types.Tool) (string, error) { + return "", nil +} + func (r *Runtime) Supports(tool types.Tool, cmd []string) bool { return tool.Source.IsGit() && len(cmd) > 0 && cmd[0] == "${GPTSCRIPT_TOOL_DIR}/bin/gptscript-go-tool" diff --git a/pkg/repos/runtimes/node/node.go b/pkg/repos/runtimes/node/node.go index fde5103d..d0a9d8cb 100644 --- a/pkg/repos/runtimes/node/node.go +++ b/pkg/repos/runtimes/node/node.go @@ -39,10 +39,7 @@ func (r *Runtime) ID() string { return "node" + r.Version } -func (r *Runtime) Supports(tool types.Tool, cmd []string) bool { - if _, hasPackageJSON := tool.MetaData[packageJSON]; !hasPackageJSON && !tool.Source.IsGit() { - return false - } +func (r *Runtime) Supports(_ types.Tool, cmd []string) bool { for _, testCmd := range []string{"node", "npx", "npm"} { if r.supports(testCmd, cmd) { return true @@ -61,6 +58,15 @@ func (r *Runtime) supports(testCmd string, cmd []string) bool { return runtimeEnv.Matches(cmd, testCmd) } +func (r *Runtime) GetHash(tool types.Tool) (string, error) { + if !tool.Source.IsGit() && tool.WorkingDir != "" { + if s, err := os.Stat(filepath.Join(tool.WorkingDir, packageJSON)); err == nil { + return hash.Digest(tool.WorkingDir + s.ModTime().String())[:7], nil + } + } + return "", nil +} + func (r *Runtime) Setup(ctx context.Context, tool types.Tool, dataRoot, toolSource string, env []string) ([]string, error) { binPath, err := r.getRuntime(ctx, dataRoot) if err != nil { @@ -74,6 +80,8 @@ func (r *Runtime) Setup(ctx context.Context, tool types.Tool, dataRoot, toolSour if _, ok := tool.MetaData[packageJSON]; ok { newEnv = append(newEnv, "GPTSCRIPT_TMPDIR="+toolSource) + } else if !tool.Source.IsGit() && tool.WorkingDir != "" { + newEnv = append(newEnv, "GPTSCRIPT_TMPDIR="+tool.WorkingDir, "GPTSCRIPT_RUNTIME_DEV=true") } return newEnv, nil @@ -120,6 +128,16 @@ func (r *Runtime) runNPM(ctx context.Context, tool types.Tool, toolSource, binDi if err := os.WriteFile(filepath.Join(toolSource, packageJSON), []byte(contents+"\n"), 0644); err != nil { return err } + } else if !tool.Source.IsGit() { + if tool.WorkingDir == "" { + return nil + } + if _, err := os.Stat(filepath.Join(tool.WorkingDir, packageJSON)); errors.Is(fs.ErrNotExist, err) { + return nil + } else if err != nil { + return err + } + cmd.Dir = tool.WorkingDir } return cmd.Run() } diff --git a/pkg/repos/runtimes/python/python.go b/pkg/repos/runtimes/python/python.go index ae24f92a..87b072e5 100644 --- a/pkg/repos/runtimes/python/python.go +++ b/pkg/repos/runtimes/python/python.go @@ -24,8 +24,9 @@ import ( var releasesData []byte const ( - uvVersion = "uv==0.2.33" - requirementsTxt = "requirements.txt" + uvVersion = "uv==0.2.33" + requirementsTxt = "requirements.txt" + gptscriptRequirementsTxt = "requirements-gptscript.txt" ) type Release struct { @@ -47,10 +48,7 @@ func (r *Runtime) ID() string { return "python" + r.Version } -func (r *Runtime) Supports(tool types.Tool, cmd []string) bool { - if _, hasRequirements := tool.MetaData[requirementsTxt]; !hasRequirements && !tool.Source.IsGit() { - return false - } +func (r *Runtime) Supports(_ types.Tool, cmd []string) bool { if runtimeEnv.Matches(cmd, r.ID()) { return true } @@ -177,6 +175,22 @@ func (r *Runtime) getReleaseAndDigest() (string, string, error) { return "", "", fmt.Errorf("failed to find an python runtime for %s", r.Version) } +func (r *Runtime) GetHash(tool types.Tool) (string, error) { + if !tool.Source.IsGit() && tool.WorkingDir != "" { + if _, ok := tool.MetaData[requirementsTxt]; ok { + return "", nil + } + for _, req := range []string{gptscriptRequirementsTxt, requirementsTxt} { + reqFile := filepath.Join(tool.WorkingDir, req) + if s, err := os.Stat(reqFile); err == nil && !s.IsDir() { + return hash.Digest(tool.WorkingDir + s.ModTime().String())[:7], nil + } + } + } + + return "", nil +} + func (r *Runtime) runPip(ctx context.Context, tool types.Tool, toolSource, binDir string, env []string) error { log.InfofCtx(ctx, "Running pip in %s", toolSource) if content, ok := tool.MetaData[requirementsTxt]; ok { @@ -189,8 +203,16 @@ func (r *Runtime) runPip(ctx context.Context, tool types.Tool, toolSource, binDi return cmd.Run() } - for _, req := range []string{"requirements-gptscript.txt", requirementsTxt} { - reqFile := filepath.Join(toolSource, req) + reqPath := toolSource + if !tool.Source.IsGit() { + if tool.WorkingDir == "" { + return nil + } + reqPath = tool.WorkingDir + } + + for _, req := range []string{gptscriptRequirementsTxt, requirementsTxt} { + reqFile := filepath.Join(reqPath, req) if s, err := os.Stat(reqFile); err == nil && !s.IsDir() { cmd := debugcmd.New(ctx, uvBin(binDir), "pip", "install", "-r", reqFile) cmd.Env = env diff --git a/pkg/tests/runner_test.go b/pkg/tests/runner_test.go index 424c84c1..141e6aff 100644 --- a/pkg/tests/runner_test.go +++ b/pkg/tests/runner_test.go @@ -1018,3 +1018,26 @@ func TestRuntimes(t *testing.T) { }) r.RunDefault() } + +func TestRuntimesLocalDev(t *testing.T) { + r := tester.NewRunner(t) + r.RespondWith(tester.Result{ + Func: types.CompletionFunctionCall{ + Name: "py", + Arguments: "{}", + }, + }, tester.Result{ + Func: types.CompletionFunctionCall{ + Name: "node", + Arguments: "{}", + }, + }, tester.Result{ + Func: types.CompletionFunctionCall{ + Name: "bash", + Arguments: "{}", + }, + }) + r.RunDefault() + _ = os.RemoveAll("testdata/TestRuntimesLocalDev/node_modules") + _ = os.RemoveAll("testdata/TestRuntimesLocalDev/package-lock.json") +} diff --git a/pkg/tests/testdata/TestRuntimesLocalDev/call1-resp.golden b/pkg/tests/testdata/TestRuntimesLocalDev/call1-resp.golden new file mode 100644 index 00000000..1d53670a --- /dev/null +++ b/pkg/tests/testdata/TestRuntimesLocalDev/call1-resp.golden @@ -0,0 +1,16 @@ +`{ + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + } + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestRuntimesLocalDev/call1.golden b/pkg/tests/testdata/TestRuntimesLocalDev/call1.golden new file mode 100644 index 00000000..7e775029 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimesLocalDev/call1.golden @@ -0,0 +1,37 @@ +`{ + "model": "gpt-4o", + "tools": [ + { + "function": { + "toolID": "testdata/TestRuntimesLocalDev/test.gpt:py", + "name": "py", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimesLocalDev/test.gpt:node", + "name": "node", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimesLocalDev/test.gpt:bash", + "name": "bash", + "parameters": null + } + } + ], + "messages": [ + { + "role": "system", + "content": [ + { + "text": "Dummy" + } + ], + "usage": {} + } + ] +}` diff --git a/pkg/tests/testdata/TestRuntimesLocalDev/call2-resp.golden b/pkg/tests/testdata/TestRuntimesLocalDev/call2-resp.golden new file mode 100644 index 00000000..4806793c --- /dev/null +++ b/pkg/tests/testdata/TestRuntimesLocalDev/call2-resp.golden @@ -0,0 +1,16 @@ +`{ + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "node", + "arguments": "{}" + } + } + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestRuntimesLocalDev/call2.golden b/pkg/tests/testdata/TestRuntimesLocalDev/call2.golden new file mode 100644 index 00000000..cc1fd1b7 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimesLocalDev/call2.golden @@ -0,0 +1,70 @@ +`{ + "model": "gpt-4o", + "tools": [ + { + "function": { + "toolID": "testdata/TestRuntimesLocalDev/test.gpt:py", + "name": "py", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimesLocalDev/test.gpt:node", + "name": "node", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimesLocalDev/test.gpt:bash", + "name": "bash", + "parameters": null + } + } + ], + "messages": [ + { + "role": "system", + "content": [ + { + "text": "Dummy" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + } + } + ], + "usage": {} + }, + { + "role": "tool", + "content": [ + { + "text": "py worked\r\n" + } + ], + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + }, + "usage": {} + } + ] +}` diff --git a/pkg/tests/testdata/TestRuntimesLocalDev/call3-resp.golden b/pkg/tests/testdata/TestRuntimesLocalDev/call3-resp.golden new file mode 100644 index 00000000..1103f824 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimesLocalDev/call3-resp.golden @@ -0,0 +1,16 @@ +`{ + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 2, + "id": "call_3", + "function": { + "name": "bash", + "arguments": "{}" + } + } + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestRuntimesLocalDev/call3.golden b/pkg/tests/testdata/TestRuntimesLocalDev/call3.golden new file mode 100644 index 00000000..7c928c07 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimesLocalDev/call3.golden @@ -0,0 +1,103 @@ +`{ + "model": "gpt-4o", + "tools": [ + { + "function": { + "toolID": "testdata/TestRuntimesLocalDev/test.gpt:py", + "name": "py", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimesLocalDev/test.gpt:node", + "name": "node", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimesLocalDev/test.gpt:bash", + "name": "bash", + "parameters": null + } + } + ], + "messages": [ + { + "role": "system", + "content": [ + { + "text": "Dummy" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + } + } + ], + "usage": {} + }, + { + "role": "tool", + "content": [ + { + "text": "py worked\r\n" + } + ], + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + }, + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "node", + "arguments": "{}" + } + } + } + ], + "usage": {} + }, + { + "role": "tool", + "content": [ + { + "text": "node worked\n" + } + ], + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "node", + "arguments": "{}" + } + }, + "usage": {} + } + ] +}` diff --git a/pkg/tests/testdata/TestRuntimesLocalDev/call4-resp.golden b/pkg/tests/testdata/TestRuntimesLocalDev/call4-resp.golden new file mode 100644 index 00000000..8135a8c9 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimesLocalDev/call4-resp.golden @@ -0,0 +1,9 @@ +`{ + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 4" + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestRuntimesLocalDev/call4.golden b/pkg/tests/testdata/TestRuntimesLocalDev/call4.golden new file mode 100644 index 00000000..b95b880d --- /dev/null +++ b/pkg/tests/testdata/TestRuntimesLocalDev/call4.golden @@ -0,0 +1,136 @@ +`{ + "model": "gpt-4o", + "tools": [ + { + "function": { + "toolID": "testdata/TestRuntimesLocalDev/test.gpt:py", + "name": "py", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimesLocalDev/test.gpt:node", + "name": "node", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestRuntimesLocalDev/test.gpt:bash", + "name": "bash", + "parameters": null + } + } + ], + "messages": [ + { + "role": "system", + "content": [ + { + "text": "Dummy" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + } + } + ], + "usage": {} + }, + { + "role": "tool", + "content": [ + { + "text": "py worked\r\n" + } + ], + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "py", + "arguments": "{}" + } + }, + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "node", + "arguments": "{}" + } + } + } + ], + "usage": {} + }, + { + "role": "tool", + "content": [ + { + "text": "node worked\n" + } + ], + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "node", + "arguments": "{}" + } + }, + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 2, + "id": "call_3", + "function": { + "name": "bash", + "arguments": "{}" + } + } + } + ], + "usage": {} + }, + { + "role": "tool", + "content": [ + { + "text": "bash works\n" + } + ], + "toolCall": { + "index": 2, + "id": "call_3", + "function": { + "name": "bash", + "arguments": "{}" + } + }, + "usage": {} + } + ] +}` diff --git a/pkg/tests/testdata/TestRuntimesLocalDev/package.json b/pkg/tests/testdata/TestRuntimesLocalDev/package.json new file mode 100644 index 00000000..d5f400a1 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimesLocalDev/package.json @@ -0,0 +1,15 @@ +{ + "name": "chalk-example", + "version": "1.0.0", + "type": "module", + "description": "A simple example project to demonstrate the use of chalk", + "main": "example.js", + "scripts": { + "start": "node example.js" + }, + "author": "Your Name", + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0" + } +} diff --git a/pkg/tests/testdata/TestRuntimesLocalDev/requirements.txt b/pkg/tests/testdata/TestRuntimesLocalDev/requirements.txt new file mode 100644 index 00000000..f2293605 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimesLocalDev/requirements.txt @@ -0,0 +1 @@ +requests diff --git a/pkg/tests/testdata/TestRuntimesLocalDev/test.gpt b/pkg/tests/testdata/TestRuntimesLocalDev/test.gpt new file mode 100644 index 00000000..454ffce0 --- /dev/null +++ b/pkg/tests/testdata/TestRuntimesLocalDev/test.gpt @@ -0,0 +1,34 @@ +name: first +tools: py, node, bash + +Dummy + +--- +name: py + +#!/usr/bin/env python3 + +import requests +import platform + +# this is dumb hack to get the line endings to always be \r\n so the golden files match +# on both linux and windows +if platform.system() == 'Windows': + print('py worked') +else: + print('py worked\r') + +--- +name: node + +#!/usr/bin/env node + +import chalk from 'chalk'; +console.log("node worked") + +--- +name: bash + +#!/bin/bash + +echo bash works \ No newline at end of file From ca8ab3aedb8ed535c9512f23acd01fbaaa494c75 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Sat, 10 Aug 2024 22:47:30 -0700 Subject: [PATCH 22/83] feat: enable github release binary downloads for go tools --- pkg/repos/get.go | 34 +++-- pkg/repos/runtimes/busybox/busybox.go | 4 + pkg/repos/runtimes/golang/golang.go | 181 ++++++++++++++++++++++++++ pkg/repos/runtimes/node/node.go | 4 + pkg/repos/runtimes/python/python.go | 4 + 5 files changed, 217 insertions(+), 10 deletions(-) diff --git a/pkg/repos/get.go b/pkg/repos/get.go index b43bc63b..416f4c61 100644 --- a/pkg/repos/get.go +++ b/pkg/repos/get.go @@ -27,6 +27,7 @@ const credentialHelpersRepo = "github.com/gptscript-ai/gptscript-credential-help type Runtime interface { ID() string Supports(tool types.Tool, cmd []string) bool + Binary(ctx context.Context, tool types.Tool, dataRoot, toolSource string, env []string) (bool, []string, error) Setup(ctx context.Context, tool types.Tool, dataRoot, toolSource string, env []string) ([]string, error) GetHash(tool types.Tool) (string, error) } @@ -46,6 +47,10 @@ func (n noopRuntime) Supports(_ types.Tool, _ []string) bool { return false } +func (n noopRuntime) Binary(_ context.Context, _ types.Tool, _, _ string, _ []string) (bool, []string, error) { + return false, nil, nil +} + func (n noopRuntime) Setup(_ context.Context, _ types.Tool, _, _ string, _ []string) ([]string, error) { return nil, nil } @@ -211,21 +216,30 @@ func (m *Manager) setup(ctx context.Context, runtime Runtime, tool types.Tool, e _ = os.RemoveAll(doneFile) _ = os.RemoveAll(target) - if tool.Source.Repo.VCS == "git" { - if err := git.Checkout(ctx, m.gitDir, tool.Source.Repo.Root, tool.Source.Repo.Revision, target); err != nil { - return "", nil, err + var ( + newEnv []string + isBinary bool + ) + + if isBinary, newEnv, err = runtime.Binary(ctx, tool, m.runtimeDir, targetFinal, env); err != nil { + return "", nil, err + } else if !isBinary { + if tool.Source.Repo.VCS == "git" { + if err := git.Checkout(ctx, m.gitDir, tool.Source.Repo.Root, tool.Source.Repo.Revision, target); err != nil { + return "", nil, err + } + } else { + if err := os.MkdirAll(target, 0755); err != nil { + return "", nil, err + } } - } else { - if err := os.MkdirAll(target, 0755); err != nil { + + newEnv, err = runtime.Setup(ctx, tool, m.runtimeDir, targetFinal, env) + if err != nil { return "", nil, err } } - newEnv, err := runtime.Setup(ctx, tool, m.runtimeDir, targetFinal, env) - if err != nil { - return "", nil, err - } - out, err := os.Create(doneFile + ".tmp") if err != nil { return "", nil, err diff --git a/pkg/repos/runtimes/busybox/busybox.go b/pkg/repos/runtimes/busybox/busybox.go index 481ed1fe..e4604b06 100644 --- a/pkg/repos/runtimes/busybox/busybox.go +++ b/pkg/repos/runtimes/busybox/busybox.go @@ -49,6 +49,10 @@ func (r *Runtime) Supports(_ types.Tool, cmd []string) bool { return false } +func (r *Runtime) Binary(_ context.Context, _ types.Tool, _, _ string, _ []string) (bool, []string, error) { + return false, nil, nil +} + func (r *Runtime) Setup(ctx context.Context, _ types.Tool, dataRoot, _ string, env []string) ([]string, error) { binPath, err := r.getRuntime(ctx, dataRoot) if err != nil { diff --git a/pkg/repos/runtimes/golang/golang.go b/pkg/repos/runtimes/golang/golang.go index 882e8a0b..9e472e90 100644 --- a/pkg/repos/runtimes/golang/golang.go +++ b/pkg/repos/runtimes/golang/golang.go @@ -4,10 +4,14 @@ import ( "bufio" "bytes" "context" + "crypto/sha256" _ "embed" + "encoding/hex" "errors" "fmt" + "io" "io/fs" + "net/http" "os" "path/filepath" "runtime" @@ -44,6 +48,183 @@ func (r *Runtime) Supports(tool types.Tool, cmd []string) bool { len(cmd) > 0 && cmd[0] == "${GPTSCRIPT_TOOL_DIR}/bin/gptscript-go-tool" } +type release struct { + account, repo, label string +} + +func (r release) checksumTxt() string { + return fmt.Sprintf( + "https://github.com/%s/%s/releases/download/%s/checksums.txt", + r.account, + r.repo, + r.label) +} + +func (r release) binURL() string { + return fmt.Sprintf( + "https://github.com/%s/%s/releases/download/%s/%s", + r.account, + r.repo, + r.label, + r.srcBinName()) +} + +func (r release) targetBinName() string { + suffix := "" + if runtime.GOOS == "windows" { + suffix = ".exe" + } + + return "gptscript-go-tool" + suffix +} + +func (r release) srcBinName() string { + suffix := "" + if runtime.GOOS == "windows" { + suffix = ".exe" + } + + return r.repo + "-" + + runtime.GOOS + "-" + + runtime.GOARCH + suffix +} + +func getLatestRelease(tool types.Tool) (*release, bool) { + if tool.Source.Repo == nil || !strings.HasPrefix(tool.Source.Repo.Root, "https://github.com/") { + return nil, false + } + + parts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(tool.Source.Repo.Root, ".git"), "https://"), "/") + if len(parts) != 3 { + return nil, false + } + + client := http.Client{ + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + resp, err := client.Get(fmt.Sprintf("https://github.com/%s/%s/releases/latest", parts[1], parts[2])) + if err != nil || resp.StatusCode != http.StatusFound { + // ignore error + return nil, false + } + defer resp.Body.Close() + + target := resp.Header.Get("Location") + if target == "" { + return nil, false + } + + account, repo := parts[1], parts[2] + parts = strings.Split(target, "/") + label := parts[len(parts)-1] + + return &release{ + account: account, + repo: repo, + label: label, + }, true +} + +func get(ctx context.Context, url string) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } else if resp.StatusCode != http.StatusOK { + _ = resp.Body.Close() + return nil, fmt.Errorf("bad HTTP status code: %d", resp.StatusCode) + } + + return resp, nil +} + +func downloadBin(ctx context.Context, checksum, src, url, bin string) error { + resp, err := get(ctx, url) + if err != nil { + return err + } + defer resp.Body.Close() + + if err := os.MkdirAll(filepath.Join(src, "bin"), 0755); err != nil { + return err + } + + targetFile, err := os.Create(filepath.Join(src, "bin", bin)) + if err != nil { + return err + } + + digest := sha256.New() + + if _, err := io.Copy(io.MultiWriter(targetFile, digest), resp.Body); err != nil { + return err + } + + if err := targetFile.Close(); err != nil { + return nil + } + + if got := hex.EncodeToString(digest.Sum(nil)); got != checksum { + return fmt.Errorf("checksum mismatch %s != %s", got, checksum) + } + + if err := os.Chmod(targetFile.Name(), 0755); err != nil { + return err + } + + return nil +} + +func getChecksum(ctx context.Context, rel *release) string { + resp, err := get(ctx, rel.checksumTxt()) + if err != nil { + // ignore error + return "" + } + defer resp.Body.Close() + + scan := bufio.NewScanner(resp.Body) + for scan.Scan() { + fields := strings.Fields(scan.Text()) + if len(fields) != 2 || fields[1] != rel.srcBinName() { + continue + } + return fields[0] + } + + return "" +} + +func (r *Runtime) Binary(ctx context.Context, tool types.Tool, _, toolSource string, env []string) (bool, []string, error) { + if !tool.Source.IsGit() { + return false, nil, nil + } + + rel, ok := getLatestRelease(tool) + if !ok { + return false, nil, nil + } + + checksum := getChecksum(ctx, rel) + if checksum == "" { + return false, nil, nil + } + + if err := downloadBin(ctx, checksum, toolSource, rel.binURL(), rel.targetBinName()); err != nil { + // ignore error + return false, nil, nil + } + + return true, env, nil +} + func (r *Runtime) Setup(ctx context.Context, _ types.Tool, dataRoot, toolSource string, env []string) ([]string, error) { binPath, err := r.getRuntime(ctx, dataRoot) if err != nil { diff --git a/pkg/repos/runtimes/node/node.go b/pkg/repos/runtimes/node/node.go index d0a9d8cb..01a752e6 100644 --- a/pkg/repos/runtimes/node/node.go +++ b/pkg/repos/runtimes/node/node.go @@ -39,6 +39,10 @@ func (r *Runtime) ID() string { return "node" + r.Version } +func (r *Runtime) Binary(_ context.Context, _ types.Tool, _, _ string, _ []string) (bool, []string, error) { + return false, nil, nil +} + func (r *Runtime) Supports(_ types.Tool, cmd []string) bool { for _, testCmd := range []string{"node", "npx", "npm"} { if r.supports(testCmd, cmd) { diff --git a/pkg/repos/runtimes/python/python.go b/pkg/repos/runtimes/python/python.go index 87b072e5..ee4bf571 100644 --- a/pkg/repos/runtimes/python/python.go +++ b/pkg/repos/runtimes/python/python.go @@ -175,6 +175,10 @@ func (r *Runtime) getReleaseAndDigest() (string, string, error) { return "", "", fmt.Errorf("failed to find an python runtime for %s", r.Version) } +func (r *Runtime) Binary(_ context.Context, _ types.Tool, _, _ string, _ []string) (bool, []string, error) { + return false, nil, nil +} + func (r *Runtime) GetHash(tool types.Tool) (string, error) { if !tool.Source.IsGit() && tool.WorkingDir != "" { if _, ok := tool.MetaData[requirementsTxt]; ok { From b05ceb7a5d55deb9bf5ca13872e8706e781a28de Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Mon, 12 Aug 2024 09:43:50 -0400 Subject: [PATCH 23/83] fix: support share credentials in context tools (#782) Signed-off-by: Grant Linville --- pkg/types/tool.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/types/tool.go b/pkg/types/tool.go index bb49e6f1..7e08f604 100644 --- a/pkg/types/tool.go +++ b/pkg/types/tool.go @@ -752,6 +752,16 @@ func (t Tool) GetCredentialTools(prg Program, agentGroup []ToolReference) ([]Too result.AddAll(referencedTool.GetToolRefsFromNames(referencedTool.ExportCredentials)) } + contextToolRefs, err := t.getDirectContextToolRefs(prg) + if err != nil { + return nil, err + } + + for _, contextToolRef := range contextToolRefs { + contextTool := prg.ToolSet[contextToolRef.ToolID] + result.AddAll(contextTool.GetToolRefsFromNames(contextTool.ExportCredentials)) + } + return result.List() } From f356013661be6858d27f0b85338bafbec3ad42f2 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Mon, 12 Aug 2024 09:43:45 -0400 Subject: [PATCH 24/83] fix: stop running tool for providers that are runnning A previous change stopped caching clients so that they would be restarted whenever needed. However, the running of the tool produces unwanted output if the provider is already running. This change includes a way to tell whether the provider is running or needs to be restarted. Signed-off-by: Donnie Adams --- pkg/engine/daemon.go | 15 +++++++++++++-- pkg/remote/remote.go | 42 +++++++++++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/pkg/engine/daemon.go b/pkg/engine/daemon.go index 113aa1ba..f0a1c10c 100644 --- a/pkg/engine/daemon.go +++ b/pkg/engine/daemon.go @@ -18,8 +18,9 @@ import ( var ports Ports type Ports struct { - daemonPorts map[string]int64 - daemonLock sync.Mutex + daemonPorts map[string]int64 + daemonsRunning map[string]struct{} + daemonLock sync.Mutex startPort, endPort int64 usedPorts map[int64]struct{} @@ -28,6 +29,13 @@ type Ports struct { daemonWG sync.WaitGroup } +func IsDaemonRunning(url string) bool { + ports.daemonLock.Lock() + defer ports.daemonLock.Unlock() + _, ok := ports.daemonsRunning[url] + return ok +} + func SetPorts(start, end int64) { ports.daemonLock.Lock() defer ports.daemonLock.Unlock() @@ -164,8 +172,10 @@ func (e *Engine) startDaemon(tool types.Tool) (string, error) { if ports.daemonPorts == nil { ports.daemonPorts = map[string]int64{} + ports.daemonsRunning = map[string]struct{}{} } ports.daemonPorts[tool.ID] = port + ports.daemonsRunning[url] = struct{}{} killedCtx, cancel := context.WithCancelCause(ctx) defer cancel(nil) @@ -185,6 +195,7 @@ func (e *Engine) startDaemon(tool types.Tool) (string, error) { defer ports.daemonLock.Unlock() delete(ports.daemonPorts, tool.ID) + delete(ports.daemonsRunning, url) ports.daemonWG.Done() }() diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 89863529..6a21413b 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -22,8 +22,9 @@ import ( ) type Client struct { - modelsLock sync.Mutex + clientsLock sync.Mutex cache *cache.Client + clients map[string]clientInfo modelToProvider map[string]string runner *runner.Runner envs []string @@ -38,13 +39,15 @@ func New(r *runner.Runner, envs []string, cache *cache.Client, credStore credent envs: envs, credStore: credStore, defaultProvider: defaultProvider, + modelToProvider: make(map[string]string), + clients: make(map[string]clientInfo), } } func (c *Client) Call(ctx context.Context, messageRequest types.CompletionRequest, status chan<- types.CompletionStatus) (*types.CompletionMessage, error) { - c.modelsLock.Lock() + c.clientsLock.Lock() provider, ok := c.modelToProvider[messageRequest.Model] - c.modelsLock.Unlock() + c.clientsLock.Unlock() if !ok { return nil, fmt.Errorf("failed to find remote model %s", messageRequest.Model) @@ -105,12 +108,8 @@ func (c *Client) Supports(ctx context.Context, modelString string) (bool, error) return false, err } - c.modelsLock.Lock() - defer c.modelsLock.Unlock() - - if c.modelToProvider == nil { - c.modelToProvider = map[string]string{} - } + c.clientsLock.Lock() + defer c.clientsLock.Unlock() c.modelToProvider[modelString] = providerName return true, nil @@ -145,11 +144,23 @@ func (c *Client) clientFromURL(ctx context.Context, apiURL string) (*openai.Clie } func (c *Client) load(ctx context.Context, toolName string) (*openai.Client, error) { + c.clientsLock.Lock() + defer c.clientsLock.Unlock() + + client, ok := c.clients[toolName] + if ok && !isHTTPURL(toolName) && engine.IsDaemonRunning(client.url) { + return client.client, nil + } + if isHTTPURL(toolName) { remoteClient, err := c.clientFromURL(ctx, toolName) if err != nil { return nil, err } + c.clients[toolName] = clientInfo{ + client: remoteClient, + url: toolName, + } return remoteClient, nil } @@ -165,7 +176,7 @@ func (c *Client) load(ctx context.Context, toolName string) (*openai.Client, err return nil, err } - client, err := openai.NewClient(ctx, c.credStore, openai.Options{ + oClient, err := openai.NewClient(ctx, c.credStore, openai.Options{ BaseURL: strings.TrimSuffix(url, "/") + "/v1", Cache: c.cache, CacheKey: prg.EntryToolID, @@ -174,7 +185,11 @@ func (c *Client) load(ctx context.Context, toolName string) (*openai.Client, err return nil, err } - return client, nil + c.clients[toolName] = clientInfo{ + client: oClient, + url: url, + } + return client.client, nil } func (c *Client) retrieveAPIKey(ctx context.Context, env, url string) (string, error) { @@ -185,3 +200,8 @@ func isLocalhost(url string) bool { return strings.HasPrefix(url, "http://localhost") || strings.HasPrefix(url, "http://127.0.0.1") || strings.HasPrefix(url, "https://localhost") || strings.HasPrefix(url, "https://127.0.0.1") } + +type clientInfo struct { + client *openai.Client + url string +} From 9696f8d64d06aa4809eccb0a7b018fea63cbd39a Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Mon, 12 Aug 2024 13:45:28 -0400 Subject: [PATCH 25/83] fix: share credentials in context tools (#785) Signed-off-by: Grant Linville --- pkg/types/tool.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/types/tool.go b/pkg/types/tool.go index 7e08f604..789215b6 100644 --- a/pkg/types/tool.go +++ b/pkg/types/tool.go @@ -752,7 +752,7 @@ func (t Tool) GetCredentialTools(prg Program, agentGroup []ToolReference) ([]Too result.AddAll(referencedTool.GetToolRefsFromNames(referencedTool.ExportCredentials)) } - contextToolRefs, err := t.getDirectContextToolRefs(prg) + contextToolRefs, err := t.GetContextTools(prg) if err != nil { return nil, err } From e1e200226466f7069b8dec0fa45c7782d970b43d Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Mon, 12 Aug 2024 11:15:47 -0700 Subject: [PATCH 26/83] chore: support looking up go binary releases by tag --- pkg/repos/runtimes/golang/golang.go | 34 +++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/pkg/repos/runtimes/golang/golang.go b/pkg/repos/runtimes/golang/golang.go index 9e472e90..f82b628a 100644 --- a/pkg/repos/runtimes/golang/golang.go +++ b/pkg/repos/runtimes/golang/golang.go @@ -7,6 +7,7 @@ import ( "crypto/sha256" _ "embed" "encoding/hex" + "encoding/json" "errors" "fmt" "io" @@ -89,6 +90,13 @@ func (r release) srcBinName() string { runtime.GOARCH + suffix } +type tag struct { + Name string `json:"name,omitempty"` + Commit struct { + Sha string `json:"sha,omitempty"` + } `json:"commit"` +} + func getLatestRelease(tool types.Tool) (*release, bool) { if tool.Source.Repo == nil || !strings.HasPrefix(tool.Source.Repo.Root, "https://github.com/") { return nil, false @@ -105,7 +113,30 @@ func getLatestRelease(tool types.Tool) (*release, bool) { }, } - resp, err := client.Get(fmt.Sprintf("https://github.com/%s/%s/releases/latest", parts[1], parts[2])) + account, repo := parts[1], parts[2] + + resp, err := client.Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/tags", account, repo)) + if err != nil || resp.StatusCode != http.StatusOK { + // ignore error + return nil, false + } + defer resp.Body.Close() + + var tags []tag + if err := json.NewDecoder(resp.Body).Decode(&tags); err != nil { + return nil, false + } + for _, tag := range tags { + if tag.Commit.Sha == tool.Source.Repo.Revision { + return &release{ + account: account, + repo: repo, + label: tag.Name, + }, true + } + } + + resp, err = client.Get(fmt.Sprintf("https://github.com/%s/%s/releases/latest", account, repo)) if err != nil || resp.StatusCode != http.StatusFound { // ignore error return nil, false @@ -117,7 +148,6 @@ func getLatestRelease(tool types.Tool) (*release, bool) { return nil, false } - account, repo := parts[1], parts[2] parts = strings.Split(target, "/") label := parts[len(parts)-1] From bacc628a26a97b1874f2d9e9c73a61d32e113c51 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Mon, 12 Aug 2024 12:37:07 -0700 Subject: [PATCH 27/83] chore: fallback to pure go git if git isn't found --- go.mod | 21 ++++++++- go.sum | 72 +++++++++++++++++++++++++++++-- pkg/repos/git/cmd.go | 4 ++ pkg/repos/git/git.go | 8 +++- pkg/repos/git/git_go.go | 91 +++++++++++++++++++++++++++++++++++++++ pkg/repos/git/git_test.go | 2 +- 6 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 pkg/repos/git/git_go.go diff --git a/go.mod b/go.mod index c84dae97..4cf0ba73 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/docker/docker-credential-helpers v0.8.1 github.com/fatih/color v1.17.0 github.com/getkin/kin-openapi v0.124.0 + github.com/go-git/go-git/v5 v5.12.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/gptscript-ai/broadcaster v0.0.0-20240625175512-c43682019b86 @@ -28,7 +29,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.17.1 github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc @@ -43,6 +44,9 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/alecthomas/chroma/v2 v2.8.0 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect @@ -53,14 +57,20 @@ require ( github.com/charmbracelet/glamour v0.7.0 // indirect github.com/charmbracelet/lipgloss v0.11.0 // indirect github.com/charmbracelet/x/ansi v0.1.1 // indirect + github.com/cloudflare/circl v1.3.7 // indirect github.com/connesc/cipherio v0.2.1 // indirect github.com/containerd/console v1.0.4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect github.com/dsnet/compress v0.0.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/swag v0.22.8 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/gookit/color v1.5.4 // indirect @@ -71,8 +81,10 @@ require ( github.com/hexops/gotextdiff v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.2.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.16.5 // indirect github.com/klauspost/pgzip v1.2.5 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect @@ -91,27 +103,32 @@ require ( github.com/olekukonko/tablewriter v0.0.6-0.20230925090304-df64c4bbad77 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pierrec/lz4/v4 v4.1.15 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pterm/pterm v0.12.79 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect github.com/sourcegraph/go-diff-patch v0.0.0-20240223163233-798fd1e94a8e // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/therootcompany/xz v1.0.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/ulikunitz/xz v0.5.10 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.5.4 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect go4.org v0.0.0-20200411211856-f5505b9728dd // indirect + golang.org/x/crypto v0.25.0 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/tools v0.23.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect mvdan.cc/gofumpt v0.6.0 // indirect ) diff --git a/go.sum b/go.sum index 7e2d7b75..1b518130 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= @@ -38,8 +40,13 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/ github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= @@ -50,6 +57,10 @@ github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= @@ -61,6 +72,7 @@ github.com/bodgit/sevenzip v1.3.0 h1:1ljgELgtHqvgIp8W8kgeEGHIWP4ch3xGI8uOBZgLVKY github.com/bodgit/sevenzip v1.3.0/go.mod h1:omwNcgZTEooWM8gA/IJ2Nk/+ZQ94+GsytRzOJJ8FBlM= github.com/bodgit/windows v1.0.0 h1:rLQ/XjsleZvx4fR1tB/UxQrK+SJ2OFHzfPjLWWOhDIA= github.com/bodgit/windows v1.0.0/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= @@ -78,6 +90,9 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw= github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= @@ -88,6 +103,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= @@ -100,6 +117,10 @@ github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRK github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -112,6 +133,16 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= @@ -124,6 +155,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -200,12 +233,16 @@ 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/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA= github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= @@ -268,10 +305,14 @@ github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= github.com/olekukonko/tablewriter v0.0.6-0.20230925090304-df64c4bbad77 h1:3bMMZ1f+GPXFQ1uNaYbO/uECWvSfqEA+ZEXn1rFAT88= github.com/olekukonko/tablewriter v0.0.6-0.20230925090304-df64c4bbad77/go.mod h1:8Hf+pH6thup1sPZPD+NLg7d6vbpsdilu9CPIeikvgMQ= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -304,10 +345,14 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/sourcegraph/go-diff-patch v0.0.0-20240223163233-798fd1e94a8e h1:H+jDTUeF+SVd4ApwnSFoew8ZwGNRfgb9EsZc7LcocAg= github.com/sourcegraph/go-diff-patch v0.0.0-20240223163233-798fd1e94a8e/go.mod h1:VsUklG6OQo7Ctunu0gS3AtEOCEc2kMB6r5rKzxAes58= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= @@ -317,13 +362,14 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= @@ -337,6 +383,8 @@ github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95 github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -363,7 +411,12 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -413,9 +466,12 @@ golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= @@ -448,6 +504,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -456,6 +513,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -468,6 +526,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -480,8 +539,10 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= @@ -492,10 +553,12 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= @@ -575,8 +638,11 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/repos/git/cmd.go b/pkg/repos/git/cmd.go index ad6d7350..3cdcff09 100644 --- a/pkg/repos/git/cmd.go +++ b/pkg/repos/git/cmd.go @@ -17,6 +17,10 @@ func newGitCommand(ctx context.Context, args ...string) *debugcmd.WrappedCmd { } func LsRemote(ctx context.Context, repo, ref string) (string, error) { + if usePureGo() { + return lsRemotePureGo(ctx, repo, ref) + } + cmd := newGitCommand(ctx, "ls-remote", repo, ref) if err := cmd.Run(); err != nil { return "", err diff --git a/pkg/repos/git/git.go b/pkg/repos/git/git.go index 978f3a6d..0c9c22be 100644 --- a/pkg/repos/git/git.go +++ b/pkg/repos/git/git.go @@ -29,7 +29,11 @@ func Checkout(ctx context.Context, base, repo, commit, toDir string) error { return err } - if err := Fetch(ctx, base, repo, commit); err != nil { + if usePureGo() { + return checkoutPureGo(ctx, base, repo, commit, toDir) + } + + if err := fetch(ctx, base, repo, commit); err != nil { return err } @@ -41,7 +45,7 @@ func gitDir(base, repo string) string { return filepath.Join(base, "repos", hash.Digest(repo)) } -func Fetch(ctx context.Context, base, repo, commit string) error { +func fetch(ctx context.Context, base, repo, commit string) error { gitDir := gitDir(base, repo) if found, err := exists(gitDir); err != nil { return err diff --git a/pkg/repos/git/git_go.go b/pkg/repos/git/git_go.go new file mode 100644 index 00000000..aa76c765 --- /dev/null +++ b/pkg/repos/git/git_go.go @@ -0,0 +1,91 @@ +package git + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "sync" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/storage/memory" +) + +var ( + gitCheck sync.Once + externalGit bool +) + +func usePureGo() bool { + if os.Getenv("GPTSCRIPT_PURE_GO_GIT") == "true" { + return true + } + gitCheck.Do(func() { + _, err := exec.LookPath("git") + externalGit = err == nil + }) + return !externalGit +} + +func lsRemotePureGo(_ context.Context, repo, ref string) (string, error) { + // Clone the repository in memory + r := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{ + Name: "origin", + URLs: []string{repo}, + }) + + refs, err := r.List(&git.ListOptions{ + PeelingOption: git.AppendPeeled, + }) + if err != nil { + return "", fmt.Errorf("failed to list remote refs: %w", err) + } + + for _, checkRef := range refs { + if checkRef.Name().Short() == ref { + return checkRef.Hash().String(), nil + } + } + + return "", fmt.Errorf("failed to find remote ref %q", ref) +} + +func checkoutPureGo(ctx context.Context, _, repo, commit, toDir string) error { + log.InfofCtx(ctx, "Checking out %s to %s", commit, toDir) + // Clone the repository + r, err := git.PlainCloneContext(ctx, toDir, false, &git.CloneOptions{ + URL: repo, + NoCheckout: true, + }) + if err != nil { + return fmt.Errorf("failed to clone the repo: %w", err) + } + + // Fetch the specific commit + err = r.Fetch(&git.FetchOptions{ + RefSpecs: []config.RefSpec{ + config.RefSpec(fmt.Sprintf("+%s:%s", commit, commit)), + }, + }) + if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { + return fmt.Errorf("failed to fetch the commit: %w", err) + } + + // Checkout the specific commit + w, err := r.Worktree() + if err != nil { + return fmt.Errorf("failed to get worktree: %w", err) + } + + err = w.Checkout(&git.CheckoutOptions{ + Hash: plumbing.NewHash(commit), + }) + if err != nil { + return fmt.Errorf("failed to checkout the commit: %w", err) + } + + return nil +} diff --git a/pkg/repos/git/git_test.go b/pkg/repos/git/git_test.go index 5b66d49f..573bf0bb 100644 --- a/pkg/repos/git/git_test.go +++ b/pkg/repos/git/git_test.go @@ -17,7 +17,7 @@ var ( ) func TestFetch(t *testing.T) { - err := Fetch(context.Background(), testCacheHome, + err := fetch(context.Background(), testCacheHome, "https://github.com/gptscript-ai/dalle-image-generation.git", testCommit) require.NoError(t, err) From 4197b39ed68fad8f631b8c124e71dbda0691a47b Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Mon, 12 Aug 2024 15:56:11 -0400 Subject: [PATCH 28/83] fix: address panic when listing models with default model provider Apparently, this code path is exercised by listing models and not by using the provider with LLM calls. Signed-off-by: Donnie Adams --- pkg/remote/remote.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index 6a21413b..baa54677 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -189,7 +189,7 @@ func (c *Client) load(ctx context.Context, toolName string) (*openai.Client, err client: oClient, url: url, } - return client.client, nil + return oClient, nil } func (c *Client) retrieveAPIKey(ctx context.Context, env, url string) (string, error) { From 071c5f2284bd3d09607086e1a1866eedae11cb48 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Mon, 12 Aug 2024 13:00:29 -0700 Subject: [PATCH 29/83] bug: check for git on mac by using xcode-select --- pkg/repos/git/git_go.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/repos/git/git_go.go b/pkg/repos/git/git_go.go index aa76c765..8f6517a2 100644 --- a/pkg/repos/git/git_go.go +++ b/pkg/repos/git/git_go.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "os/exec" + "runtime" "sync" "github.com/go-git/go-git/v5" @@ -24,8 +25,13 @@ func usePureGo() bool { return true } gitCheck.Do(func() { - _, err := exec.LookPath("git") - externalGit = err == nil + if runtime.GOOS == "darwin" { + if exec.Command("xcode-select", "-p").Run() == nil { + externalGit = true + } + } else if _, err := exec.LookPath("git"); err == nil { + externalGit = true + } }) return !externalGit } From 707d48380f6ed55566828c64f098d3603327370b Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Mon, 12 Aug 2024 16:04:29 -0700 Subject: [PATCH 30/83] bug: include shared context from context tools referenced by "tools:" --- pkg/tests/testdata/TestToolRefAll/call1.golden | 2 +- pkg/tests/testdata/TestToolRefAll/test.gpt | 8 ++++++++ pkg/types/tool.go | 12 +++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pkg/tests/testdata/TestToolRefAll/call1.golden b/pkg/tests/testdata/TestToolRefAll/call1.golden index 4957014d..ef36e3fb 100644 --- a/pkg/tests/testdata/TestToolRefAll/call1.golden +++ b/pkg/tests/testdata/TestToolRefAll/call1.golden @@ -52,7 +52,7 @@ "role": "system", "content": [ { - "text": "\nContext Body\nMain tool" + "text": "\nShared context\n\nContext Body\nMain tool" } ], "usage": {} diff --git a/pkg/tests/testdata/TestToolRefAll/test.gpt b/pkg/tests/testdata/TestToolRefAll/test.gpt index 93c4ea05..423cf766 100644 --- a/pkg/tests/testdata/TestToolRefAll/test.gpt +++ b/pkg/tests/testdata/TestToolRefAll/test.gpt @@ -11,11 +11,19 @@ Agent body --- name: context type: context +share context: sharedcontext #!sys.echo Context Body +--- +name: sharedcontext + +#!sys.echo + +Shared context + --- name: none param: noneArg: stuff diff --git a/pkg/types/tool.go b/pkg/types/tool.go index 789215b6..b59a1953 100644 --- a/pkg/types/tool.go +++ b/pkg/types/tool.go @@ -546,7 +546,17 @@ func (t Tool) getExportedTools(prg Program) ([]ToolReference, error) { func (t Tool) GetContextTools(prg Program) ([]ToolReference, error) { result := &toolRefSet{} result.AddAll(t.getDirectContextToolRefs(prg)) - result.AddAll(t.getCompletionToolRefs(prg, nil, ToolTypeContext)) + + contextRefs, err := t.getCompletionToolRefs(prg, nil, ToolTypeContext) + if err != nil { + return nil, err + } + + for _, contextRef := range contextRefs { + result.AddAll(prg.ToolSet[contextRef.ToolID].getExportedContext(prg)) + result.Add(contextRef) + } + return result.List() } From 89cff8726f180fe0511980ff1ae1ae162b914f64 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Mon, 12 Aug 2024 22:13:23 -0700 Subject: [PATCH 31/83] feat: add sys.model.provider.credential --- go.mod | 2 +- go.sum | 4 +- pkg/builtin/builtin.go | 42 +++++++++++--- pkg/credentials/credential.go | 1 + pkg/engine/cmd.go | 2 +- pkg/engine/engine.go | 8 ++- pkg/llm/proxy.go | 104 ++++++++++++++++++++++++++++++++++ pkg/llm/registry.go | 57 ++++++++++++++++++- pkg/openai/client.go | 7 +++ pkg/runner/runner.go | 27 ++++++--- pkg/tests/tester/runner.go | 4 ++ pkg/types/tool.go | 11 +++- pkg/types/toolstring.go | 2 +- 13 files changed, 245 insertions(+), 26 deletions(-) create mode 100644 pkg/llm/proxy.go diff --git a/go.mod b/go.mod index 4cf0ba73..3cfbc98e 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/gptscript-ai/broadcaster v0.0.0-20240625175512-c43682019b86 - github.com/gptscript-ai/chat-completion-client v0.0.0-20240531200700-af8e7ecf0379 + github.com/gptscript-ai/chat-completion-client v0.0.0-20240813051153-a440ada7e3c3 github.com/gptscript-ai/cmd v0.0.0-20240802230653-326b7baf6fcb github.com/gptscript-ai/go-gptscript v0.9.4-0.20240801203434-840b14393b17 github.com/gptscript-ai/tui v0.0.0-20240804004233-efc5673dc76e diff --git a/go.sum b/go.sum index 1b518130..85a3f76e 100644 --- a/go.sum +++ b/go.sum @@ -200,8 +200,8 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gptscript-ai/broadcaster v0.0.0-20240625175512-c43682019b86 h1:m9yLtIEd0z1ia8qFjq3u0Ozb6QKwidyL856JLJp6nbA= github.com/gptscript-ai/broadcaster v0.0.0-20240625175512-c43682019b86/go.mod h1:lK3K5EZx4dyT24UG3yCt0wmspkYqrj4D/8kxdN3relk= -github.com/gptscript-ai/chat-completion-client v0.0.0-20240531200700-af8e7ecf0379 h1:vYnXoIyCXzaCEw0sYifQ4bDpsv3/fO/dZ2suEsTwCIo= -github.com/gptscript-ai/chat-completion-client v0.0.0-20240531200700-af8e7ecf0379/go.mod h1:7P/o6/IWa1KqsntVf68hSnLKuu3+xuqm6lYhch1w4jo= +github.com/gptscript-ai/chat-completion-client v0.0.0-20240813051153-a440ada7e3c3 h1:EQiFTZv+BnOWJX2B9XdF09fL2Zj7h19n1l23TpWCafc= +github.com/gptscript-ai/chat-completion-client v0.0.0-20240813051153-a440ada7e3c3/go.mod h1:7P/o6/IWa1KqsntVf68hSnLKuu3+xuqm6lYhch1w4jo= github.com/gptscript-ai/cmd v0.0.0-20240802230653-326b7baf6fcb h1:ky2J2CzBOskC7Jgm2VJAQi2x3p7FVGa+2/PcywkFJuc= github.com/gptscript-ai/cmd v0.0.0-20240802230653-326b7baf6fcb/go.mod h1:DJAo1xTht1LDkNYFNydVjTHd576TC7MlpsVRl3oloVw= github.com/gptscript-ai/go-gptscript v0.9.4-0.20240801203434-840b14393b17 h1:BTfJ6ls31Roq42lznlZnuPzRf0wrT8jT+tWcvq7wDXY= diff --git a/pkg/builtin/builtin.go b/pkg/builtin/builtin.go index f6811549..23db5152 100644 --- a/pkg/builtin/builtin.go +++ b/pkg/builtin/builtin.go @@ -26,14 +26,15 @@ import ( ) var SafeTools = map[string]struct{}{ - "sys.abort": {}, - "sys.chat.finish": {}, - "sys.chat.history": {}, - "sys.chat.current": {}, - "sys.echo": {}, - "sys.prompt": {}, - "sys.time.now": {}, - "sys.context": {}, + "sys.abort": {}, + "sys.chat.finish": {}, + "sys.chat.history": {}, + "sys.chat.current": {}, + "sys.echo": {}, + "sys.prompt": {}, + "sys.time.now": {}, + "sys.context": {}, + "sys.model.provider.credential": {}, } var tools = map[string]types.Tool{ @@ -248,6 +249,15 @@ var tools = map[string]types.Tool{ BuiltinFunc: SysContext, }, }, + "sys.model.provider.credential": { + ToolDef: types.ToolDef{ + Parameters: types.Parameters{ + Description: "A credential tool to set the OPENAI_API_KEY and OPENAI_BASE_URL to give access to the default model provider", + Arguments: types.ObjectSchema(), + }, + BuiltinFunc: SysModelProviderCredential, + }, + }, } func ListTools() (result []types.Tool) { @@ -678,6 +688,22 @@ func invalidArgument(input string, err error) string { return fmt.Sprintf("Failed to parse arguments %s: %v", input, err) } +func SysModelProviderCredential(ctx context.Context, _ []string, _ string, _ chan<- string) (string, error) { + engineContext, _ := engine.FromContext(ctx) + auth, url, err := engineContext.Engine.Model.ProxyInfo() + if err != nil { + return "", err + } + data, err := json.Marshal(map[string]any{ + "env": map[string]string{ + "OPENAI_API_KEY": auth, + "OPENAI_BASE_URL": url, + }, + "ephemeral": true, + }) + return string(data), err +} + func SysContext(ctx context.Context, _ []string, _ string, _ chan<- string) (string, error) { engineContext, _ := engine.FromContext(ctx) diff --git a/pkg/credentials/credential.go b/pkg/credentials/credential.go index 3d1e2192..f589a065 100644 --- a/pkg/credentials/credential.go +++ b/pkg/credentials/credential.go @@ -24,6 +24,7 @@ type Credential struct { ToolName string `json:"toolName"` Type CredentialType `json:"type"` Env map[string]string `json:"env"` + Ephemeral bool `json:"ephemeral,omitempty"` ExpiresAt *time.Time `json:"expiresAt"` RefreshToken string `json:"refreshToken"` } diff --git a/pkg/engine/cmd.go b/pkg/engine/cmd.go index 14b41183..960bcfe8 100644 --- a/pkg/engine/cmd.go +++ b/pkg/engine/cmd.go @@ -109,7 +109,7 @@ func (e *Engine) runCommand(ctx Context, tool types.Tool, input string, toolCate } }() - return tool.BuiltinFunc(ctx.WrappedContext(), e.Env, input, progress) + return tool.BuiltinFunc(ctx.WrappedContext(e), e.Env, input, progress) } var instructions []string diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index d3daa674..20ca43a9 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -16,6 +16,7 @@ import ( type Model interface { Call(ctx context.Context, messageRequest types.CompletionRequest, status chan<- types.CompletionStatus) (*types.CompletionMessage, error) + ProxyInfo() (string, string, error) } type RuntimeManager interface { @@ -79,6 +80,7 @@ type Context struct { Parent *Context LastReturn *Return CurrentReturn *Return + Engine *Engine Program *types.Program // Input is saved only so that we can render display text, don't use otherwise Input string @@ -250,8 +252,10 @@ func FromContext(ctx context.Context) (*Context, bool) { return c, ok } -func (c *Context) WrappedContext() context.Context { - return context.WithValue(c.Ctx, engineContext{}, c) +func (c *Context) WrappedContext(e *Engine) context.Context { + cp := *c + cp.Engine = e + return context.WithValue(c.Ctx, engineContext{}, &cp) } func (e *Engine) Start(ctx Context, input string) (ret *Return, _ error) { diff --git a/pkg/llm/proxy.go b/pkg/llm/proxy.go new file mode 100644 index 00000000..7c3091b3 --- /dev/null +++ b/pkg/llm/proxy.go @@ -0,0 +1,104 @@ +package llm + +import ( + "bytes" + "encoding/json" + "io" + "net" + "net/http" + "net/http/httputil" + "net/url" + "path" + "strings" + + "github.com/gptscript-ai/gptscript/pkg/builtin" + "github.com/gptscript-ai/gptscript/pkg/openai" +) + +func (r *Registry) ProxyInfo() (string, string, error) { + r.proxyLock.Lock() + defer r.proxyLock.Unlock() + + if r.proxyURL != "" { + return r.proxyToken, r.proxyURL, nil + } + + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return "", "", err + } + + go func() { + _ = http.Serve(l, r) + r.proxyLock.Lock() + defer r.proxyLock.Unlock() + _ = l.Close() + r.proxyURL = "" + }() + + r.proxyURL = "http://" + l.Addr().String() + return r.proxyToken, r.proxyURL, nil +} + +func (r *Registry) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if r.proxyToken != strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer ") { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + inBytes, err := io.ReadAll(req.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var ( + model string + data = map[string]any{} + ) + + if json.Unmarshal(inBytes, &data) == nil { + model, _ = data["model"].(string) + } + + if model == "" { + model = builtin.GetDefaultModel() + } + + c, err := r.getClient(req.Context(), model) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + oai, ok := c.(*openai.Client) + if !ok { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + + auth, targetURL := oai.ProxyInfo() + if targetURL == "" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + + newURL, err := url.Parse(targetURL) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + newURL.Path = path.Join(newURL.Path, req.URL.Path) + + rp := httputil.ReverseProxy{ + Director: func(proxyReq *http.Request) { + proxyReq.Body = io.NopCloser(bytes.NewReader(inBytes)) + proxyReq.URL = newURL + proxyReq.Header.Del("Authorization") + proxyReq.Header.Add("Authorization", "Bearer "+auth) + proxyReq.Host = newURL.Hostname() + }, + } + rp.ServeHTTP(w, req) +} diff --git a/pkg/llm/registry.go b/pkg/llm/registry.go index c568b43c..8129c788 100644 --- a/pkg/llm/registry.go +++ b/pkg/llm/registry.go @@ -5,7 +5,10 @@ import ( "errors" "fmt" "sort" + "sync" + "github.com/google/uuid" + "github.com/gptscript-ai/gptscript/pkg/env" "github.com/gptscript-ai/gptscript/pkg/openai" "github.com/gptscript-ai/gptscript/pkg/remote" "github.com/gptscript-ai/gptscript/pkg/types" @@ -18,11 +21,16 @@ type Client interface { } type Registry struct { - clients []Client + proxyToken string + proxyURL string + proxyLock sync.Mutex + clients []Client } func NewRegistry() *Registry { - return &Registry{} + return &Registry{ + proxyToken: env.VarOrDefault("GPTSCRIPT_INTERNAL_PROXY_TOKEN", uuid.New().String()), + } } func (r *Registry) AddClient(client Client) error { @@ -44,6 +52,10 @@ func (r *Registry) ListModels(ctx context.Context, providers ...string) (result func (r *Registry) fastPath(modelName string) Client { // This is optimization hack to avoid doing List Models + if len(r.clients) == 1 { + return r.clients[0] + } + if len(r.clients) != 2 { return nil } @@ -66,6 +78,47 @@ func (r *Registry) fastPath(modelName string) Client { return r.clients[0] } +func (r *Registry) getClient(ctx context.Context, modelName string) (Client, error) { + if c := r.fastPath(modelName); c != nil { + return c, nil + } + + var errs []error + var oaiClient *openai.Client + for _, client := range r.clients { + ok, err := client.Supports(ctx, modelName) + if err != nil { + // If we got an OpenAI invalid auth error back, store the OpenAI client for later. + if errors.Is(err, openai.InvalidAuthError{}) { + oaiClient = client.(*openai.Client) + } + + errs = append(errs, err) + } else if ok { + return client, nil + } + } + + if len(errs) > 0 && oaiClient != nil { + // Prompt the user to enter their OpenAI API key and try again. + if err := oaiClient.RetrieveAPIKey(ctx); err != nil { + return nil, err + } + ok, err := oaiClient.Supports(ctx, modelName) + if err != nil { + return nil, err + } else if ok { + return oaiClient, nil + } + } + + if len(errs) == 0 { + return nil, fmt.Errorf("failed to find a model provider for model [%s]", modelName) + } + + return nil, errors.Join(errs...) +} + func (r *Registry) Call(ctx context.Context, messageRequest types.CompletionRequest, status chan<- types.CompletionStatus) (*types.CompletionMessage, error) { if messageRequest.Model == "" { return nil, fmt.Errorf("model is required") diff --git a/pkg/openai/client.go b/pkg/openai/client.go index 53252895..42a1a39e 100644 --- a/pkg/openai/client.go +++ b/pkg/openai/client.go @@ -130,6 +130,13 @@ func NewClient(ctx context.Context, credStore credentials.CredentialStore, opts }, nil } +func (c *Client) ProxyInfo() (token, urlBase string) { + if c.invalidAuth { + return "", "" + } + return c.c.GetAPIKeyAndBaseURL() +} + func (c *Client) ValidAuth() error { if c.invalidAuth { return InvalidAuthError{} diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index a8d88fee..f92b0705 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -872,6 +872,11 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env return nil, fmt.Errorf("failed to parse credential tool %q: %w", ref.Reference, err) } + if callCtx.Program.ToolSet[ref.ToolID].IsNoop() { + // ignore empty tools + continue + } + credName := toolName if credentialAlias != "" { credName = credentialAlias @@ -944,6 +949,10 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env return nil, fmt.Errorf("invalid state: credential tool [%s] can not result in a continuation", ref.Reference) } + if *res.Result == "" { + continue + } + if err := json.Unmarshal([]byte(*res.Result), &c); err != nil { return nil, fmt.Errorf("failed to unmarshal credential tool %s response: %w", ref.Reference, err) } @@ -958,15 +967,17 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env } } - // Only store the credential if the tool is on GitHub or has an alias, and the credential is non-empty. - if (isGitHubTool(toolName) && callCtx.Program.ToolSet[ref.ToolID].Source.Repo != nil) || credentialAlias != "" { - if isEmpty { - log.Warnf("Not saving empty credential for tool %s", toolName) - } else if err := r.credStore.Add(callCtx.Ctx, *c); err != nil { - return nil, fmt.Errorf("failed to add credential for tool %s: %w", toolName, err) + if !c.Ephemeral { + // Only store the credential if the tool is on GitHub or has an alias, and the credential is non-empty. + if (isGitHubTool(toolName) && callCtx.Program.ToolSet[ref.ToolID].Source.Repo != nil) || credentialAlias != "" { + if isEmpty { + log.Warnf("Not saving empty credential for tool %s", toolName) + } else if err := r.credStore.Add(callCtx.Ctx, *c); err != nil { + return nil, fmt.Errorf("failed to add credential for tool %s: %w", toolName, err) + } + } else { + log.Warnf("Not saving credential for tool %s - credentials will only be saved for tools from GitHub, or tools that use aliases.", toolName) } - } else { - log.Warnf("Not saving credential for tool %s - credentials will only be saved for tools from GitHub, or tools that use aliases.", toolName) } } diff --git a/pkg/tests/tester/runner.go b/pkg/tests/tester/runner.go index a36c5e91..66337ff5 100644 --- a/pkg/tests/tester/runner.go +++ b/pkg/tests/tester/runner.go @@ -31,6 +31,10 @@ type Result struct { Err error } +func (c *Client) ProxyInfo() (string, string, error) { + return "test-auth", "test-url", nil +} + func (c *Client) Call(_ context.Context, messageRequest types.CompletionRequest, _ chan<- types.CompletionStatus) (resp *types.CompletionMessage, respErr error) { msgData, err := json.MarshalIndent(messageRequest, "", " ") require.NoError(c.t, err) diff --git a/pkg/types/tool.go b/pkg/types/tool.go index b59a1953..57ce3fbf 100644 --- a/pkg/types/tool.go +++ b/pkg/types/tool.go @@ -753,7 +753,16 @@ func (t Tool) GetCredentialTools(prg Program, agentGroup []ToolReference) ([]Too result.AddAll(t.getCompletionToolRefs(prg, nil, ToolTypeCredential)) - toolRefs, err := t.getCompletionToolRefs(prg, agentGroup) + toolRefs, err := result.List() + if err != nil { + return nil, err + } + for _, toolRef := range toolRefs { + referencedTool := prg.ToolSet[toolRef.ToolID] + result.AddAll(referencedTool.GetToolRefsFromNames(referencedTool.ExportCredentials)) + } + + toolRefs, err = t.getCompletionToolRefs(prg, agentGroup) if err != nil { return nil, err } diff --git a/pkg/types/toolstring.go b/pkg/types/toolstring.go index 64f53638..2be6d0fc 100644 --- a/pkg/types/toolstring.go +++ b/pkg/types/toolstring.go @@ -74,7 +74,7 @@ func ToSysDisplayString(id string, args map[string]string) (string, error) { return fmt.Sprintf("Removing `%s`", args["location"]), nil case "sys.write": return fmt.Sprintf("Writing `%s`", args["filename"]), nil - case "sys.context", "sys.stat", "sys.getenv", "sys.abort", "sys.chat.current", "sys.chat.finish", "sys.chat.history", "sys.echo", "sys.prompt", "sys.time.now": + case "sys.context", "sys.stat", "sys.getenv", "sys.abort", "sys.chat.current", "sys.chat.finish", "sys.chat.history", "sys.echo", "sys.prompt", "sys.time.now", "sys.model.provider.credential": return "", nil default: return "", fmt.Errorf("unknown tool for display string: %s", id) From a4f3253870cd9d55ca1a3c5f42eca67098d5b56d Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Tue, 13 Aug 2024 09:40:50 -0700 Subject: [PATCH 32/83] bug: allow asterick on go binary checksum files --- pkg/repos/runtimes/golang/golang.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/repos/runtimes/golang/golang.go b/pkg/repos/runtimes/golang/golang.go index f82b628a..3514fadb 100644 --- a/pkg/repos/runtimes/golang/golang.go +++ b/pkg/repos/runtimes/golang/golang.go @@ -223,10 +223,9 @@ func getChecksum(ctx context.Context, rel *release) string { scan := bufio.NewScanner(resp.Body) for scan.Scan() { fields := strings.Fields(scan.Text()) - if len(fields) != 2 || fields[1] != rel.srcBinName() { - continue + if len(fields) == 2 && (fields[1] == rel.srcBinName() || fields[1] == "*"+rel.srcBinName()) { + return fields[0] } - return fields[0] } return "" From 5c608125e1af9b9a0bba0d34079eebecc576fbac Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Tue, 13 Aug 2024 14:43:19 -0400 Subject: [PATCH 33/83] feat: download binaries for cred helpers Signed-off-by: Donnie Adams --- pkg/cli/credential.go | 2 +- pkg/cli/credential_delete.go | 2 +- pkg/cli/credential_show.go | 2 +- pkg/credentials/util.go | 4 +- pkg/engine/engine.go | 2 +- pkg/gptscript/gptscript.go | 2 +- pkg/repos/get.go | 74 ++++++++++++++--------------- pkg/repos/runtimes/golang/golang.go | 55 ++++++++++++--------- pkg/runner/runtimemanager.go | 2 +- 9 files changed, 77 insertions(+), 68 deletions(-) diff --git a/pkg/cli/credential.go b/pkg/cli/credential.go index b0c4a30a..cb000125 100644 --- a/pkg/cli/credential.go +++ b/pkg/cli/credential.go @@ -58,7 +58,7 @@ func (c *Credential) Run(cmd *cobra.Command, _ []string) error { opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir) } - if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg, opts.Env); err != nil { + if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg); err != nil { return err } diff --git a/pkg/cli/credential_delete.go b/pkg/cli/credential_delete.go index 9c986c54..4e9919df 100644 --- a/pkg/cli/credential_delete.go +++ b/pkg/cli/credential_delete.go @@ -40,7 +40,7 @@ func (c *Delete) Run(cmd *cobra.Command, args []string) error { opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir) } - if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg, opts.Env); err != nil { + if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg); err != nil { return err } diff --git a/pkg/cli/credential_show.go b/pkg/cli/credential_show.go index ccfe3675..fac1b719 100644 --- a/pkg/cli/credential_show.go +++ b/pkg/cli/credential_show.go @@ -42,7 +42,7 @@ func (c *Show) Run(cmd *cobra.Command, args []string) error { opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir) } - if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg, opts.Env); err != nil { + if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg); err != nil { return err } diff --git a/pkg/credentials/util.go b/pkg/credentials/util.go index 367b4d1d..70f31e97 100644 --- a/pkg/credentials/util.go +++ b/pkg/credentials/util.go @@ -5,7 +5,7 @@ import ( ) type CredentialHelperDirs struct { - RevisionFile, LastCheckedFile, BinDir, RepoDir, HelperDir string + RevisionFile, LastCheckedFile, BinDir string } func GetCredentialHelperDirs(cacheDir string) CredentialHelperDirs { @@ -13,7 +13,5 @@ func GetCredentialHelperDirs(cacheDir string) CredentialHelperDirs { RevisionFile: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "revision"), LastCheckedFile: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "last-checked"), BinDir: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "bin"), - RepoDir: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "repo"), - HelperDir: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers"), } } diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index d3daa674..cb8fe273 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -21,7 +21,7 @@ type Model interface { type RuntimeManager interface { GetContext(ctx context.Context, tool types.Tool, cmd, env []string) (string, []string, error) EnsureCredentialHelpers(ctx context.Context) error - SetUpCredentialHelpers(ctx context.Context, cliCfg *config.CLIConfig, env []string) error + SetUpCredentialHelpers(ctx context.Context, cliCfg *config.CLIConfig) error } type Engine struct { diff --git a/pkg/gptscript/gptscript.go b/pkg/gptscript/gptscript.go index 43f429fc..755fe632 100644 --- a/pkg/gptscript/gptscript.go +++ b/pkg/gptscript/gptscript.go @@ -99,7 +99,7 @@ func New(ctx context.Context, o ...Options) (*GPTScript, error) { opts.Runner.RuntimeManager = runtimes.Default(cacheClient.CacheDir()) } - if err := opts.Runner.RuntimeManager.SetUpCredentialHelpers(context.Background(), cliCfg, opts.Env); err != nil { + if err := opts.Runner.RuntimeManager.SetUpCredentialHelpers(context.Background(), cliCfg); err != nil { return nil, err } diff --git a/pkg/repos/get.go b/pkg/repos/get.go index 416f4c61..8981d1fa 100644 --- a/pkg/repos/get.go +++ b/pkg/repos/get.go @@ -8,6 +8,7 @@ import ( "io/fs" "os" "path/filepath" + "runtime" "strings" "sync" "time" @@ -15,15 +16,13 @@ import ( "github.com/BurntSushi/locker" "github.com/gptscript-ai/gptscript/pkg/config" "github.com/gptscript-ai/gptscript/pkg/credentials" + runtimeEnv "github.com/gptscript-ai/gptscript/pkg/env" "github.com/gptscript-ai/gptscript/pkg/hash" - "github.com/gptscript-ai/gptscript/pkg/loader/github" "github.com/gptscript-ai/gptscript/pkg/repos/git" "github.com/gptscript-ai/gptscript/pkg/repos/runtimes/golang" "github.com/gptscript-ai/gptscript/pkg/types" ) -const credentialHelpersRepo = "github.com/gptscript-ai/gptscript-credential-helpers" - type Runtime interface { ID() string Supports(tool types.Tool, cmd []string) bool @@ -68,7 +67,6 @@ type credHelperConfig struct { lock sync.Mutex initialized bool cliCfg *config.CLIConfig - env []string } func New(cacheDir string, runtimes ...Runtime) *Manager { @@ -90,7 +88,7 @@ func (m *Manager) EnsureCredentialHelpers(ctx context.Context) error { defer m.credHelperConfig.lock.Unlock() if !m.credHelperConfig.initialized { - if err := m.deferredSetUpCredentialHelpers(ctx, m.credHelperConfig.cliCfg, m.credHelperConfig.env); err != nil { + if err := m.deferredSetUpCredentialHelpers(ctx, m.credHelperConfig.cliCfg); err != nil { return err } m.credHelperConfig.initialized = true @@ -99,27 +97,28 @@ func (m *Manager) EnsureCredentialHelpers(ctx context.Context) error { return nil } -func (m *Manager) SetUpCredentialHelpers(_ context.Context, cliCfg *config.CLIConfig, env []string) error { +func (m *Manager) SetUpCredentialHelpers(_ context.Context, cliCfg *config.CLIConfig) error { m.credHelperConfig = &credHelperConfig{ cliCfg: cliCfg, - env: env, } return nil } -func (m *Manager) deferredSetUpCredentialHelpers(ctx context.Context, cliCfg *config.CLIConfig, env []string) error { +func (m *Manager) deferredSetUpCredentialHelpers(ctx context.Context, cliCfg *config.CLIConfig) error { var ( - helperName = cliCfg.CredentialsStore - suffix string + helperName = cliCfg.CredentialsStore + distInfo, suffix string ) - if helperName == "wincred" { - suffix = ".exe" - } - - // The file helper is built-in and does not need to be compiled. + // The file helper is built-in and does not need to be downloaded. if helperName == "file" { return nil } + switch helperName { + case "wincred": + suffix = ".exe" + default: + distInfo = fmt.Sprintf("-%s-%s", runtime.GOOS, runtime.GOARCH) + } locker.Lock("gptscript-credential-helpers") defer locker.Unlock("gptscript-credential-helpers") @@ -137,13 +136,7 @@ func (m *Manager) deferredSetUpCredentialHelpers(ctx context.Context, cliCfg *co } } - // Load the credential helpers repo information. - _, _, repo, _, err := github.Load(ctx, nil, credentialHelpersRepo) - if err != nil { - return err - } - - if err := os.MkdirAll(m.credHelperDirs.HelperDir, 0755); err != nil { + if err := os.MkdirAll(filepath.Dir(m.credHelperDirs.LastCheckedFile), 0755); err != nil { return err } @@ -152,37 +145,44 @@ func (m *Manager) deferredSetUpCredentialHelpers(ctx context.Context, cliCfg *co return err } - var needsBuild bool + tool := types.Tool{ + Source: types.ToolSource{ + Repo: &types.Repo{ + Root: runtimeEnv.VarOrDefault("GPTSCRIPT_CRED_HELPERS_ROOT", "https://github.com/gptscript-ai/gptscript-credential-helpers.git"), + }, + }, + } + tag, err := golang.GetLatestTag(tool) + if err != nil { + return err + } + var needsDownloaded bool // Check the last revision shasum and see if it is different from the current one. lastRevision, err := os.ReadFile(m.credHelperDirs.RevisionFile) - if (err == nil && strings.TrimSpace(string(lastRevision)) != repo.Revision) || errors.Is(err, fs.ErrNotExist) { + if (err == nil && strings.TrimSpace(string(lastRevision)) != tool.Source.Repo.Root+tag) || errors.Is(err, fs.ErrNotExist) { // Need to pull the latest version. - needsBuild = true - if err := git.Checkout(ctx, m.gitDir, repo.Root, repo.Revision, filepath.Join(m.credHelperDirs.RepoDir, repo.Revision)); err != nil { - return err - } + needsDownloaded = true // Update the revision file to the new revision. - if err := os.WriteFile(m.credHelperDirs.RevisionFile, []byte(repo.Revision), 0644); err != nil { + if err = os.WriteFile(m.credHelperDirs.RevisionFile, []byte(tool.Source.Repo.Root+tag), 0644); err != nil { return err } } else if err != nil { return err } - if !needsBuild { - // Check for the existence of the gptscript-credential-osxkeychain binary. - // If it's there, we have no need to build it and can just return. - if _, err := os.Stat(filepath.Join(m.credHelperDirs.BinDir, "gptscript-credential-"+helperName+suffix)); err == nil { + if !needsDownloaded { + // Check for the existence of the credential helper binary. + // If it's there, we have no need to download it and can just return. + if _, err = os.Stat(filepath.Join(m.credHelperDirs.BinDir, "gptscript-credential-"+helperName+suffix)); err == nil { return nil } } // Find the Go runtime and use it to build the credential helper. - for _, runtime := range m.runtimes { - if strings.HasPrefix(runtime.ID(), "go") { - goRuntime := runtime.(*golang.Runtime) - return goRuntime.BuildCredentialHelper(ctx, helperName, m.credHelperDirs, m.runtimeDir, repo.Revision, env) + for _, rt := range m.runtimes { + if strings.HasPrefix(rt.ID(), "go") { + return rt.(*golang.Runtime).DownloadCredentialHelper(ctx, tool, helperName, distInfo, suffix, m.credHelperDirs.BinDir) } } diff --git a/pkg/repos/runtimes/golang/golang.go b/pkg/repos/runtimes/golang/golang.go index 3514fadb..52e8fe0b 100644 --- a/pkg/repos/runtimes/golang/golang.go +++ b/pkg/repos/runtimes/golang/golang.go @@ -18,7 +18,6 @@ import ( "runtime" "strings" - "github.com/gptscript-ai/gptscript/pkg/credentials" "github.com/gptscript-ai/gptscript/pkg/debugcmd" runtimeEnv "github.com/gptscript-ai/gptscript/pkg/env" "github.com/gptscript-ai/gptscript/pkg/hash" @@ -97,6 +96,14 @@ type tag struct { } `json:"commit"` } +func GetLatestTag(tool types.Tool) (string, error) { + r, ok := getLatestRelease(tool) + if !ok { + return "", fmt.Errorf("failed to get latest release for %s", tool.Name) + } + return r.label, nil +} + func getLatestRelease(tool types.Tool) (*release, bool) { if tool.Source.Repo == nil || !strings.HasPrefix(tool.Source.Repo.Root, "https://github.com/") { return nil, false @@ -116,11 +123,14 @@ func getLatestRelease(tool types.Tool) (*release, bool) { account, repo := parts[1], parts[2] resp, err := client.Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/tags", account, repo)) - if err != nil || resp.StatusCode != http.StatusOK { + if err != nil { // ignore error return nil, false } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, false + } var tags []tag if err := json.NewDecoder(resp.Body).Decode(&tags); err != nil { @@ -137,11 +147,14 @@ func getLatestRelease(tool types.Tool) (*release, bool) { } resp, err = client.Get(fmt.Sprintf("https://github.com/%s/%s/releases/latest", account, repo)) - if err != nil || resp.StatusCode != http.StatusFound { + if err != nil { // ignore error return nil, false } defer resp.Body.Close() + if resp.StatusCode != http.StatusFound { + return nil, false + } target := resp.Header.Get("Location") if target == "" { @@ -212,7 +225,7 @@ func downloadBin(ctx context.Context, checksum, src, url, bin string) error { return nil } -func getChecksum(ctx context.Context, rel *release) string { +func getChecksum(ctx context.Context, rel *release, artifactName string) string { resp, err := get(ctx, rel.checksumTxt()) if err != nil { // ignore error @@ -223,7 +236,7 @@ func getChecksum(ctx context.Context, rel *release) string { scan := bufio.NewScanner(resp.Body) for scan.Scan() { fields := strings.Fields(scan.Text()) - if len(fields) == 2 && (fields[1] == rel.srcBinName() || fields[1] == "*"+rel.srcBinName()) { + if len(fields) == 2 && (fields[1] == artifactName || fields[1] == "*"+artifactName) { return fields[0] } } @@ -241,7 +254,7 @@ func (r *Runtime) Binary(ctx context.Context, tool types.Tool, _, toolSource str return false, nil, nil } - checksum := getChecksum(ctx, rel) + checksum := getChecksum(ctx, rel, rel.srcBinName()) if checksum == "" { return false, nil, nil } @@ -268,30 +281,28 @@ func (r *Runtime) Setup(ctx context.Context, _ types.Tool, dataRoot, toolSource return newEnv, nil } -func (r *Runtime) BuildCredentialHelper(ctx context.Context, helperName string, credHelperDirs credentials.CredentialHelperDirs, dataRoot, revision string, env []string) error { +func (r *Runtime) DownloadCredentialHelper(ctx context.Context, tool types.Tool, helperName, distInfo, suffix string, binDir string) error { if helperName == "file" { return nil } - var suffix string - if helperName == "wincred" { - suffix = ".exe" + rel, ok := getLatestRelease(tool) + if !ok { + return fmt.Errorf("failed to find %s release", r.ID()) + } + binaryName := "gptscript-credential-" + helperName + checksum := getChecksum(ctx, rel, binaryName+distInfo+suffix) + if checksum == "" { + return fmt.Errorf("failed to find %s release checksum for os=%s arch=%s", r.ID(), runtime.GOOS, runtime.GOARCH) } - binPath, err := r.getRuntime(ctx, dataRoot) - if err != nil { - return err + url, _ := strings.CutSuffix(rel.binURL(), rel.srcBinName()) + url += binaryName + distInfo + suffix + if err := downloadBin(ctx, checksum, strings.TrimSuffix(binDir, "bin"), url, binaryName+suffix); err != nil { + return fmt.Errorf("failed to download %s release for os=%s arch=%s: %w", r.ID(), runtime.GOOS, runtime.GOARCH, err) } - newEnv := runtimeEnv.AppendPath(env, binPath) - log.InfofCtx(ctx, "Building credential helper %s", helperName) - cmd := debugcmd.New(ctx, filepath.Join(binPath, "go"), - "build", "-buildvcs=false", "-o", - filepath.Join(credHelperDirs.BinDir, "gptscript-credential-"+helperName+suffix), - fmt.Sprintf("./%s/cmd/", helperName)) - cmd.Env = stripGo(append(env, newEnv...)) - cmd.Dir = filepath.Join(credHelperDirs.RepoDir, revision) - return cmd.Run() + return nil } func (r *Runtime) getReleaseAndDigest() (string, string, error) { diff --git a/pkg/runner/runtimemanager.go b/pkg/runner/runtimemanager.go index e1c5a4c6..ed191d15 100644 --- a/pkg/runner/runtimemanager.go +++ b/pkg/runner/runtimemanager.go @@ -45,6 +45,6 @@ func (r runtimeManagerLogger) EnsureCredentialHelpers(ctx context.Context) error return r.rm.EnsureCredentialHelpers(mvl.WithInfo(ctx, r)) } -func (r runtimeManagerLogger) SetUpCredentialHelpers(_ context.Context, _ *config.CLIConfig, _ []string) error { +func (r runtimeManagerLogger) SetUpCredentialHelpers(_ context.Context, _ *config.CLIConfig) error { panic("not implemented") } From 983cd8c9e58342e520c91a7a2c4a1c556ec85fec Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Tue, 13 Aug 2024 15:07:17 -0700 Subject: [PATCH 34/83] chore: add load method to sdk server --- pkg/repos/runtimes/default.go | 2 +- pkg/repos/runtimes/node/SHASUMS256.txt.asc | 165 +++++++-------------- pkg/repos/runtimes/node/node_test.go | 14 -- pkg/sdkserver/routes.go | 38 +++++ pkg/sdkserver/types.go | 9 ++ 5 files changed, 102 insertions(+), 126 deletions(-) diff --git a/pkg/repos/runtimes/default.go b/pkg/repos/runtimes/default.go index 3782e26e..d4eb4db5 100644 --- a/pkg/repos/runtimes/default.go +++ b/pkg/repos/runtimes/default.go @@ -22,7 +22,7 @@ var Runtimes = []repos.Runtime{ Version: "3.10", }, &node.Runtime{ - Version: "21", + Version: "20", Default: true, }, &golang.Runtime{ diff --git a/pkg/repos/runtimes/node/SHASUMS256.txt.asc b/pkg/repos/runtimes/node/SHASUMS256.txt.asc index 093da96b..faaeab97 100644 --- a/pkg/repos/runtimes/node/SHASUMS256.txt.asc +++ b/pkg/repos/runtimes/node/SHASUMS256.txt.asc @@ -1,117 +1,60 @@ -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 -43a881788549e1b3425eb5f2b92608f438f146e08213de09c5bd5ff841cae7ae node-v20.11.1-aix-ppc64.tar.gz -3f8e77b775372c0b27d2b85ce899d80339691f480e64dde43d4eb01504a58679 node-v20.11.1-arm64.msi -e0065c61f340e85106a99c4b54746c5cee09d59b08c5712f67f99e92aa44995d node-v20.11.1-darwin-arm64.tar.gz -fd771bf3881733bfc0622128918ae6baf2ed1178146538a53c30ac2f7006af5b node-v20.11.1-darwin-arm64.tar.xz -c52e7fb0709dbe63a4cbe08ac8af3479188692937a7bd8e776e0eedfa33bb848 node-v20.11.1-darwin-x64.tar.gz -ed69f1f300beb75fb4cad45d96aacd141c3ddca03b6d77c76b42cb258202363d node-v20.11.1-darwin-x64.tar.xz -0aa42c91b441e945ff43bd3a837759c58b436de57dcd033d02e5cbcd2fba1f87 node-v20.11.1-headers.tar.gz -edce238817acf5adce3123366b55304aff2a1f0849231d1b49f42370e454b6f8 node-v20.11.1-headers.tar.xz -e34ab2fc2726b4abd896bcbff0250e9b2da737cbd9d24267518a802ed0606f3b node-v20.11.1-linux-arm64.tar.gz -c957f29eb4e341903520caf362534f0acd1db7be79c502ae8e283994eed07fe1 node-v20.11.1-linux-arm64.tar.xz -e42791f76ece283c7a4b97fbf716da72c5128c54a9779f10f03ae74a4bcfb8f6 node-v20.11.1-linux-armv7l.tar.gz -28e0120d2d150a8f41717899d33167b8b32053778665583d49ff971bfd188d1b node-v20.11.1-linux-armv7l.tar.xz -9823305ac3a66925a9b61d8032f6bbb4c3e33c28e7f957ebb27e49732feffb23 node-v20.11.1-linux-ppc64le.tar.gz -51343cacf5cdf5c4b5e93e919d19dd373d6ef43d5f2c666eae299f26e31d08b5 node-v20.11.1-linux-ppc64le.tar.xz -4c66b2f247fdd8720853321526d7cda483018fcb32014b75c30f3a54ecacaea7 node-v20.11.1-linux-s390x.tar.gz -b32616b705cd0ddbb230b95c693e3d7a37becc2ced9bcadea8dc824cceed6be0 node-v20.11.1-linux-s390x.tar.xz -bf3a779bef19452da90fb88358ec2c57e0d2f882839b20dc6afc297b6aafc0d7 node-v20.11.1-linux-x64.tar.gz -d8dab549b09672b03356aa2257699f3de3b58c96e74eb26a8b495fbdc9cf6fbe node-v20.11.1-linux-x64.tar.xz -f1cd449fcbeb1b948e8498cb8edd9655fa319d109a7f4c5bd96a9b122b91538a node-v20.11.1-win-arm64.7z -e85461ec124956a2853c4ee6e13c4f4889d63c88beb3d530c1ee0c4b51dc10e7 node-v20.11.1-win-arm64.zip -fb9b5348259988a562a48eed7349e7e716c0bec78d98ad0a336b2993a8b3bf34 node-v20.11.1-win-x64.7z -bc032628d77d206ffa7f133518a6225a9c5d6d9210ead30d67e294ff37044bda node-v20.11.1-win-x64.zip -c2b1863d8979546804a39fc63d0a9bc9c6e49cb2f6c9d1e52844a24629b24765 node-v20.11.1-win-x86.7z -b98e95f78416d1359b647cfa09ba2a48b76d41b56a776df822bf36ffe8e76a2d node-v20.11.1-win-x86.zip -c54f5f7e2416e826fd84e878f28e3b53363ae9c3f60a140af4434b2453b5ae89 node-v20.11.1-x64.msi -63e2aed4dabb96eed6903a3974e006d3c29c218472aac60ae3c3c7de00df13b1 node-v20.11.1-x86.msi -c46019a095a1549d000e85da13f17972a448e0be5854a51786ecccde7278a012 node-v20.11.1.pkg -4af1ba6ea848cc05908b8a62b02fb27684dd52b2a7988ee82b0cfa72deb90b94 node-v20.11.1.tar.gz -77813edbf3f7f16d2d35d3353443dee4e61d5ee84d9e3138c7538a3c0ca5209e node-v20.11.1.tar.xz -a5a9d30a8f7d56e00ccb27c1a7d24c8d0bc96a2689ebba8eb7527698793496f1 win-arm64/node.exe -93529170cebe57c0f4830a4cc6a261b6cc9bcf0cd8b3e88ac4995a5015031d79 win-arm64/node.lib -c14c6e927406b8683cbfb8a67ca4c8fd5093ca7812b5b1627e3d6a53d3674565 win-arm64/node_pdb.7z -68034cd09d8dfaa755d1b280da13e20388cc486ac57b037b3e11dfe2d6b74284 win-arm64/node_pdb.zip -bc585910690318aaebe3c57669cb83ca9d1e5791efd63195e238f54686e6c2ec win-x64/node.exe -53a982d490cb9fcc4b231a8b95147de423b36186bc6f4ba5697b20117fdcbd5d win-x64/node.lib -ccac9f2f5219ed858aeddb306d6493478ba9675c7cbf009e83742437d6752c4f win-x64/node_pdb.7z -bec5da4035c84580843978a59ef9bcc1c0eaca881cf9e1c94e63a1862cf14421 win-x64/node_pdb.zip -3829137e062b1e2eb9947ef05e4b717ae578a8fce1c5c60fe4f6ae7ef2ec0240 win-x86/node.exe -c5321bb65dcecb3989f9b8f6ec56369c16627ca4bade0c78afb6b88f7dde50e4 win-x86/node.lib -20ca60ced1fc21f15ea952b4406aec6bde39d20eab11cf042040628841b2249e win-x86/node_pdb.7z -bef05cebedce5949ae35e87e7d4789c16fa73caf478483fcf92e5dbb9ba5d774 win-x86/node_pdb.zip +5eb1b7ea405c86be0a21ec3850997c89df238d6e4659a0b990aa793a8cbfd9cf node-v20.16.0-aix-ppc64.tar.gz +f366fe5903dcb3b6cd495c8add77c87a32772085718a672d52ad17d9d91d2018 node-v20.16.0-arm64.msi +fc7355e778b181575153b7dea4879e8021776eeb376c43c50f65893d2ea70aa3 node-v20.16.0-darwin-arm64.tar.gz +5043e98cdf859963b1a0aff54c1f1813a2a8059e4179403171860d664ca090f2 node-v20.16.0-darwin-arm64.tar.xz +e18942cd706e4d69a4845ddacee2f1c17a72e853a229e3d2623d2edeb7efde72 node-v20.16.0-darwin-x64.tar.gz +9df751ac5edbb2181335200060dff14de25f828eaed70d8b48459d2c203aeedc node-v20.16.0-darwin-x64.tar.xz +6cc5690a67b9b1e1fa8cedaeca41f1bdb5e1af1f7948761c798d33d99f789a5c node-v20.16.0-headers.tar.gz +a1464c304980d3ab41922cda7025ebc2ec0dc2a0b89d9b9183c589560810feaa node-v20.16.0-headers.tar.xz +551588f8f5ca05c04efb53f1b2bb7d9834603327bdc82d60a944d385569866e1 node-v20.16.0-linux-arm64.tar.gz +1d9929e72f692179f884cd676b2dfabd879cb77defa7869dc8cfc802619277fb node-v20.16.0-linux-arm64.tar.xz +1c77c52ab507ddee479012f0b4bf523dd8400df4504447d623632353076e2e27 node-v20.16.0-linux-armv7l.tar.gz +a23a49029e8c7788c701eb3ace553260b7676a5a2ea9965ba92e4817008fbefe node-v20.16.0-linux-armv7l.tar.xz +80b515595e46afb9bae77f61083a4ca7c21bbdb627f69ff53fd5dca3a26773fb node-v20.16.0-linux-ppc64le.tar.gz +86cf6e8c93a9e517bfcfdfb4ad2774105312679ad21e03da75ab516ebc10e2dc node-v20.16.0-linux-ppc64le.tar.xz +ae7a9f6e631a0bede76a501d8b1d806f56b97acfa5a1d6833bab5ce90a404e5e node-v20.16.0-linux-s390x.tar.gz +6c38ac5c516a6a36ee6e0426975e6466795db30b9ced04e59f0f33fe6b3d657e node-v20.16.0-linux-s390x.tar.xz +b3f874ea84e440d69ed02ca92429d0eccd17737fde86db69c1c153d16ec654f2 node-v20.16.0-linux-x64.tar.gz +c30af7dfea46de7d8b9b370fa33b8b15440bc93f0a686af8601bbb48b82f16c0 node-v20.16.0-linux-x64.tar.xz +55852a420ca41db9f128f97e0dd8751199c23d63f5a7978432fd7c9e0c74c323 node-v20.16.0.pkg +8f24bf9abe455a09ab30f9ae8edda1e945ed678a4b1c3b07ee0f901fdc0ff4fd node-v20.16.0.tar.gz +cd6c8fc3ff2606aadbc7155db6f7e77247d2d0065ac18e2f7f049095584b8b46 node-v20.16.0.tar.xz +52e5666a379acd8533d9ccab66c2321a6ffc83766248419bfbd41ba8bc071244 node-v20.16.0-win-arm64.7z +af5a85ea299fcebd34c3c726a47a926e73171f9b657a6eaa796c011597241bf8 node-v20.16.0-win-arm64.zip +1b3961054a484476872715d9ca04bc491d797fde6336db514b6e6fcbb71fae9d node-v20.16.0-win-x64.7z +4e88373ac5ae859ad4d50cc3c5fa86eb3178d089b72e64c4dbe6eeac5d7b5979 node-v20.16.0-win-x64.zip +76f1806fde0b09ed4044f29ea140fb2bea9bce745b9892ec4aeb6537344db6f1 node-v20.16.0-win-x86.7z +1adc1f086595ecbc98da40eccb42fa1691b6c6c0658ff875dda19e4e02b1d5f0 node-v20.16.0-win-x86.zip +813306c94e6f5f061a5789f037d48f57d52240284a679e5ace4a0f73f8f2feeb node-v20.16.0-x64.msi +2bb8c3084384c95c47c4191c38098d5ecf55c0f02c1de5e0968730dec957ea15 node-v20.16.0-x86.msi +7e773fba3a19eac5ccbe85c1f87a05d7b112ecf41440076e6b6de1c7bffa0fdf win-arm64/node.exe +a4f01329c1c211082ac3ed387ff6651530040bbf7250ec419ce8f95b10d7804a win-arm64/node.lib +e1bec70ae9529cc637a21de850c070125f8016070451094d72f96408001200a2 win-arm64/node_pdb.7z +bc5b60eecd3b6c92b35755adef2e4aad01e021a3c434d46c2555a49056c5bcf7 win-arm64/node_pdb.zip +ba221658a3b68bd583e3068903eb675b5206d86a883c084ed95502e8f634b82a win-x64/node.exe +87056190b7cd06f40058f8e059efd328cdcc7600b825afa102c0aa5039865af5 win-x64/node.lib +bf2ad1e1f4e7c3853d5209fe9ef24ad7117edafc71f6401ec0121d8b681b8c3c win-x64/node_pdb.7z +5386f3c3af1af1b325b43b574043c5a7e830b3e9e7df0370ae0797ce4f39b375 win-x64/node_pdb.zip +b7b8d6b5fdd1c073b6f5f6d15bc849f4b5f92c4a66f23e77294f4bdf5f51e9f6 win-x86/node.exe +fa02ae7feca7eb6c4a0f1b929126df400719f5d18a2ec4b7d12c52fbe0b13814 win-x86/node.lib +328b2dcc91255c1c75faa8ce7eb687a8960ae09555d3bca0ae8e0dac4238c873 win-x86/node_pdb.7z +71b1e6b75c61227342ba6f1edb9014445dbee857d6cb14dce3d9b8d94c694d55 win-x86/node_pdb.zip -----BEGIN PGP SIGNATURE----- -iQGzBAEBCAAdFiEEiQwI24V5Fi/uDfnbi+q0389VXvQFAmXM+TcACgkQi+q0389V -XvQl3AwAqqm2uBMDzd+BlR1sG7y/eUtUYPVdwmCh0DeFXPHxuaIbFf0PGMEgcV8u -kn3OBF4pnSCPZNbJYJsLO1S+b/5Vk+Vlkq1WkOxqQHUHmM9GcJUuShadl0YaDNen -WXXMoYKWqMRJ6fQ3tRRh+vbMSXtsLqXT8TMVJq+Qb7a7yj4QRjw/Dd+8uKGGIhBY -U04HWsz33RJLu6AUnhF03eO1N8E1V48JptklDx5ZkY8GYa3F6jQsFld+jhmkZ9tg -4q9NDNijVpj56UsUhLAYD0J9IKS18tvQxNrKmBGUSZjFOByVhbUdLXnSMtW1i1U9 -cYhP6Q5wg/fnjqCfQ90TauoJZOblKIL/PHlf6cQGPrrRa1bz3xGyCAIve5KFhLxf -Vfj1ctk2ktzmuNhjAu5G/1VALQUNpiTm4Yz433JpoMMZ3mTHN+fuALOX4TQbdLRz -HKphTz02436348XC9bNz2cvjm74cy9fqwjQ/y84AmxiTJMFPg0XqICg4tu9rd49d -8FJc4TLZ -=r/CD ------END PGP SIGNATURE----- - ------BEGIN PGP SIGNED MESSAGE----- -Hash: SHA256 - -c31d5b60c4a284c6855bd468d4aae4436a16b351362b2971d3c0db2a471d3f24 node-v21.7.0-aix-ppc64.tar.gz -7d7dc37aa30b6dbb52d698d01cfed1d99056c82396eadd41a49fc55a873c423d node-v21.7.0-arm64.msi -f48ad51cf3c2814bbf61c8c26efd810e5e22dcac980786fd7ac5b54365233d2c node-v21.7.0-darwin-arm64.tar.gz -0805239d8a7402dae49e0033b7ad8208fed498dbeee9a3194863e52f6f3c6d7f node-v21.7.0-darwin-arm64.tar.xz -3f81adca80b523b413e26f03f855f4a2ae52d9af20f0cda2e259dd26a0608607 node-v21.7.0-darwin-x64.tar.gz -6a755416292854f2be38e74704ccf09edeba247718e9f047f5e1939b0dba17bd node-v21.7.0-darwin-x64.tar.xz -628d9e4f866e3828b77ce812dc99f33d3e7c673d0c499f13eadff6fa6ccb4383 node-v21.7.0-headers.tar.gz -627d6552d2120660a51f74fff0d40573f4a35d8545462250d30592ce0ba4eec7 node-v21.7.0-headers.tar.xz -520a3e5c83a05a782b1f4959f150c2fdc03e2ea056e855ef6bbb74f6ccf7aa7d node-v21.7.0-linux-arm64.tar.gz -73ce1e4e956532e0916fc7014f5b649573bd2b5870fef5cfc26cc42f58358ae7 node-v21.7.0-linux-arm64.tar.xz -723abb32135ad4baa6e9671447a72f5c9a5bfc681fc540b0e4864e965171b6ed node-v21.7.0-linux-armv7l.tar.gz -8a367a3bf667f0bb3abb9e8121326911d47a31886419ad052d5a52d8c6531d9d node-v21.7.0-linux-armv7l.tar.xz -c2290cb35b11ee2b0f0ae34ad3c8372652688ff2dc3d9a89ada46c2b84ea5dda node-v21.7.0-linux-ppc64le.tar.gz -b85348211a4d195de2f850a17cdec77aedc8fc1c402864b2bc3501608e6c9c47 node-v21.7.0-linux-ppc64le.tar.xz -90b8678ed113950613edeae5eaf298cf795c72005fac6ffd9b7fbb90ddd86738 node-v21.7.0-linux-s390x.tar.gz -99a09f4c790f3210a6d26032bf69713ba199cf2e73af43e04b1b1d9bd1c8db76 node-v21.7.0-linux-s390x.tar.xz -0fce039e2b6af00766492127a49f959ae92ed22fede4c49e9a8c2543aadbd6e2 node-v21.7.0-linux-x64.tar.gz -68510c3851133a21c6a6f9940e58c5bc8fed39f1d91a08e34c5070dd0615fef1 node-v21.7.0-linux-x64.tar.xz -d680d5c3d0b2476a97d11b30cbbdaf1d7f92ffd1cc89e5c640782a6b52480666 node-v21.7.0-win-arm64.7z -11b11b9a3f2db7b5076cf16655e05cd63dc3d8843cd4836ecb12e11315f03441 node-v21.7.0-win-arm64.zip -31c8b4721f37e30ca8e2131a4cb848fc7347f67bf87618e82959b58481f17bc4 node-v21.7.0-win-x64.7z -204de88f4073b08ae3dbe4c412b071eee565fc681e163be205d5cc88065f0322 node-v21.7.0-win-x64.zip -b17ef0c5557e61610774cae5beb0f877699ab419c4672e9c6e3bb3da3d571ed1 node-v21.7.0-win-x86.7z -6aba3fe2258d5c0c40a89e81dfe90113a67489f2a67fd05b7f216b63b4c7bb02 node-v21.7.0-win-x86.zip -512945cf8816e1e906143ea2ee6816f8744a3d114ea38f3540c3ebe685fe3e3a node-v21.7.0-x64.msi -4bedb6069c94a71fd6f0b8fbea280468d5ecdcf209eef6da1a45808e8b15cba6 node-v21.7.0-x86.msi -ccac99782e587c6090b6ad82979210fa0c352322636a6cf290d37eb41152d0b5 node-v21.7.0.pkg -26d6b600e1076f132d4175a90ddc1a709263e75d81967300aa1ffbd86103b991 node-v21.7.0.tar.gz -e41eefe1e59624ee7f312c38f8f7dfc11595641acb2293d21176f03d2763e9d4 node-v21.7.0.tar.xz -25511d1e05d7d0b049945c5ef1cf2a4daa5d6ad16692ccd2c1399142a1c57a65 win-arm64/node.exe -7920932f7be355dbf4568492ab7d104fc059f689ba1a46ac0d6568884c8d201a win-arm64/node.lib -40c423a2b126fc5b6858f8617f0f8537fd8f8d2fa73a5c918607f3ccd386f8c9 win-arm64/node_pdb.7z -dec9eaa91a431ea0f3c243605f0556dbe6459df5c04c10df7935d678a6b3fca4 win-arm64/node_pdb.zip -c486fe72a3663379105538e886ef9d2deacad1deaa64b338e570cb086be592d3 win-x64/node.exe -96d09c2055c2f252122c86b65d2aabd5f90b1a075844f24bf8bcdbab05baf53e win-x64/node.lib -08990dd6bcce80710d59ef76cd74ab98b5bed36b0d2584ca3acbc029f92db4fc win-x64/node_pdb.7z -1a27a25c92f6339b3aa77588063cca537af389aee26bfdf1d0ef505d790e63a3 win-x64/node_pdb.zip -4aaa5b3a95ee4ab932a80b9708c31662a9c4a99d19fea7cb1f7b0ff79d8399ed win-x86/node.exe -6e2502e84c3a0e2da643f6399b59381ade5b525f544a5bcabae923188b8f9998 win-x86/node.lib -d0cd5494364039f558c76d4fc7a1db69739149873e10a5200fb9e2a0ab12fe10 win-x86/node_pdb.7z -354031f3f9576733ebeeccbcafcc691c8326427153a48978ff5cd6f2c8ef5d36 win-x86/node_pdb.zip ------BEGIN PGP SIGNATURE----- - -iQGzBAEBCAAdFiEEiQwI24V5Fi/uDfnbi+q0389VXvQFAmXouAIACgkQi+q0389V -XvRp+wv+IPHjBUmVC6YzAxFhRD4GHVUgjckfSbP2jH/acre1mYgm9LJ//7l2GaJy -oEOO85WaHgaKCHCdv9GBc3dDbbt1n9J2IGmBqcdE8e9cRko5qhBoVUvW7p7Ki7ci -nAq5DS3YkpWAocsY/k+LyR0Ky8mW466ARAucTp9kuZmxB2FW53B0bYK57++1qGuo -tr9kJPoGYQB0cUiTSwTaMbOIdl/4CL+a9J7mIrpaDVW5g3PnNy5y1vgDvtuU7Qcn -uEucciBlOn0Ib4mBnky+NX1ThL9WNwLjaivxdioFgc0E4sMwf0CjF3vMUuEvI8qi -PJ5lYndsHI4fdh1SbcgoFNZzTkMZbTr9xcZIGLzLkMX8r+ztLTiFtiLIUSQq0jgm -fqKQghuDN2SVi7WW4KAa7K1285zmV7L27N9mnNWH4ujTqCW73Wdo2XkG/TwM3yEC -5o+YookAV6RHT1X6RPJ8rQaC0BrBgpm/MQH1kvH4vUyF2HRVZ2ZgEYorvKtOwf9D -f7v3IC9J -=/YNz +iQIzBAEBCAAdFiEEzGj1oxBv9EgyLkjtJ/XjjVsKIV8FAmag7tcACgkQJ/XjjVsK +IV/hlRAAjvdBRTWPfjXzTqxQODXLZps1HREXRZAa8C8bbAoagCJ1jfm6d8yVUegH +Bl5FqDAutGfTlEhXtQqBmnbQPv4Ahj156cVYtp3dFrxPF15bP+o9q+Un5+R1zcfX +kH9W26G7IvfrtFJkDClpBlPKYE5HcDrrJBNfvALf4th17bkVHMpr04oJz5IwGV2M +petLMwqFxcqQ+15tzRW42Z6EhWHvNaMveab6SM4JEqBxvqB8K+m4nsw2ER7ycU5b +Isa0bUsxtRICtSX0yzzdzEYrFXZmb9eXZRVfJ4sBpUhw0xtBmHn3c1MZH0qez+Nm +tbc6pcgGv9cUSXauBeD8rrYMzQHcrhihd51i9a3Cen3RDy/dtuNx9jEXnxfkY+n9 +wkwKb4Lask962L+yTHQCfJ+JQxgouADxqzMxhcup1iiHXCd7pSBSoeAvd5Z1AeGX +qBYrLU9mcyIuLrbtADSfnWmXWs2k1hgnP3UXMBhu/GuobQf9kJ2Gwwx5Gp0aB8z9 +4EA+oUXnkM2kJF0MYVMXL+z8VcQpHgyVPujglhNn/a4WdCVTr1jKptNqqnriH9zl +bHMZuiKbAt8RL9rQ3XuFD1sN9k1z/mj8bCHES2WVta+3kCmY9u+eKXNdXJYt+5Xh +bGxwXP5T+Z8Yzc9FVmgPzZzVddCX74Yug0j8BUyE3vPaDq32H6M= +=cZH5 -----END PGP SIGNATURE----- diff --git a/pkg/repos/runtimes/node/node_test.go b/pkg/repos/runtimes/node/node_test.go index 014619c8..ce2fcde1 100644 --- a/pkg/repos/runtimes/node/node_test.go +++ b/pkg/repos/runtimes/node/node_test.go @@ -37,17 +37,3 @@ func TestRuntime(t *testing.T) { } require.NoError(t, err) } - -func TestRuntime21(t *testing.T) { - r := Runtime{ - Version: "21", - } - - s, err := r.Setup(context.Background(), types.Tool{}, testCacheHome, "testdata", os.Environ()) - require.NoError(t, err) - _, err = os.Stat(filepath.Join(firstPath(s), "node.exe")) - if errors.Is(err, fs.ErrNotExist) { - _, err = os.Stat(filepath.Join(firstPath(s), "node")) - } - require.NoError(t, err) -} diff --git a/pkg/sdkserver/routes.go b/pkg/sdkserver/routes.go index 98957624..e19f5708 100644 --- a/pkg/sdkserver/routes.go +++ b/pkg/sdkserver/routes.go @@ -50,6 +50,8 @@ func (s *server) addRoutes(mux *http.ServeMux) { mux.HandleFunc("POST /run", s.execHandler) mux.HandleFunc("POST /evaluate", s.execHandler) + mux.HandleFunc("POST /load", s.load) + mux.HandleFunc("POST /parse", s.parse) mux.HandleFunc("POST /fmt", s.fmtDocument) @@ -212,6 +214,42 @@ func (s *server) execHandler(w http.ResponseWriter, r *http.Request) { s.execAndStream(ctx, programLoader, logger, w, opts, reqObject.ChatState, reqObject.Input, reqObject.SubTool, def) } +// load will load the file and return the corresponding Program. +func (s *server) load(w http.ResponseWriter, r *http.Request) { + logger := gcontext.GetLogger(r.Context()) + reqObject := new(loadRequest) + if err := json.NewDecoder(r.Body).Decode(reqObject); err != nil { + writeError(logger, w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err)) + return + } + + logger.Debugf("parsing file: file=%s, content=%s", reqObject.File, reqObject.Content) + + var ( + prg types.Program + err error + cache = s.client.Cache + ) + + if reqObject.DisableCache { + cache = nil + } + + if reqObject.Content != "" { + prg, err = loader.ProgramFromSource(r.Context(), reqObject.Content, reqObject.SubTool, loader.Options{Cache: cache}) + } else if reqObject.File != "" { + prg, err = loader.Program(r.Context(), reqObject.File, reqObject.SubTool, loader.Options{Cache: cache}) + } else { + prg, err = loader.ProgramFromSource(r.Context(), reqObject.ToolDefs.String(), reqObject.SubTool, loader.Options{Cache: cache}) + } + if err != nil { + writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to load program: %w", err)) + return + } + + writeResponse(logger, w, map[string]any{"stdout": map[string]any{"program": prg}}) +} + // parse will parse the file and return the corresponding Document. func (s *server) parse(w http.ResponseWriter, r *http.Request) { logger := gcontext.GetLogger(r.Context()) diff --git a/pkg/sdkserver/types.go b/pkg/sdkserver/types.go index ade035b2..b24ca645 100644 --- a/pkg/sdkserver/types.go +++ b/pkg/sdkserver/types.go @@ -82,6 +82,15 @@ func (f *file) String() string { return f.File } +type loadRequest struct { + content `json:",inline"` + + ToolDefs toolDefs `json:"toolDefs,inline"` + DisableCache bool `json:"disableCache"` + SubTool string `json:"subTool,omitempty"` + File string `json:"file"` +} + type parseRequest struct { parser.Options `json:",inline"` content `json:",inline"` From b522585a9f94a37aa17449c64071809d83d9bbc9 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Wed, 14 Aug 2024 14:38:27 -0400 Subject: [PATCH 35/83] chore: upgrade Go to 1.23.0 Signed-off-by: Donnie Adams --- .github/workflows/integration.yaml | 2 +- .github/workflows/main.yaml | 4 +--- .github/workflows/release.yaml | 4 +--- .github/workflows/test.yaml | 2 +- .github/workflows/validate-docs.yaml | 2 +- Makefile | 2 +- go.mod | 3 +-- pkg/loader/openapi.go | 2 +- pkg/openapi/getschema.go | 2 +- pkg/repos/runtimes/default.go | 2 +- pkg/repos/runtimes/golang/digests.txt | 20 ++++++++++---------- pkg/repos/runtimes/golang/golang_test.go | 2 +- pkg/repos/runtimes/golang/testdata/go.mod | 2 +- pkg/repos/runtimes/node/node.go | 2 +- pkg/sdkserver/routes.go | 2 +- 15 files changed, 24 insertions(+), 29 deletions(-) diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index e0b78be2..22f4a678 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -29,7 +29,7 @@ jobs: - uses: actions/setup-go@v5 with: cache: false - go-version: "1.22" + go-version: "1.23" - name: Build if: matrix.os == 'ubuntu-22.04' run: make build diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index ae26c52c..7a2114ea 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -26,9 +26,7 @@ jobs: uses: actions/setup-go@v5 with: cache: false - # This can't be upgraded until the issue with sys.daemon on Windows is resolved - # After the issue is resolved, this can be set to 1.22 - go-version: "1.22.4" + go-version: "1.23" - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f710e953..8b5b0eae 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -20,9 +20,7 @@ jobs: uses: actions/setup-go@v5 with: cache: false - # This can't be upgraded until the issue with sys.daemon on Windows is resolved - # After the issue is resolved, this can be set to 1.22 - go-version: "1.22.4" + go-version: "1.23" - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f3829c35..f4da7e62 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -29,7 +29,7 @@ jobs: - uses: actions/setup-go@v5 with: cache: false - go-version: "1.22" + go-version: "1.23" - name: Validate if: matrix.os == 'ubuntu-22.04' run: make validate diff --git a/.github/workflows/validate-docs.yaml b/.github/workflows/validate-docs.yaml index 18368355..f7e3a016 100644 --- a/.github/workflows/validate-docs.yaml +++ b/.github/workflows/validate-docs.yaml @@ -17,6 +17,6 @@ jobs: - uses: actions/setup-go@v5 with: cache: false - go-version: "1.22" + go-version: "1.23" - run: make init-docs - run: make validate-docs diff --git a/Makefile b/Makefile index 5b1b6309..4a52694a 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ smoke: build smoke: go test -v -tags='smoke' ./pkg/tests/smoke/... -GOLANGCI_LINT_VERSION ?= v1.59.0 +GOLANGCI_LINT_VERSION ?= v1.60.1 lint: if ! command -v golangci-lint &> /dev/null; then \ echo "Could not find golangci-lint, installing version $(GOLANGCI_LINT_VERSION)."; \ diff --git a/go.mod b/go.mod index 4cf0ba73..0d38ca0c 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,6 @@ module github.com/gptscript-ai/gptscript -// This can't be upgraded until the issue with sys.daemon on Windows is resolved -go 1.22.4 +go 1.23.0 require ( github.com/AlecAivazis/survey/v2 v2.3.7 diff --git a/pkg/loader/openapi.go b/pkg/loader/openapi.go index bc469a4e..cf3c3f34 100644 --- a/pkg/loader/openapi.go +++ b/pkg/loader/openapi.go @@ -86,7 +86,7 @@ func getOpenAPITools(t *openapi3.T, defaultHost, source, targetToolName string) pathObj := pathMap[pathString] // Handle path-level server override, if one exists pathServer := defaultServer - if pathObj.Servers != nil && len(pathObj.Servers) > 0 { + if len(pathObj.Servers) > 0 { pathServer, err = parseServer(pathObj.Servers[0]) if err != nil { return nil, err diff --git a/pkg/openapi/getschema.go b/pkg/openapi/getschema.go index 3550afcf..d2966aac 100644 --- a/pkg/openapi/getschema.go +++ b/pkg/openapi/getschema.go @@ -83,7 +83,7 @@ func GetSchema(operationID, defaultHost string, t *openapi3.T) (string, Operatio for path, pathItem := range t.Paths.Map() { // Handle path-level server override, if one exists. pathServer := defaultServer - if pathItem.Servers != nil && len(pathItem.Servers) > 0 { + if len(pathItem.Servers) > 0 { pathServer, err = parseServer(pathItem.Servers[0]) if err != nil { return "", OperationInfo{}, false, err diff --git a/pkg/repos/runtimes/default.go b/pkg/repos/runtimes/default.go index d4eb4db5..a93fb735 100644 --- a/pkg/repos/runtimes/default.go +++ b/pkg/repos/runtimes/default.go @@ -26,7 +26,7 @@ var Runtimes = []repos.Runtime{ Default: true, }, &golang.Runtime{ - Version: "1.22.1", + Version: "1.23.0", }, } diff --git a/pkg/repos/runtimes/golang/digests.txt b/pkg/repos/runtimes/golang/digests.txt index 8a1b82c6..df86facf 100644 --- a/pkg/repos/runtimes/golang/digests.txt +++ b/pkg/repos/runtimes/golang/digests.txt @@ -1,10 +1,10 @@ -3bc971772f4712fec0364f4bc3de06af22a00a12daab10b6f717fdcd13156cc0 go1.22.1.darwin-amd64.tar.gz -943e4f9f038239f9911c44366f52ab9202f6ee13610322a668fe42406fb3deef go1.22.1.darwin-amd64.pkg -f6a9cec6b8a002fcc9c0ee24ec04d67f430a52abc3cfd613836986bcc00d8383 go1.22.1.darwin-arm64.tar.gz -5f10b95e2678618f85ba9d87fbed506b3b87efc9d5a8cafda939055cb97949ba go1.22.1.darwin-arm64.pkg -8484df36d3d40139eaf0fe5e647b006435d826cc12f9ae72973bf7ec265e0ae4 go1.22.1.linux-386.tar.gz -aab8e15785c997ae20f9c88422ee35d962c4562212bb0f879d052a35c8307c7f go1.22.1.linux-amd64.tar.gz -e56685a245b6a0c592fc4a55f0b7803af5b3f827aaa29feab1f40e491acf35b8 go1.22.1.linux-arm64.tar.gz -8cb7a90e48c20daed39a6ac8b8a40760030ba5e93c12274c42191d868687c281 go1.22.1.linux-armv6l.tar.gz -0c5ebb7eb39b7884ec99f92b425d4c03a96a72443562aafbf6e7d15c42a3108a go1.22.1.windows-386.zip -cf9c66a208a106402a527f5b956269ca506cfe535fc388e828d249ea88ed28ba go1.22.1.windows-amd64.zip +ffd070acf59f054e8691b838f274d540572db0bd09654af851e4e76ab88403dc go1.23.0.darwin-amd64.tar.gz +bc91d2573939a01731413fac0884c329606c1c168883692131ce772669caf27b go1.23.0.darwin-amd64.pkg +b770812aef17d7b2ea406588e2b97689e9557aac7e646fe76218b216e2c51406 go1.23.0.darwin-arm64.tar.gz +d73ae741ed449ea842238f76f4b02935277eb867689f84ace0640965b2caf700 go1.23.0.darwin-arm64.pkg +0e8a7340c2632e6fb5088d60f95b52be1f8303143e04cd34e9b2314fafc24edd go1.23.0.linux-386.tar.gz +905a297f19ead44780548933e0ff1a1b86e8327bb459e92f9c0012569f76f5e3 go1.23.0.linux-amd64.tar.gz +62788056693009bcf7020eedc778cdd1781941c6145eab7688bd087bce0f8659 go1.23.0.linux-arm64.tar.gz +0efa1338e644d7f74064fa7f1016b5da7872b2df0070ea3b56e4fef63192e35b go1.23.0.linux-armv6l.tar.gz +09448fedec0cdf98ad12397222e0c8bfc835b1d0894c0015ced653534b8d7427 go1.23.0.windows-386.zip +d4be481ef73079ee0ad46081d278923aa3fd78db1b3cf147172592f73e14c1ac go1.23.0.windows-amd64.zip diff --git a/pkg/repos/runtimes/golang/golang_test.go b/pkg/repos/runtimes/golang/golang_test.go index 56098a51..f3d888fd 100644 --- a/pkg/repos/runtimes/golang/golang_test.go +++ b/pkg/repos/runtimes/golang/golang_test.go @@ -25,7 +25,7 @@ func TestRuntime(t *testing.T) { os.RemoveAll("testdata/bin") }) r := Runtime{ - Version: "1.22.1", + Version: "1.23.0", } s, err := r.Setup(context.Background(), types.Tool{}, testCacheHome, "testdata", os.Environ()) diff --git a/pkg/repos/runtimes/golang/testdata/go.mod b/pkg/repos/runtimes/golang/testdata/go.mod index 93eac7fe..6725cbfd 100644 --- a/pkg/repos/runtimes/golang/testdata/go.mod +++ b/pkg/repos/runtimes/golang/testdata/go.mod @@ -1,3 +1,3 @@ module example.com -go 1.22.1 +go 1.23.0 diff --git a/pkg/repos/runtimes/node/node.go b/pkg/repos/runtimes/node/node.go index 01a752e6..aa57b059 100644 --- a/pkg/repos/runtimes/node/node.go +++ b/pkg/repos/runtimes/node/node.go @@ -136,7 +136,7 @@ func (r *Runtime) runNPM(ctx context.Context, tool types.Tool, toolSource, binDi if tool.WorkingDir == "" { return nil } - if _, err := os.Stat(filepath.Join(tool.WorkingDir, packageJSON)); errors.Is(fs.ErrNotExist, err) { + if _, err := os.Stat(filepath.Join(tool.WorkingDir, packageJSON)); errors.Is(err, fs.ErrNotExist) { return nil } else if err != nil { return err diff --git a/pkg/sdkserver/routes.go b/pkg/sdkserver/routes.go index e19f5708..4309bc28 100644 --- a/pkg/sdkserver/routes.go +++ b/pkg/sdkserver/routes.go @@ -271,7 +271,7 @@ func (s *server) parse(w http.ResponseWriter, r *http.Request) { } else { content, loadErr := input.FromLocation(reqObject.File, reqObject.DisableCache) if loadErr != nil { - logger.Errorf(loadErr.Error()) + logger.Errorf("failed to load file: %v", loadErr) writeError(logger, w, http.StatusInternalServerError, loadErr) return } From 538323b7e19ca4d5581272591bbd5dfb3469bc15 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Wed, 14 Aug 2024 14:33:53 -0700 Subject: [PATCH 36/83] chore: allow wildcard matching in metadata key names --- pkg/parser/parser.go | 14 +++++++++++++- pkg/parser/parser_test.go | 6 ++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index b57cb658..956822dd 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -4,6 +4,8 @@ import ( "bufio" "fmt" "io" + "maps" + "path" "regexp" "slices" "strconv" @@ -16,7 +18,7 @@ import ( var ( sepRegex = regexp.MustCompile(`^\s*---+\s*$`) strictSepRegex = regexp.MustCompile(`^---\n$`) - skipRegex = regexp.MustCompile(`^![-.:\w]+\s*$`) + skipRegex = regexp.MustCompile(`^![-.:*\w]+\s*$`) ) func normalize(key string) string { @@ -390,6 +392,16 @@ func assignMetadata(nodes []Node) (result []Node) { for _, node := range nodes { if node.ToolNode != nil { node.ToolNode.Tool.MetaData = metadata[node.ToolNode.Tool.Name] + for wildcard := range metadata { + if strings.Contains(wildcard, "*") { + if m, err := path.Match(wildcard, node.ToolNode.Tool.Name); m && err == nil { + if node.ToolNode.Tool.MetaData == nil { + node.ToolNode.Tool.MetaData = map[string]string{} + } + maps.Copy(node.ToolNode.Tool.MetaData, metadata[wildcard]) + } + } + } } result = append(result, node) } diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go index 3967ebd5..f98b74e2 100644 --- a/pkg/parser/parser_test.go +++ b/pkg/parser/parser_test.go @@ -258,6 +258,11 @@ asdf2 --- !metadata:first:requirements.txt asdf + +--- +!metadata:f*r*:other + +foo bar ` tools, err := ParseTools(strings.NewReader(input)) require.NoError(t, err) @@ -266,5 +271,6 @@ asdf autogold.Expect(map[string]string{ "package.json": "foo=base\nf", "requirements.txt": "asdf", + "other": "foo bar", }).Equal(t, tools[0].MetaData) } From 91a5df67edbf3e526a1e5527c89395dce2afc779 Mon Sep 17 00:00:00 2001 From: Rinor Hoxha Date: Thu, 15 Aug 2024 04:24:22 +0200 Subject: [PATCH 37/83] fix(openapi): don't panic on requestBody content mime without schema (#797) --- pkg/loader/openapi.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/loader/openapi.go b/pkg/loader/openapi.go index cf3c3f34..8dd83892 100644 --- a/pkg/loader/openapi.go +++ b/pkg/loader/openapi.go @@ -209,6 +209,11 @@ func getOpenAPITools(t *openapi3.T, defaultHost, source, targetToolName string) } bodyMIME = mime + // requestBody content mime without schema + if content == nil || content.Schema == nil { + continue + } + arg := content.Schema.Value if arg.Description == "" { arg.Description = content.Schema.Value.Description From 2e75740d300c7a8968292043ba3c42f3b507a3e3 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Thu, 15 Aug 2024 08:43:54 -0700 Subject: [PATCH 38/83] bug: fix default model provider --- pkg/remote/remote.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index baa54677..fa1d40c2 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -25,7 +25,6 @@ type Client struct { clientsLock sync.Mutex cache *cache.Client clients map[string]clientInfo - modelToProvider map[string]string runner *runner.Runner envs []string credStore credentials.CredentialStore @@ -39,17 +38,13 @@ func New(r *runner.Runner, envs []string, cache *cache.Client, credStore credent envs: envs, credStore: credStore, defaultProvider: defaultProvider, - modelToProvider: make(map[string]string), clients: make(map[string]clientInfo), } } func (c *Client) Call(ctx context.Context, messageRequest types.CompletionRequest, status chan<- types.CompletionStatus) (*types.CompletionMessage, error) { - c.clientsLock.Lock() - provider, ok := c.modelToProvider[messageRequest.Model] - c.clientsLock.Unlock() - - if !ok { + _, provider := c.parseModel(messageRequest.Model) + if provider == "" { return nil, fmt.Errorf("failed to find remote model %s", messageRequest.Model) } @@ -108,10 +103,6 @@ func (c *Client) Supports(ctx context.Context, modelString string) (bool, error) return false, err } - c.clientsLock.Lock() - defer c.clientsLock.Unlock() - - c.modelToProvider[modelString] = providerName return true, nil } From 20c983ecf452cd9d65bc1a9ab51f1fb12d71d4fa Mon Sep 17 00:00:00 2001 From: John Engelman Date: Fri, 16 Aug 2024 08:41:58 -0500 Subject: [PATCH 39/83] fix: Append all credentials for OpenAPI security infos (#661) Co-authored-by: John Engelman --- pkg/loader/openapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/loader/openapi.go b/pkg/loader/openapi.go index 8dd83892..e62fc5ef 100644 --- a/pkg/loader/openapi.go +++ b/pkg/loader/openapi.go @@ -305,7 +305,7 @@ func getOpenAPITools(t *openapi3.T, defaultHost, source, targetToolName string) if err != nil { return nil, fmt.Errorf("failed to parse operation server URL: %w", err) } - tool.Credentials = info.GetCredentialToolStrings(operationServerURL.Hostname()) + tool.Credentials = append(tool.Credentials, info.GetCredentialToolStrings(operationServerURL.Hostname())...) } } From 1124ed1c6952c2e8ff033a64a7b5dfc9fc877087 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Fri, 16 Aug 2024 16:34:48 -0400 Subject: [PATCH 40/83] feat: add ability to list models from other providers Signed-off-by: Donnie Adams --- pkg/sdkserver/routes.go | 41 ++++++++++++----------------------------- pkg/sdkserver/types.go | 4 +++- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/pkg/sdkserver/routes.go b/pkg/sdkserver/routes.go index 4309bc28..6cb1e620 100644 --- a/pkg/sdkserver/routes.go +++ b/pkg/sdkserver/routes.go @@ -73,39 +73,13 @@ func (s *server) version(w http.ResponseWriter, r *http.Request) { // listTools will return the output of `gptscript --list-tools` func (s *server) listTools(w http.ResponseWriter, r *http.Request) { logger := gcontext.GetLogger(r.Context()) - var prg types.Program - if r.ContentLength != 0 { - reqObject := new(toolOrFileRequest) - err := json.NewDecoder(r.Body).Decode(reqObject) - if err != nil { - writeError(logger, w, http.StatusBadRequest, fmt.Errorf("failed to decode request body: %w", err)) - return - } - - if reqObject.Content != "" { - prg, err = loader.ProgramFromSource(r.Context(), reqObject.Content, reqObject.SubTool, loader.Options{Cache: s.client.Cache}) - } else if reqObject.File != "" { - prg, err = loader.Program(r.Context(), reqObject.File, reqObject.SubTool, loader.Options{Cache: s.client.Cache}) - } else { - prg, err = loader.ProgramFromSource(r.Context(), reqObject.ToolDefs.String(), reqObject.SubTool, loader.Options{Cache: s.client.Cache}) - } - if err != nil { - writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to load program: %w", err)) - return - } - } - - tools := s.client.ListTools(r.Context(), prg) + tools := s.client.ListTools(r.Context(), types.Program{}) sort.Slice(tools, func(i, j int) bool { return tools[i].Name < tools[j].Name }) lines := make([]string, 0, len(tools)) for _, tool := range tools { - if tool.Name == "" { - tool.Name = prg.Name - } - // Don't print instructions tool.Instructions = "" @@ -118,22 +92,31 @@ func (s *server) listTools(w http.ResponseWriter, r *http.Request) { // listModels will return the output of `gptscript --list-models` func (s *server) listModels(w http.ResponseWriter, r *http.Request) { logger := gcontext.GetLogger(r.Context()) + client := s.client + var providers []string if r.ContentLength != 0 { reqObject := new(modelsRequest) - if err := json.NewDecoder(r.Body).Decode(reqObject); err != nil { + err := json.NewDecoder(r.Body).Decode(reqObject) + if err != nil { writeError(logger, w, http.StatusBadRequest, fmt.Errorf("failed to decode request body: %w", err)) return } providers = reqObject.Providers + + client, err = gptscript.New(r.Context(), s.gptscriptOpts, gptscript.Options{Env: reqObject.Env, Runner: runner.Options{CredentialOverrides: reqObject.CredentialOverrides}}) + if err != nil { + writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to create client: %w", err)) + return + } } if s.gptscriptOpts.DefaultModelProvider != "" { providers = append(providers, s.gptscriptOpts.DefaultModelProvider) } - out, err := s.client.ListModels(r.Context(), providers...) + out, err := client.ListModels(r.Context(), providers...) if err != nil { writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to list models: %w", err)) return diff --git a/pkg/sdkserver/types.go b/pkg/sdkserver/types.go index b24ca645..e26bbba5 100644 --- a/pkg/sdkserver/types.go +++ b/pkg/sdkserver/types.go @@ -100,7 +100,9 @@ type parseRequest struct { } type modelsRequest struct { - Providers []string `json:"providers"` + Providers []string `json:"providers"` + Env []string `json:"env"` + CredentialOverrides []string `json:"credentialOverrides"` } type runInfo struct { From 1226edbeb364745fa656cfaff9ef864054452453 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Mon, 19 Aug 2024 13:18:53 -0400 Subject: [PATCH 41/83] fix: openapi: return validation errors to the LLM; improve confirmation prompt (#805) Signed-off-by: Grant Linville --- pkg/openapi/run.go | 3 ++- pkg/types/toolstring.go | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/openapi/run.go b/pkg/openapi/run.go index 2efc2309..6c7e4ca7 100644 --- a/pkg/openapi/run.go +++ b/pkg/openapi/run.go @@ -42,7 +42,8 @@ func Run(operationID, defaultHost, args string, t *openapi3.T, envs []string) (s } if !validationResult.Valid() { - return "", false, fmt.Errorf("invalid arguments for operation %s: %s", operationID, validationResult.Errors()) + // We don't return an error here because we want the LLM to be able to maintain control and try again. + return fmt.Sprintf("invalid arguments for operation %s: %s", operationID, validationResult.Errors()), true, nil } // Construct and execute the HTTP request. diff --git a/pkg/types/toolstring.go b/pkg/types/toolstring.go index 2be6d0fc..9d6d765b 100644 --- a/pkg/types/toolstring.go +++ b/pkg/types/toolstring.go @@ -3,6 +3,7 @@ package types import ( "encoding/json" "fmt" + "os" "path/filepath" "strings" ) @@ -76,6 +77,11 @@ func ToSysDisplayString(id string, args map[string]string) (string, error) { return fmt.Sprintf("Writing `%s`", args["filename"]), nil case "sys.context", "sys.stat", "sys.getenv", "sys.abort", "sys.chat.current", "sys.chat.finish", "sys.chat.history", "sys.echo", "sys.prompt", "sys.time.now", "sys.model.provider.credential": return "", nil + case "sys.openapi": + if os.Getenv("GPTSCRIPT_OPENAPI_REVAMP") == "true" && args["operation"] != "" { + return fmt.Sprintf("Running API operation `%s` with arguments %s", args["operation"], args["args"]), nil + } + fallthrough default: return "", fmt.Errorf("unknown tool for display string: %s", id) } From 2b18a1d1ca85fb9a9f3b3fb7741697ed8e7a9038 Mon Sep 17 00:00:00 2001 From: Atulpriya Sharma Date: Tue, 20 Aug 2024 12:56:28 +0530 Subject: [PATCH 42/83] Create gcp-assistant.gpt Adding gcp assistant gptscript. Signed-off-by: Atulpriya Sharma --- examples/gcp-assistant.gpt | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 examples/gcp-assistant.gpt diff --git a/examples/gcp-assistant.gpt b/examples/gcp-assistant.gpt new file mode 100644 index 00000000..29b682d0 --- /dev/null +++ b/examples/gcp-assistant.gpt @@ -0,0 +1,28 @@ +Name: GCP Assistant +Description: Agent to help you interact with Google Cloud +Context: learn-gcp, learn-kubectl +Tools: sys.exec, sys.http.html2text?, sys.find, sys.read, sys.write +Chat:true +You are an assistant for Google Cloud Platform (GCP). +Rules +1. Use gcloud CLI to interact with GCP. +2. Assume the user is using Google cloud. + +--- +Name: learn-gcp +Description: A tool to help you learn gcp cli +#!/bin/bash +echo "Current gcloud config:" +gcloud config list || true +--- +Name: learn-kubectl +Description: A tool to help you learn k8s and related commands +#!/bin/bash + +CMDS="kubectl helm" +echo 'The additional CLI commands are available locally, use the `exec` tool to invoke them:' +for i in $CMDS; do + if [ -e "$(command -v $i)" ]; then + echo ' ' $i + fi +done From 5b7d8b75ffad76aed2a46402051f7d48bf166a40 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Tue, 20 Aug 2024 12:03:20 -0400 Subject: [PATCH 43/83] feat: add ability to include metadata with prompts Based on the metadata, the receiver of the prompt event would be able to handle the prompt in a different way, if needed. Signed-off-by: Donnie Adams --- pkg/builtin/builtin.go | 1 + pkg/prompt/prompt.go | 12 ++++++++---- pkg/sdkserver/prompt.go | 6 +----- pkg/types/prompt.go | 7 ++++--- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/pkg/builtin/builtin.go b/pkg/builtin/builtin.go index 23db5152..f972d14c 100644 --- a/pkg/builtin/builtin.go +++ b/pkg/builtin/builtin.go @@ -217,6 +217,7 @@ var tools = map[string]types.Tool{ "message", "The message to display to the user", "fields", "A comma-separated list of fields to prompt for", "sensitive", "(true or false) Whether the input should be hidden", + "metadata", "(optional) A JSON object of metadata to attach to the prompt", ), }, BuiltinFunc: prompt.SysPrompt, diff --git a/pkg/prompt/prompt.go b/pkg/prompt/prompt.go index 44cb20f1..f91a04b6 100644 --- a/pkg/prompt/prompt.go +++ b/pkg/prompt/prompt.go @@ -51,25 +51,29 @@ func sysPromptHTTP(ctx context.Context, envs []string, url string, prompt types. func SysPrompt(ctx context.Context, envs []string, input string, _ chan<- string) (_ string, err error) { var params struct { - Message string `json:"message,omitempty"` - Fields string `json:"fields,omitempty"` - Sensitive string `json:"sensitive,omitempty"` + Message string `json:"message,omitempty"` + Fields string `json:"fields,omitempty"` + Sensitive string `json:"sensitive,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` } if err := json.Unmarshal([]byte(input), ¶ms); err != nil { return "", err } + var fields []string for _, env := range envs { if url, ok := strings.CutPrefix(env, types.PromptURLEnvVar+"="); ok { - var fields []string if params.Fields != "" { fields = strings.Split(params.Fields, ",") } + httpPrompt := types.Prompt{ Message: params.Message, Fields: fields, Sensitive: params.Sensitive == "true", + Metadata: params.Metadata, } + return sysPromptHTTP(ctx, envs, url, httpPrompt) } } diff --git a/pkg/sdkserver/prompt.go b/pkg/sdkserver/prompt.go index 8d34fc53..a519f7b2 100644 --- a/pkg/sdkserver/prompt.go +++ b/pkg/sdkserver/prompt.go @@ -76,11 +76,7 @@ func (s *server) prompt(w http.ResponseWriter, r *http.Request) { }(id) s.events.C <- event{ - Prompt: types.Prompt{ - Message: prompt.Message, - Fields: prompt.Fields, - Sensitive: prompt.Sensitive, - }, + Prompt: prompt, Event: gserver.Event{ RunID: id, Event: runner.Event{ diff --git a/pkg/types/prompt.go b/pkg/types/prompt.go index ea17c11c..653ad066 100644 --- a/pkg/types/prompt.go +++ b/pkg/types/prompt.go @@ -6,7 +6,8 @@ const ( ) type Prompt struct { - Message string `json:"message,omitempty"` - Fields []string `json:"fields,omitempty"` - Sensitive bool `json:"sensitive,omitempty"` + Message string `json:"message,omitempty"` + Fields []string `json:"fields,omitempty"` + Sensitive bool `json:"sensitive,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` } From a387cd8f695f8b0a400376088daf353604620d2a Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Tue, 20 Aug 2024 12:18:59 -0400 Subject: [PATCH 44/83] enhance: pretty-print JSON arguments for OpenAPI operations (#808) Signed-off-by: Grant Linville --- pkg/types/toolstring.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/types/toolstring.go b/pkg/types/toolstring.go index 9d6d765b..086ad043 100644 --- a/pkg/types/toolstring.go +++ b/pkg/types/toolstring.go @@ -79,7 +79,16 @@ func ToSysDisplayString(id string, args map[string]string) (string, error) { return "", nil case "sys.openapi": if os.Getenv("GPTSCRIPT_OPENAPI_REVAMP") == "true" && args["operation"] != "" { - return fmt.Sprintf("Running API operation `%s` with arguments %s", args["operation"], args["args"]), nil + // Pretty print the JSON by unmarshaling and marshaling it + var jsonArgs map[string]any + if err := json.Unmarshal([]byte(args["args"]), &jsonArgs); err != nil { + return "", err + } + jsonPretty, err := json.MarshalIndent(jsonArgs, "", " ") + if err != nil { + return "", err + } + return fmt.Sprintf("Running API operation `%s` with arguments %s", args["operation"], string(jsonPretty)), nil } fallthrough default: From 1202543787fe8cef7aff6bc48cdfa1ad13700c1e Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Tue, 20 Aug 2024 21:46:30 -0400 Subject: [PATCH 45/83] fix: include proper input on call events Signed-off-by: Donnie Adams --- pkg/runner/runner.go | 5 +++++ pkg/sdkserver/monitor.go | 5 +---- pkg/sdkserver/types.go | 7 +++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index f92b0705..93e40670 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -628,11 +628,16 @@ func (r *Runner) resume(callCtx engine.Context, monitor Monitor, env []string, s } } + var content string + if state.ResumeInput != nil { + content = *state.ResumeInput + } monitor.Event(Event{ Time: time.Now(), CallContext: callCtx.GetCallContext(), Type: EventTypeCallContinue, ToolResults: len(callResults), + Content: content, }) e := engine.Engine{ diff --git a/pkg/sdkserver/monitor.go b/pkg/sdkserver/monitor.go index a5b0236b..bdd88c67 100644 --- a/pkg/sdkserver/monitor.go +++ b/pkg/sdkserver/monitor.go @@ -33,6 +33,7 @@ func (s SessionFactory) Start(ctx context.Context, prg *types.Program, env []str Time: time.Now(), Type: runner.EventTypeRunStart, }, + Input: input, RunID: id, Program: prg, }, @@ -43,7 +44,6 @@ func (s SessionFactory) Start(ctx context.Context, prg *types.Program, env []str id: id, prj: prg, env: env, - input: input, events: s.events, }, nil } @@ -56,7 +56,6 @@ type Session struct { id string prj *types.Program env []string - input string events *broadcaster.Broadcaster[event] runLock sync.Mutex } @@ -68,7 +67,6 @@ func (s *Session) Event(e runner.Event) { Event: gserver.Event{ Event: e, RunID: s.id, - Input: s.input, }, } } @@ -87,7 +85,6 @@ func (s *Session) Stop(ctx context.Context, output string, err error) { Type: runner.EventTypeRunFinish, }, RunID: s.id, - Input: s.input, Output: output, }, } diff --git a/pkg/sdkserver/types.go b/pkg/sdkserver/types.go index e26bbba5..2889626b 100644 --- a/pkg/sdkserver/types.go +++ b/pkg/sdkserver/types.go @@ -144,6 +144,7 @@ func (r *runInfo) process(e event) map[string]any { r.Start = e.Time r.Program = *e.Program r.State = Running + r.Input = e.Input case runner.EventTypeRunFinish: r.End = e.Time r.Output = e.Output @@ -167,9 +168,11 @@ func (r *runInfo) process(e event) map[string]any { call.Type = e.Type switch e.Type { - case runner.EventTypeCallStart: + case runner.EventTypeCallStart, runner.EventTypeCallContinue: call.Start = e.Time - call.Input = e.Content + if e.Content != "" { + call.Input = e.Content + } case runner.EventTypeCallSubCalls: call.setSubCalls(e.ToolSubCalls) From c516f7b5a2b956312d3fd2305b2af5cc8e673e14 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Wed, 21 Aug 2024 13:35:35 -0400 Subject: [PATCH 46/83] fix: remove config file location from the config (#813) Signed-off-by: Grant Linville --- pkg/config/cliconfig.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/config/cliconfig.go b/pkg/config/cliconfig.go index 7970415f..43649135 100644 --- a/pkg/config/cliconfig.go +++ b/pkg/config/cliconfig.go @@ -52,14 +52,14 @@ func (a *AuthConfig) UnmarshalJSON(data []byte) error { } type CLIConfig struct { - Auths map[string]AuthConfig `json:"auths,omitempty"` - CredentialsStore string `json:"credsStore,omitempty"` - GPTScriptConfigFile string `json:"gptscriptConfig,omitempty"` - GatewayURL string `json:"gatewayURL,omitempty"` - Integrations map[string]string `json:"integrations,omitempty"` + Auths map[string]AuthConfig `json:"auths,omitempty"` + CredentialsStore string `json:"credsStore,omitempty"` + GatewayURL string `json:"gatewayURL,omitempty"` + Integrations map[string]string `json:"integrations,omitempty"` auths map[string]types.AuthConfig authsLock *sync.Mutex + location string } func (c *CLIConfig) Sanitize() *CLIConfig { @@ -93,7 +93,7 @@ func (c *CLIConfig) Save() error { if err != nil { return err } - return os.WriteFile(c.GPTScriptConfigFile, data, 0655) + return os.WriteFile(c.location, data, 0655) } func (c *CLIConfig) GetAuthConfigs() map[string]types.AuthConfig { @@ -113,7 +113,7 @@ func (c *CLIConfig) GetAuthConfigs() map[string]types.AuthConfig { } func (c *CLIConfig) GetFilename() string { - return c.GPTScriptConfigFile + return c.location } func ReadCLIConfig(gptscriptConfigFile string) (*CLIConfig, error) { @@ -133,8 +133,8 @@ func ReadCLIConfig(gptscriptConfigFile string) (*CLIConfig, error) { return nil, err } result := &CLIConfig{ - authsLock: &sync.Mutex{}, - GPTScriptConfigFile: gptscriptConfigFile, + authsLock: &sync.Mutex{}, + location: gptscriptConfigFile, } if err := json.Unmarshal(data, result); err != nil { return nil, err @@ -158,7 +158,7 @@ func ReadCLIConfig(gptscriptConfigFile string) (*CLIConfig, error) { default: errMsg += " (use 'file')" } - errMsg += fmt.Sprintf("\nPlease edit your config file at %s to fix this.", result.GPTScriptConfigFile) + errMsg += fmt.Sprintf("\nPlease edit your config file at %s to fix this.", result.location) return nil, errors.New(errMsg) } From 83622e6669d52b019a9d5d5af646ccfe4362cc15 Mon Sep 17 00:00:00 2001 From: Bill Maxwell Date: Wed, 21 Aug 2024 09:24:04 -0700 Subject: [PATCH 47/83] chore: add content length when handling json request bodies. Signed-off-by: Bill Maxwell --- pkg/openapi/run.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/openapi/run.go b/pkg/openapi/run.go index 6c7e4ca7..fb3b746c 100644 --- a/pkg/openapi/run.go +++ b/pkg/openapi/run.go @@ -107,6 +107,7 @@ func Run(operationID, defaultHost, args string, t *openapi3.T, envs []string) (s return "", false, fmt.Errorf("failed to encode JSON: %w", err) } req.Header.Set("Content-Type", "application/json") + req.ContentLength = int64(body.Len()) case "text/plain": reqBody := "" From 979908c5e3c5d24200565962d0a0647fb1934cd3 Mon Sep 17 00:00:00 2001 From: Daishan Peng Date: Thu, 22 Aug 2024 18:04:51 -0700 Subject: [PATCH 48/83] Fix: Do not return full env map after downloading releases Signed-off-by: Daishan Peng --- pkg/repos/runtimes/golang/golang.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/repos/runtimes/golang/golang.go b/pkg/repos/runtimes/golang/golang.go index 52e8fe0b..2601f521 100644 --- a/pkg/repos/runtimes/golang/golang.go +++ b/pkg/repos/runtimes/golang/golang.go @@ -244,7 +244,7 @@ func getChecksum(ctx context.Context, rel *release, artifactName string) string return "" } -func (r *Runtime) Binary(ctx context.Context, tool types.Tool, _, toolSource string, env []string) (bool, []string, error) { +func (r *Runtime) Binary(ctx context.Context, tool types.Tool, _, toolSource string, _ []string) (bool, []string, error) { if !tool.Source.IsGit() { return false, nil, nil } @@ -264,7 +264,7 @@ func (r *Runtime) Binary(ctx context.Context, tool types.Tool, _, toolSource str return false, nil, nil } - return true, env, nil + return true, nil, nil } func (r *Runtime) Setup(ctx context.Context, _ types.Tool, dataRoot, toolSource string, env []string) ([]string, error) { From ed94de0067fa2a59d948fa73ed26799a48f3662e Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Tue, 27 Aug 2024 09:27:05 -0700 Subject: [PATCH 49/83] chore: dynamically update tools and other tool params on subsequent chats --- pkg/engine/engine.go | 49 ++++++---- pkg/tests/runner_test.go | 31 ++++++ .../TestToolsChange/call1-resp.golden | 9 ++ .../testdata/TestToolsChange/call1.golden | 70 ++++++++++++++ .../TestToolsChange/call2-resp.golden | 9 ++ .../testdata/TestToolsChange/call2.golden | 73 ++++++++++++++ .../testdata/TestToolsChange/step1.golden | 93 ++++++++++++++++++ .../testdata/TestToolsChange/step2.golden | 96 +++++++++++++++++++ 8 files changed, 410 insertions(+), 20 deletions(-) create mode 100644 pkg/tests/testdata/TestToolsChange/call1-resp.golden create mode 100644 pkg/tests/testdata/TestToolsChange/call1.golden create mode 100644 pkg/tests/testdata/TestToolsChange/call2-resp.golden create mode 100644 pkg/tests/testdata/TestToolsChange/call2.golden create mode 100644 pkg/tests/testdata/TestToolsChange/step1.golden create mode 100644 pkg/tests/testdata/TestToolsChange/step2.golden diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index f8fd8154..14b75e0a 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -258,6 +258,29 @@ func (c *Context) WrappedContext(e *Engine) context.Context { return context.WithValue(c.Ctx, engineContext{}, &cp) } +func populateMessageParams(ctx Context, completion *types.CompletionRequest, tool types.Tool) error { + completion.Model = tool.Parameters.ModelName + completion.MaxTokens = tool.Parameters.MaxTokens + completion.JSONResponse = tool.Parameters.JSONResponse + completion.Cache = tool.Parameters.Cache + completion.Chat = tool.Parameters.Chat + completion.Temperature = tool.Parameters.Temperature + completion.InternalSystemPrompt = tool.Parameters.InternalPrompt + + if tool.Chat && completion.InternalSystemPrompt == nil { + completion.InternalSystemPrompt = new(bool) + } + + var err error + completion.Tools, err = tool.GetCompletionTools(*ctx.Program, ctx.AgentGroup...) + if err != nil { + return err + } + + completion.Messages = addUpdateSystem(ctx, tool, completion.Messages) + return nil +} + func (e *Engine) Start(ctx Context, input string) (ret *Return, _ error) { tool := ctx.Tool @@ -290,28 +313,11 @@ func (e *Engine) Start(ctx Context, input string) (ret *Return, _ error) { return nil, fmt.Errorf("credential tools cannot make calls to the LLM") } - completion := types.CompletionRequest{ - Model: tool.Parameters.ModelName, - MaxTokens: tool.Parameters.MaxTokens, - JSONResponse: tool.Parameters.JSONResponse, - Cache: tool.Parameters.Cache, - Chat: tool.Parameters.Chat, - Temperature: tool.Parameters.Temperature, - InternalSystemPrompt: tool.Parameters.InternalPrompt, - } - - if tool.Chat && completion.InternalSystemPrompt == nil { - completion.InternalSystemPrompt = new(bool) - } - - var err error - completion.Tools, err = tool.GetCompletionTools(*ctx.Program, ctx.AgentGroup...) - if err != nil { + var completion types.CompletionRequest + if err := populateMessageParams(ctx, &completion, tool); err != nil { return nil, err } - completion.Messages = addUpdateSystem(ctx, tool, completion.Messages) - if tool.Chat && input == "{}" { input = "" } @@ -497,6 +503,9 @@ func (e *Engine) Continue(ctx Context, state *State, results ...CallResult) (*Re return nil, fmt.Errorf("invalid continue call, no completion needed") } - state.Completion.Messages = addUpdateSystem(ctx, ctx.Tool, state.Completion.Messages) + if err := populateMessageParams(ctx, &state.Completion, ctx.Tool); err != nil { + return nil, err + } + return e.complete(ctx.Ctx, state) } diff --git a/pkg/tests/runner_test.go b/pkg/tests/runner_test.go index 141e6aff..483b5b6f 100644 --- a/pkg/tests/runner_test.go +++ b/pkg/tests/runner_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/gptscript-ai/gptscript/pkg/engine" + "github.com/gptscript-ai/gptscript/pkg/loader" "github.com/gptscript-ai/gptscript/pkg/tests/tester" "github.com/gptscript-ai/gptscript/pkg/types" "github.com/hexops/autogold/v2" @@ -1041,3 +1042,33 @@ func TestRuntimesLocalDev(t *testing.T) { _ = os.RemoveAll("testdata/TestRuntimesLocalDev/node_modules") _ = os.RemoveAll("testdata/TestRuntimesLocalDev/package-lock.json") } + +func TestToolsChange(t *testing.T) { + r := tester.NewRunner(t) + prg, err := loader.ProgramFromSource(context.Background(), ` +chat: true +tools: sys.ls, sys.read, sys.write +`, "") + require.NoError(t, err) + + resp, err := r.Chat(context.Background(), nil, prg, nil, "input 1") + require.NoError(t, err) + r.AssertResponded(t) + assert.False(t, resp.Done) + autogold.Expect("TEST RESULT CALL: 1").Equal(t, resp.Content) + autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step1")) + + prg, err = loader.ProgramFromSource(context.Background(), ` +chat: true +temperature: 0.6 +tools: sys.ls, sys.write +`, "") + require.NoError(t, err) + + resp, err = r.Chat(context.Background(), resp.State, prg, nil, "input 2") + require.NoError(t, err) + r.AssertResponded(t) + assert.False(t, resp.Done) + autogold.Expect("TEST RESULT CALL: 2").Equal(t, resp.Content) + autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step2")) +} diff --git a/pkg/tests/testdata/TestToolsChange/call1-resp.golden b/pkg/tests/testdata/TestToolsChange/call1-resp.golden new file mode 100644 index 00000000..2861a036 --- /dev/null +++ b/pkg/tests/testdata/TestToolsChange/call1-resp.golden @@ -0,0 +1,9 @@ +`{ + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 1" + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestToolsChange/call1.golden b/pkg/tests/testdata/TestToolsChange/call1.golden new file mode 100644 index 00000000..6c7c2d55 --- /dev/null +++ b/pkg/tests/testdata/TestToolsChange/call1.golden @@ -0,0 +1,70 @@ +`{ + "model": "gpt-4o", + "internalSystemPrompt": false, + "tools": [ + { + "function": { + "toolID": "sys.ls", + "name": "ls", + "description": "Lists the contents of a directory", + "parameters": { + "properties": { + "dir": { + "description": "The directory to list", + "type": "string" + } + }, + "type": "object" + } + } + }, + { + "function": { + "toolID": "sys.read", + "name": "read", + "description": "Reads the contents of a file", + "parameters": { + "properties": { + "filename": { + "description": "The name of the file to read", + "type": "string" + } + }, + "type": "object" + } + } + }, + { + "function": { + "toolID": "sys.write", + "name": "write", + "description": "Write the contents to a file", + "parameters": { + "properties": { + "content": { + "description": "The content to write", + "type": "string" + }, + "filename": { + "description": "The name of the file to write to", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "messages": [ + { + "role": "user", + "content": [ + { + "text": "input 1" + } + ], + "usage": {} + } + ], + "chat": true +}` diff --git a/pkg/tests/testdata/TestToolsChange/call2-resp.golden b/pkg/tests/testdata/TestToolsChange/call2-resp.golden new file mode 100644 index 00000000..997ca1b9 --- /dev/null +++ b/pkg/tests/testdata/TestToolsChange/call2-resp.golden @@ -0,0 +1,9 @@ +`{ + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 2" + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestToolsChange/call2.golden b/pkg/tests/testdata/TestToolsChange/call2.golden new file mode 100644 index 00000000..ad86b7ce --- /dev/null +++ b/pkg/tests/testdata/TestToolsChange/call2.golden @@ -0,0 +1,73 @@ +`{ + "model": "gpt-4o", + "internalSystemPrompt": false, + "tools": [ + { + "function": { + "toolID": "sys.ls", + "name": "ls", + "description": "Lists the contents of a directory", + "parameters": { + "properties": { + "dir": { + "description": "The directory to list", + "type": "string" + } + }, + "type": "object" + } + } + }, + { + "function": { + "toolID": "sys.write", + "name": "write", + "description": "Write the contents to a file", + "parameters": { + "properties": { + "content": { + "description": "The content to write", + "type": "string" + }, + "filename": { + "description": "The name of the file to write to", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "messages": [ + { + "role": "user", + "content": [ + { + "text": "input 1" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 1" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "input 2" + } + ], + "usage": {} + } + ], + "chat": true, + "temperature": 0.6 +}` diff --git a/pkg/tests/testdata/TestToolsChange/step1.golden b/pkg/tests/testdata/TestToolsChange/step1.golden new file mode 100644 index 00000000..1aae05d1 --- /dev/null +++ b/pkg/tests/testdata/TestToolsChange/step1.golden @@ -0,0 +1,93 @@ +`{ + "done": false, + "content": "TEST RESULT CALL: 1", + "toolID": "inline:", + "state": { + "continuation": { + "state": { + "input": "input 1", + "completion": { + "model": "gpt-4o", + "internalSystemPrompt": false, + "tools": [ + { + "function": { + "toolID": "sys.ls", + "name": "ls", + "description": "Lists the contents of a directory", + "parameters": { + "properties": { + "dir": { + "description": "The directory to list", + "type": "string" + } + }, + "type": "object" + } + } + }, + { + "function": { + "toolID": "sys.read", + "name": "read", + "description": "Reads the contents of a file", + "parameters": { + "properties": { + "filename": { + "description": "The name of the file to read", + "type": "string" + } + }, + "type": "object" + } + } + }, + { + "function": { + "toolID": "sys.write", + "name": "write", + "description": "Write the contents to a file", + "parameters": { + "properties": { + "content": { + "description": "The content to write", + "type": "string" + }, + "filename": { + "description": "The name of the file to write to", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "messages": [ + { + "role": "user", + "content": [ + { + "text": "input 1" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 1" + } + ], + "usage": {} + } + ], + "chat": true + } + }, + "result": "TEST RESULT CALL: 1" + }, + "continuationToolID": "inline:" + } +}` diff --git a/pkg/tests/testdata/TestToolsChange/step2.golden b/pkg/tests/testdata/TestToolsChange/step2.golden new file mode 100644 index 00000000..9c9dbad7 --- /dev/null +++ b/pkg/tests/testdata/TestToolsChange/step2.golden @@ -0,0 +1,96 @@ +`{ + "done": false, + "content": "TEST RESULT CALL: 2", + "toolID": "inline:", + "state": { + "continuation": { + "state": { + "input": "input 1", + "completion": { + "model": "gpt-4o", + "internalSystemPrompt": false, + "tools": [ + { + "function": { + "toolID": "sys.ls", + "name": "ls", + "description": "Lists the contents of a directory", + "parameters": { + "properties": { + "dir": { + "description": "The directory to list", + "type": "string" + } + }, + "type": "object" + } + } + }, + { + "function": { + "toolID": "sys.write", + "name": "write", + "description": "Write the contents to a file", + "parameters": { + "properties": { + "content": { + "description": "The content to write", + "type": "string" + }, + "filename": { + "description": "The name of the file to write to", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "messages": [ + { + "role": "user", + "content": [ + { + "text": "input 1" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 1" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "input 2" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 2" + } + ], + "usage": {} + } + ], + "chat": true, + "temperature": 0.6 + } + }, + "result": "TEST RESULT CALL: 2" + }, + "continuationToolID": "inline:" + } +}` From 04dd074694d7928ecbb124635e2ea5ccad77f0a6 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Tue, 27 Aug 2024 09:52:59 -0700 Subject: [PATCH 50/83] chore: add with * syntax to context tools Basic example chat: true context: foo with * Say hi --- name: foo #!/bin/bash echo This is the input: ${GPTSCRIPT_INPUT} --- pkg/runner/runner.go | 14 +++- pkg/tests/runner2_test.go | 34 ++++++++++ .../TestContextWithAsterick/call1-resp.golden | 9 +++ .../TestContextWithAsterick/call1.golden | 25 +++++++ .../TestContextWithAsterick/call2-resp.golden | 9 +++ .../TestContextWithAsterick/call2.golden | 43 ++++++++++++ .../TestContextWithAsterick/step1.golden | 48 ++++++++++++++ .../TestContextWithAsterick/step2.golden | 66 +++++++++++++++++++ pkg/tests/tester/runner.go | 18 ++++- 9 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 pkg/tests/runner2_test.go create mode 100644 pkg/tests/testdata/TestContextWithAsterick/call1-resp.golden create mode 100644 pkg/tests/testdata/TestContextWithAsterick/call1.golden create mode 100644 pkg/tests/testdata/TestContextWithAsterick/call2-resp.golden create mode 100644 pkg/tests/testdata/TestContextWithAsterick/call2.golden create mode 100644 pkg/tests/testdata/TestContextWithAsterick/step1.golden create mode 100644 pkg/tests/testdata/TestContextWithAsterick/step2.golden diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 93e40670..30737a4c 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -260,6 +260,10 @@ func getToolRefInput(prg *types.Program, ref types.ToolReference, input string) targetArgs := prg.ToolSet[ref.ToolID].Arguments targetKeys := map[string]string{} + if ref.Arg == "*" { + return input, nil + } + if targetArgs == nil { return "", nil } @@ -647,13 +651,17 @@ func (r *Runner) resume(callCtx engine.Context, monitor Monitor, env []string, s Env: env, } - var contentInput string + var contextInput string if state.Continuation != nil && state.Continuation.State != nil { - contentInput = state.Continuation.State.Input + contextInput = state.Continuation.State.Input + } + + if state.ResumeInput != nil { + contextInput = *state.ResumeInput } - callCtx.InputContext, state, err = r.getContext(callCtx, state, monitor, env, contentInput) + callCtx.InputContext, state, err = r.getContext(callCtx, state, monitor, env, contextInput) if err != nil || state.InputContextContinuation != nil { return state, err } diff --git a/pkg/tests/runner2_test.go b/pkg/tests/runner2_test.go new file mode 100644 index 00000000..12ac4fa0 --- /dev/null +++ b/pkg/tests/runner2_test.go @@ -0,0 +1,34 @@ +package tests + +import ( + "context" + "testing" + + "github.com/gptscript-ai/gptscript/pkg/loader" + "github.com/gptscript-ai/gptscript/pkg/tests/tester" + "github.com/stretchr/testify/require" +) + +func TestContextWithAsterick(t *testing.T) { + r := tester.NewRunner(t) + prg, err := loader.ProgramFromSource(context.Background(), ` +chat: true +context: foo with * + +Say hi + +--- +name: foo + +#!/bin/bash + +echo This is the input: ${GPTSCRIPT_INPUT} +`, "") + require.NoError(t, err) + + resp, err := r.Chat(context.Background(), nil, prg, nil, "input 1") + r.AssertStep(t, resp, err) + + resp, err = r.Chat(context.Background(), resp.State, prg, nil, "input 2") + r.AssertStep(t, resp, err) +} diff --git a/pkg/tests/testdata/TestContextWithAsterick/call1-resp.golden b/pkg/tests/testdata/TestContextWithAsterick/call1-resp.golden new file mode 100644 index 00000000..2861a036 --- /dev/null +++ b/pkg/tests/testdata/TestContextWithAsterick/call1-resp.golden @@ -0,0 +1,9 @@ +`{ + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 1" + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestContextWithAsterick/call1.golden b/pkg/tests/testdata/TestContextWithAsterick/call1.golden new file mode 100644 index 00000000..6d9538ce --- /dev/null +++ b/pkg/tests/testdata/TestContextWithAsterick/call1.golden @@ -0,0 +1,25 @@ +`{ + "model": "gpt-4o", + "internalSystemPrompt": false, + "messages": [ + { + "role": "system", + "content": [ + { + "text": "This is the input: input 1\n\nSay hi" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "input 1" + } + ], + "usage": {} + } + ], + "chat": true +}` diff --git a/pkg/tests/testdata/TestContextWithAsterick/call2-resp.golden b/pkg/tests/testdata/TestContextWithAsterick/call2-resp.golden new file mode 100644 index 00000000..997ca1b9 --- /dev/null +++ b/pkg/tests/testdata/TestContextWithAsterick/call2-resp.golden @@ -0,0 +1,9 @@ +`{ + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 2" + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestContextWithAsterick/call2.golden b/pkg/tests/testdata/TestContextWithAsterick/call2.golden new file mode 100644 index 00000000..f159014c --- /dev/null +++ b/pkg/tests/testdata/TestContextWithAsterick/call2.golden @@ -0,0 +1,43 @@ +`{ + "model": "gpt-4o", + "internalSystemPrompt": false, + "messages": [ + { + "role": "system", + "content": [ + { + "text": "This is the input: input 2\n\nSay hi" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "input 1" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 1" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "input 2" + } + ], + "usage": {} + } + ], + "chat": true +}` diff --git a/pkg/tests/testdata/TestContextWithAsterick/step1.golden b/pkg/tests/testdata/TestContextWithAsterick/step1.golden new file mode 100644 index 00000000..cc42e6a6 --- /dev/null +++ b/pkg/tests/testdata/TestContextWithAsterick/step1.golden @@ -0,0 +1,48 @@ +`{ + "done": false, + "content": "TEST RESULT CALL: 1", + "toolID": "inline:", + "state": { + "continuation": { + "state": { + "input": "input 1", + "completion": { + "model": "gpt-4o", + "internalSystemPrompt": false, + "messages": [ + { + "role": "system", + "content": [ + { + "text": "This is the input: input 1\n\nSay hi" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "input 1" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 1" + } + ], + "usage": {} + } + ], + "chat": true + } + }, + "result": "TEST RESULT CALL: 1" + }, + "continuationToolID": "inline:" + } +}` diff --git a/pkg/tests/testdata/TestContextWithAsterick/step2.golden b/pkg/tests/testdata/TestContextWithAsterick/step2.golden new file mode 100644 index 00000000..02bc92fe --- /dev/null +++ b/pkg/tests/testdata/TestContextWithAsterick/step2.golden @@ -0,0 +1,66 @@ +`{ + "done": false, + "content": "TEST RESULT CALL: 2", + "toolID": "inline:", + "state": { + "continuation": { + "state": { + "input": "input 1", + "completion": { + "model": "gpt-4o", + "internalSystemPrompt": false, + "messages": [ + { + "role": "system", + "content": [ + { + "text": "This is the input: input 2\n\nSay hi" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "input 1" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 1" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "input 2" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 2" + } + ], + "usage": {} + } + ], + "chat": true + } + }, + "result": "TEST RESULT CALL: 2" + }, + "continuationToolID": "inline:" + } +}` diff --git a/pkg/tests/tester/runner.go b/pkg/tests/tester/runner.go index 66337ff5..b460ce18 100644 --- a/pkg/tests/tester/runner.go +++ b/pkg/tests/tester/runner.go @@ -135,7 +135,8 @@ func (c *Client) Call(_ context.Context, messageRequest types.CompletionRequest, type Runner struct { *runner.Runner - Client *Client + Client *Client + StepAsserted int } func (r *Runner) RunDefault() string { @@ -166,6 +167,21 @@ func (r *Runner) AssertResponded(t *testing.T) { require.Len(t, r.Client.result, 0) } +func toJSONString(t *testing.T, v interface{}) string { + t.Helper() + x, err := json.MarshalIndent(v, "", " ") + require.NoError(t, err) + return string(x) +} + +func (r *Runner) AssertStep(t *testing.T, resp runner.ChatResponse, err error) { + t.Helper() + r.StepAsserted++ + require.NoError(t, err) + r.AssertResponded(t) + autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+fmt.Sprintf("/step%d", r.StepAsserted))) +} + func (r *Runner) RespondWith(result ...Result) { r.Client.result = append(r.Client.result, result...) } From 1c75954594d59abe8ae371bc7c96ce953eec4101 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Tue, 27 Aug 2024 10:30:30 -0700 Subject: [PATCH 51/83] chore: drop input continuations feature --- pkg/runner/runner.go | 97 ++------- pkg/tests/runner_test.go | 78 +------- .../TestContextSubChat/call10-resp.golden | 9 - .../testdata/TestContextSubChat/call10.golden | 43 ---- .../TestContextSubChat/call3-resp.golden | 16 -- .../testdata/TestContextSubChat/call3.golden | 61 ------ .../TestContextSubChat/call4-resp.golden | 9 - .../testdata/TestContextSubChat/call4.golden | 64 ------ .../TestContextSubChat/call5-resp.golden | 9 - .../testdata/TestContextSubChat/call5.golden | 25 --- .../TestContextSubChat/call6-resp.golden | 16 -- .../testdata/TestContextSubChat/call6.golden | 31 --- .../TestContextSubChat/call7-resp.golden | 9 - .../testdata/TestContextSubChat/call7.golden | 43 ---- .../TestContextSubChat/call8-resp.golden | 16 -- .../testdata/TestContextSubChat/call8.golden | 61 ------ .../TestContextSubChat/call9-resp.golden | 9 - .../testdata/TestContextSubChat/call9.golden | 64 ------ .../testdata/TestContextSubChat/step1.golden | 146 -------------- .../testdata/TestContextSubChat/step2.golden | 48 ----- .../testdata/TestContextSubChat/step3.golden | 188 ------------------ .../testdata/TestContextSubChat/step4.golden | 66 ------ 22 files changed, 20 insertions(+), 1088 deletions(-) delete mode 100644 pkg/tests/testdata/TestContextSubChat/call10-resp.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call10.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call3-resp.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call3.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call4-resp.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call4.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call5-resp.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call5.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call6-resp.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call6.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call7-resp.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call7.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call8-resp.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call8.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call9-resp.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/call9.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/step1.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/step2.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/step3.golden delete mode 100644 pkg/tests/testdata/TestContextSubChat/step4.golden diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 30737a4c..3035a1d1 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -172,11 +172,7 @@ func (r *Runner) Chat(ctx context.Context, prevState ChatState, prg types.Progra return resp, err } - if state == nil || state.StartContinuation { - if state != nil { - state = state.WithResumeInput(&input) - input = state.InputContextContinuationInput - } + if state == nil { state, err = r.start(callCtx, state, monitor, env, input) if err != nil { return resp, err @@ -186,11 +182,9 @@ func (r *Runner) Chat(ctx context.Context, prevState ChatState, prg types.Progra state.ResumeInput = &input } - if !state.StartContinuation { - state, err = r.resume(callCtx, monitor, env, state) - if err != nil { - return resp, err - } + state, err = r.resume(callCtx, monitor, env, state) + if err != nil { + return resp, err } if state.Result != nil { @@ -335,24 +329,10 @@ func getToolRefInput(prg *types.Program, ref types.ToolReference, input string) return string(output), err } -func (r *Runner) getContext(callCtx engine.Context, state *State, monitor Monitor, env []string, input string) (result []engine.InputContext, _ *State, _ error) { +func (r *Runner) getContext(callCtx engine.Context, state *State, monitor Monitor, env []string, input string) (result []engine.InputContext, _ error) { toolRefs, err := callCtx.Tool.GetContextTools(*callCtx.Program) if err != nil { - return nil, nil, err - } - - var newState *State - if state != nil { - cp := *state - newState = &cp - if newState.InputContextContinuation != nil { - newState.InputContexts = nil - newState.InputContextContinuation = nil - newState.InputContextContinuationInput = "" - newState.ResumeInput = state.InputContextContinuationResumeInput - - input = state.InputContextContinuationInput - } + return nil, err } for i, toolRef := range toolRefs { @@ -363,29 +343,16 @@ func (r *Runner) getContext(callCtx engine.Context, state *State, monitor Monito contextInput, err := getToolRefInput(callCtx.Program, toolRef, input) if err != nil { - return nil, nil, err + return nil, err } var content *State - if state != nil && state.InputContextContinuation != nil { - content, err = r.subCallResume(callCtx.Ctx, callCtx, monitor, env, toolRef.ToolID, "", state.InputContextContinuation.WithResumeInput(state.ResumeInput), engine.ContextToolCategory) - } else { - content, err = r.subCall(callCtx.Ctx, callCtx, monitor, env, toolRef.ToolID, contextInput, "", engine.ContextToolCategory) - } + content, err = r.subCall(callCtx.Ctx, callCtx, monitor, env, toolRef.ToolID, contextInput, "", engine.ContextToolCategory) if err != nil { - return nil, nil, err + return nil, err } if content.Continuation != nil { - if newState == nil { - newState = &State{} - } - newState.InputContexts = result - newState.InputContextContinuation = content - newState.InputContextContinuationInput = input - if state != nil { - newState.InputContextContinuationResumeInput = state.ResumeInput - } - return nil, newState, nil + return nil, fmt.Errorf("invalid state: context tool [%s] can not result in a continuation", toolRef.ToolID) } result = append(result, engine.InputContext{ ToolID: toolRef.ToolID, @@ -393,7 +360,7 @@ func (r *Runner) getContext(callCtx engine.Context, state *State, monitor Monito }) } - return result, newState, nil + return result, nil } func (r *Runner) call(callCtx engine.Context, monitor Monitor, env []string, input string) (*State, error) { @@ -401,9 +368,6 @@ func (r *Runner) call(callCtx engine.Context, monitor Monitor, env []string, inp if err != nil { return nil, err } - if result.StartContinuation { - return result, nil - } return r.resume(callCtx, monitor, env, result) } @@ -435,15 +399,10 @@ func (r *Runner) start(callCtx engine.Context, state *State, monitor Monitor, en } } - var newState *State - callCtx.InputContext, newState, err = r.getContext(callCtx, state, monitor, env, input) + callCtx.InputContext, err = r.getContext(callCtx, state, monitor, env, input) if err != nil { return nil, err } - if newState != nil && newState.InputContextContinuation != nil { - newState.StartContinuation = true - return newState, nil - } e := engine.Engine{ Model: r.c, @@ -493,11 +452,7 @@ type State struct { SubCalls []SubCallResult `json:"subCalls,omitempty"` SubCallID string `json:"subCallID,omitempty"` - InputContexts []engine.InputContext `json:"inputContexts,omitempty"` - InputContextContinuation *State `json:"inputContextContinuation,omitempty"` - InputContextContinuationInput string `json:"inputContextContinuationInput,omitempty"` - InputContextContinuationResumeInput *string `json:"inputContextContinuationResumeInput,omitempty"` - StartContinuation bool `json:"startContinuation,omitempty"` + InputContexts []engine.InputContext `json:"inputContexts,omitempty"` } func (s State) WithResumeInput(input *string) *State { @@ -510,10 +465,6 @@ func (s State) ContinuationContentToolID() (string, error) { return s.ContinuationToolID, nil } - if s.InputContextContinuation != nil { - return s.InputContextContinuation.ContinuationContentToolID() - } - for _, subCall := range s.SubCalls { if s.SubCallID == subCall.CallID { return subCall.State.ContinuationContentToolID() @@ -527,10 +478,6 @@ func (s State) ContinuationContent() (string, error) { return *s.Continuation.Result, nil } - if s.InputContextContinuation != nil { - return s.InputContextContinuation.ContinuationContent() - } - for _, subCall := range s.SubCalls { if s.SubCallID == subCall.CallID { return subCall.State.ContinuationContent() @@ -549,10 +496,6 @@ func (r *Runner) resume(callCtx engine.Context, monitor Monitor, env []string, s retState, retErr = r.handleOutput(callCtx, monitor, env, retState, retErr) }() - if state.StartContinuation { - return nil, fmt.Errorf("invalid state, resume should not have StartContinuation set to true") - } - if state.Continuation == nil { return nil, errors.New("invalid state, resume should have Continuation data") } @@ -651,18 +594,18 @@ func (r *Runner) resume(callCtx engine.Context, monitor Monitor, env []string, s Env: env, } - var contextInput string + var contentInput string if state.Continuation != nil && state.Continuation.State != nil { - contextInput = state.Continuation.State.Input + contentInput = state.Continuation.State.Input } if state.ResumeInput != nil { - contextInput = *state.ResumeInput + contentInput = *state.ResumeInput } - callCtx.InputContext, state, err = r.getContext(callCtx, state, monitor, env, contextInput) - if err != nil || state.InputContextContinuation != nil { + callCtx.InputContext, err = r.getContext(callCtx, state, monitor, env, contentInput) + if err != nil { return state, err } @@ -772,10 +715,6 @@ func (r *Runner) subCalls(callCtx engine.Context, monitor Monitor, env []string, callCtx.LastReturn = state.Continuation } - if state.InputContextContinuation != nil { - return state, nil, nil - } - if state.SubCallID != "" { if state.ResumeInput == nil { return nil, nil, fmt.Errorf("invalid state, input must be set for sub call continuation on callID [%s]", state.SubCallID) diff --git a/pkg/tests/runner_test.go b/pkg/tests/runner_test.go index 141e6aff..f21f936e 100644 --- a/pkg/tests/runner_test.go +++ b/pkg/tests/runner_test.go @@ -212,82 +212,8 @@ func TestContextSubChat(t *testing.T) { prg, err := r.Load("") require.NoError(t, err) - resp, err := r.Chat(context.Background(), nil, prg, os.Environ(), "User 1") - require.NoError(t, err) - r.AssertResponded(t) - assert.False(t, resp.Done) - autogold.Expect("Assistant Response 1 - from chatbot1").Equal(t, resp.Content) - autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step1")) - - r.RespondWith(tester.Result{ - Content: []types.ContentPart{ - { - ToolCall: &types.CompletionToolCall{ - ID: "call_2", - Function: types.CompletionFunctionCall{ - Name: types.ToolNormalizer("sys.chat.finish"), - Arguments: "Response from context chatbot", - }, - }, - }, - }, - }, tester.Result{ - Text: "Assistant Response 2 - from context tool", - }, tester.Result{ - Text: "Assistant Response 3 - from main chat tool", - }) - resp, err = r.Chat(context.Background(), resp.State, prg, os.Environ(), "User 2") - require.NoError(t, err) - r.AssertResponded(t) - assert.False(t, resp.Done) - autogold.Expect("Assistant Response 3 - from main chat tool").Equal(t, resp.Content) - autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step2")) - - r.RespondWith(tester.Result{ - Content: []types.ContentPart{ - { - ToolCall: &types.CompletionToolCall{ - ID: "call_3", - Function: types.CompletionFunctionCall{ - Name: "chatbot", - Arguments: "Input to chatbot1 on resume", - }, - }, - }, - }, - }, tester.Result{ - Text: "Assistant Response 4 - from chatbot1", - }) - resp, err = r.Chat(context.Background(), resp.State, prg, os.Environ(), "User 3") - require.NoError(t, err) - r.AssertResponded(t) - assert.False(t, resp.Done) - autogold.Expect("Assistant Response 3 - from main chat tool").Equal(t, resp.Content) - autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step3")) - - r.RespondWith(tester.Result{ - Content: []types.ContentPart{ - { - ToolCall: &types.CompletionToolCall{ - ID: "call_4", - Function: types.CompletionFunctionCall{ - Name: types.ToolNormalizer("sys.chat.finish"), - Arguments: "Response from context chatbot after resume", - }, - }, - }, - }, - }, tester.Result{ - Text: "Assistant Response 5 - from context tool resume", - }, tester.Result{ - Text: "Assistant Response 6 - from main chat tool resume", - }) - resp, err = r.Chat(context.Background(), resp.State, prg, os.Environ(), "User 4") - require.NoError(t, err) - r.AssertResponded(t) - assert.False(t, resp.Done) - autogold.Expect("Assistant Response 6 - from main chat tool resume").Equal(t, resp.Content) - autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step4")) + _, err = r.Chat(context.Background(), nil, prg, os.Environ(), "User 1") + autogold.Expect("invalid state: context tool [testdata/TestContextSubChat/test.gpt:subtool] can not result in a continuation").Equal(t, err.Error()) } func TestSubChat(t *testing.T) { diff --git a/pkg/tests/testdata/TestContextSubChat/call10-resp.golden b/pkg/tests/testdata/TestContextSubChat/call10-resp.golden deleted file mode 100644 index 144ca8d9..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call10-resp.golden +++ /dev/null @@ -1,9 +0,0 @@ -`{ - "role": "assistant", - "content": [ - { - "text": "Assistant Response 6 - from main chat tool resume" - } - ], - "usage": {} -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call10.golden b/pkg/tests/testdata/TestContextSubChat/call10.golden deleted file mode 100644 index c8c98651..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call10.golden +++ /dev/null @@ -1,43 +0,0 @@ -`{ - "model": "gpt-4o", - "internalSystemPrompt": false, - "messages": [ - { - "role": "system", - "content": [ - { - "text": "Assistant Response 5 - from context tool resume\nHello" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "User 1" - } - ], - "usage": {} - }, - { - "role": "assistant", - "content": [ - { - "text": "Assistant Response 3 - from main chat tool" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "User 3" - } - ], - "usage": {} - } - ], - "chat": true -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call3-resp.golden b/pkg/tests/testdata/TestContextSubChat/call3-resp.golden deleted file mode 100644 index b116d066..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call3-resp.golden +++ /dev/null @@ -1,16 +0,0 @@ -`{ - "role": "assistant", - "content": [ - { - "toolCall": { - "index": 0, - "id": "call_2", - "function": { - "name": "chatFinish", - "arguments": "Response from context chatbot" - } - } - } - ], - "usage": {} -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call3.golden b/pkg/tests/testdata/TestContextSubChat/call3.golden deleted file mode 100644 index 55ad402f..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call3.golden +++ /dev/null @@ -1,61 +0,0 @@ -`{ - "model": "gpt-4o", - "internalSystemPrompt": false, - "tools": [ - { - "function": { - "toolID": "sys.chat.finish", - "name": "chatFinish", - "description": "Concludes the conversation. This can not be used to ask a question.", - "parameters": { - "properties": { - "return": { - "description": "The instructed value to return or a summary of the dialog if no value is instructed", - "type": "string" - } - }, - "type": "object" - } - } - } - ], - "messages": [ - { - "role": "system", - "content": [ - { - "text": "This is a chatbot" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "Input to chatbot1" - } - ], - "usage": {} - }, - { - "role": "assistant", - "content": [ - { - "text": "Assistant Response 1 - from chatbot1" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "User 1" - } - ], - "usage": {} - } - ], - "chat": true -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call4-resp.golden b/pkg/tests/testdata/TestContextSubChat/call4-resp.golden deleted file mode 100644 index a86ae187..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call4-resp.golden +++ /dev/null @@ -1,9 +0,0 @@ -`{ - "role": "assistant", - "content": [ - { - "text": "Assistant Response 2 - from context tool" - } - ], - "usage": {} -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call4.golden b/pkg/tests/testdata/TestContextSubChat/call4.golden deleted file mode 100644 index e1fb91ea..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call4.golden +++ /dev/null @@ -1,64 +0,0 @@ -`{ - "model": "gpt-4o", - "tools": [ - { - "function": { - "toolID": "testdata/TestContextSubChat/test.gpt:chatbot", - "name": "chatbot", - "parameters": { - "properties": { - "defaultPromptParameter": { - "description": "Prompt to send to the assistant. This may be an instruction or question.", - "type": "string" - } - }, - "type": "object" - } - } - } - ], - "messages": [ - { - "role": "system", - "content": [ - { - "text": "Call chatbot" - } - ], - "usage": {} - }, - { - "role": "assistant", - "content": [ - { - "toolCall": { - "index": 0, - "id": "call_1", - "function": { - "name": "chatbot", - "arguments": "Input to chatbot1" - } - } - } - ], - "usage": {} - }, - { - "role": "tool", - "content": [ - { - "text": "Response from context chatbot" - } - ], - "toolCall": { - "index": 0, - "id": "call_1", - "function": { - "name": "chatbot", - "arguments": "Input to chatbot1" - } - }, - "usage": {} - } - ] -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call5-resp.golden b/pkg/tests/testdata/TestContextSubChat/call5-resp.golden deleted file mode 100644 index e49a8481..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call5-resp.golden +++ /dev/null @@ -1,9 +0,0 @@ -`{ - "role": "assistant", - "content": [ - { - "text": "Assistant Response 3 - from main chat tool" - } - ], - "usage": {} -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call5.golden b/pkg/tests/testdata/TestContextSubChat/call5.golden deleted file mode 100644 index 2b8cf41e..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call5.golden +++ /dev/null @@ -1,25 +0,0 @@ -`{ - "model": "gpt-4o", - "internalSystemPrompt": false, - "messages": [ - { - "role": "system", - "content": [ - { - "text": "Assistant Response 2 - from context tool\nHello" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "User 1" - } - ], - "usage": {} - } - ], - "chat": true -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call6-resp.golden b/pkg/tests/testdata/TestContextSubChat/call6-resp.golden deleted file mode 100644 index 6807fce9..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call6-resp.golden +++ /dev/null @@ -1,16 +0,0 @@ -`{ - "role": "assistant", - "content": [ - { - "toolCall": { - "index": 0, - "id": "call_3", - "function": { - "name": "chatbot", - "arguments": "Input to chatbot1 on resume" - } - } - } - ], - "usage": {} -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call6.golden b/pkg/tests/testdata/TestContextSubChat/call6.golden deleted file mode 100644 index 225401db..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call6.golden +++ /dev/null @@ -1,31 +0,0 @@ -`{ - "model": "gpt-4o", - "tools": [ - { - "function": { - "toolID": "testdata/TestContextSubChat/test.gpt:chatbot", - "name": "chatbot", - "parameters": { - "properties": { - "defaultPromptParameter": { - "description": "Prompt to send to the assistant. This may be an instruction or question.", - "type": "string" - } - }, - "type": "object" - } - } - } - ], - "messages": [ - { - "role": "system", - "content": [ - { - "text": "Call chatbot" - } - ], - "usage": {} - } - ] -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call7-resp.golden b/pkg/tests/testdata/TestContextSubChat/call7-resp.golden deleted file mode 100644 index 3e0c5f3c..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call7-resp.golden +++ /dev/null @@ -1,9 +0,0 @@ -`{ - "role": "assistant", - "content": [ - { - "text": "Assistant Response 4 - from chatbot1" - } - ], - "usage": {} -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call7.golden b/pkg/tests/testdata/TestContextSubChat/call7.golden deleted file mode 100644 index b0ef4e39..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call7.golden +++ /dev/null @@ -1,43 +0,0 @@ -`{ - "model": "gpt-4o", - "internalSystemPrompt": false, - "tools": [ - { - "function": { - "toolID": "sys.chat.finish", - "name": "chatFinish", - "description": "Concludes the conversation. This can not be used to ask a question.", - "parameters": { - "properties": { - "return": { - "description": "The instructed value to return or a summary of the dialog if no value is instructed", - "type": "string" - } - }, - "type": "object" - } - } - } - ], - "messages": [ - { - "role": "system", - "content": [ - { - "text": "This is a chatbot" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "Input to chatbot1 on resume" - } - ], - "usage": {} - } - ], - "chat": true -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call8-resp.golden b/pkg/tests/testdata/TestContextSubChat/call8-resp.golden deleted file mode 100644 index 2e608b31..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call8-resp.golden +++ /dev/null @@ -1,16 +0,0 @@ -`{ - "role": "assistant", - "content": [ - { - "toolCall": { - "index": 0, - "id": "call_4", - "function": { - "name": "chatFinish", - "arguments": "Response from context chatbot after resume" - } - } - } - ], - "usage": {} -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call8.golden b/pkg/tests/testdata/TestContextSubChat/call8.golden deleted file mode 100644 index 3d0db61b..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call8.golden +++ /dev/null @@ -1,61 +0,0 @@ -`{ - "model": "gpt-4o", - "internalSystemPrompt": false, - "tools": [ - { - "function": { - "toolID": "sys.chat.finish", - "name": "chatFinish", - "description": "Concludes the conversation. This can not be used to ask a question.", - "parameters": { - "properties": { - "return": { - "description": "The instructed value to return or a summary of the dialog if no value is instructed", - "type": "string" - } - }, - "type": "object" - } - } - } - ], - "messages": [ - { - "role": "system", - "content": [ - { - "text": "This is a chatbot" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "Input to chatbot1 on resume" - } - ], - "usage": {} - }, - { - "role": "assistant", - "content": [ - { - "text": "Assistant Response 4 - from chatbot1" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "User 4" - } - ], - "usage": {} - } - ], - "chat": true -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call9-resp.golden b/pkg/tests/testdata/TestContextSubChat/call9-resp.golden deleted file mode 100644 index 4424246d..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call9-resp.golden +++ /dev/null @@ -1,9 +0,0 @@ -`{ - "role": "assistant", - "content": [ - { - "text": "Assistant Response 5 - from context tool resume" - } - ], - "usage": {} -}` diff --git a/pkg/tests/testdata/TestContextSubChat/call9.golden b/pkg/tests/testdata/TestContextSubChat/call9.golden deleted file mode 100644 index 33768f26..00000000 --- a/pkg/tests/testdata/TestContextSubChat/call9.golden +++ /dev/null @@ -1,64 +0,0 @@ -`{ - "model": "gpt-4o", - "tools": [ - { - "function": { - "toolID": "testdata/TestContextSubChat/test.gpt:chatbot", - "name": "chatbot", - "parameters": { - "properties": { - "defaultPromptParameter": { - "description": "Prompt to send to the assistant. This may be an instruction or question.", - "type": "string" - } - }, - "type": "object" - } - } - } - ], - "messages": [ - { - "role": "system", - "content": [ - { - "text": "Call chatbot" - } - ], - "usage": {} - }, - { - "role": "assistant", - "content": [ - { - "toolCall": { - "index": 0, - "id": "call_3", - "function": { - "name": "chatbot", - "arguments": "Input to chatbot1 on resume" - } - } - } - ], - "usage": {} - }, - { - "role": "tool", - "content": [ - { - "text": "Response from context chatbot after resume" - } - ], - "toolCall": { - "index": 0, - "id": "call_3", - "function": { - "name": "chatbot", - "arguments": "Input to chatbot1 on resume" - } - }, - "usage": {} - } - ] -}` diff --git a/pkg/tests/testdata/TestContextSubChat/step1.golden b/pkg/tests/testdata/TestContextSubChat/step1.golden deleted file mode 100644 index 2ffb138e..00000000 --- a/pkg/tests/testdata/TestContextSubChat/step1.golden +++ /dev/null @@ -1,146 +0,0 @@ -`{ - "done": false, - "content": "Assistant Response 1 - from chatbot1", - "toolID": "testdata/TestContextSubChat/test.gpt:chatbot", - "state": { - "inputContextContinuation": { - "continuation": { - "state": { - "completion": { - "model": "gpt-4o", - "tools": [ - { - "function": { - "toolID": "testdata/TestContextSubChat/test.gpt:chatbot", - "name": "chatbot", - "parameters": { - "properties": { - "defaultPromptParameter": { - "description": "Prompt to send to the assistant. This may be an instruction or question.", - "type": "string" - } - }, - "type": "object" - } - } - } - ], - "messages": [ - { - "role": "system", - "content": [ - { - "text": "Call chatbot" - } - ], - "usage": {} - }, - { - "role": "assistant", - "content": [ - { - "toolCall": { - "index": 0, - "id": "call_1", - "function": { - "name": "chatbot", - "arguments": "Input to chatbot1" - } - } - } - ], - "usage": {} - } - ] - }, - "pending": { - "call_1": { - "index": 0, - "id": "call_1", - "function": { - "name": "chatbot", - "arguments": "Input to chatbot1" - } - } - } - }, - "calls": { - "call_1": { - "toolID": "testdata/TestContextSubChat/test.gpt:chatbot", - "input": "Input to chatbot1" - } - } - }, - "subCalls": [ - { - "toolId": "testdata/TestContextSubChat/test.gpt:chatbot", - "callId": "call_1", - "state": { - "continuation": { - "state": { - "input": "Input to chatbot1", - "completion": { - "model": "gpt-4o", - "internalSystemPrompt": false, - "tools": [ - { - "function": { - "toolID": "sys.chat.finish", - "name": "chatFinish", - "description": "Concludes the conversation. This can not be used to ask a question.", - "parameters": { - "properties": { - "return": { - "description": "The instructed value to return or a summary of the dialog if no value is instructed", - "type": "string" - } - }, - "type": "object" - } - } - } - ], - "messages": [ - { - "role": "system", - "content": [ - { - "text": "This is a chatbot" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "Input to chatbot1" - } - ], - "usage": {} - }, - { - "role": "assistant", - "content": [ - { - "text": "Assistant Response 1 - from chatbot1" - } - ], - "usage": {} - } - ], - "chat": true - } - }, - "result": "Assistant Response 1 - from chatbot1" - }, - "continuationToolID": "testdata/TestContextSubChat/test.gpt:chatbot" - } - } - ], - "subCallID": "call_1" - }, - "inputContextContinuationInput": "User 1", - "startContinuation": true - } -}` diff --git a/pkg/tests/testdata/TestContextSubChat/step2.golden b/pkg/tests/testdata/TestContextSubChat/step2.golden deleted file mode 100644 index dfcb2b96..00000000 --- a/pkg/tests/testdata/TestContextSubChat/step2.golden +++ /dev/null @@ -1,48 +0,0 @@ -`{ - "done": false, - "content": "Assistant Response 3 - from main chat tool", - "toolID": "testdata/TestContextSubChat/test.gpt:", - "state": { - "continuation": { - "state": { - "input": "User 1", - "completion": { - "model": "gpt-4o", - "internalSystemPrompt": false, - "messages": [ - { - "role": "system", - "content": [ - { - "text": "Assistant Response 2 - from context tool\nHello" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "User 1" - } - ], - "usage": {} - }, - { - "role": "assistant", - "content": [ - { - "text": "Assistant Response 3 - from main chat tool" - } - ], - "usage": {} - } - ], - "chat": true - } - }, - "result": "Assistant Response 3 - from main chat tool" - }, - "continuationToolID": "testdata/TestContextSubChat/test.gpt:" - } -}` diff --git a/pkg/tests/testdata/TestContextSubChat/step3.golden b/pkg/tests/testdata/TestContextSubChat/step3.golden deleted file mode 100644 index 0ccb188b..00000000 --- a/pkg/tests/testdata/TestContextSubChat/step3.golden +++ /dev/null @@ -1,188 +0,0 @@ -`{ - "done": false, - "content": "Assistant Response 3 - from main chat tool", - "toolID": "testdata/TestContextSubChat/test.gpt:", - "state": { - "continuation": { - "state": { - "input": "User 1", - "completion": { - "model": "gpt-4o", - "internalSystemPrompt": false, - "messages": [ - { - "role": "system", - "content": [ - { - "text": "Assistant Response 2 - from context tool\nHello" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "User 1" - } - ], - "usage": {} - }, - { - "role": "assistant", - "content": [ - { - "text": "Assistant Response 3 - from main chat tool" - } - ], - "usage": {} - } - ], - "chat": true - } - }, - "result": "Assistant Response 3 - from main chat tool" - }, - "continuationToolID": "testdata/TestContextSubChat/test.gpt:", - "resumeInput": "User 3", - "inputContextContinuation": { - "continuation": { - "state": { - "completion": { - "model": "gpt-4o", - "tools": [ - { - "function": { - "toolID": "testdata/TestContextSubChat/test.gpt:chatbot", - "name": "chatbot", - "parameters": { - "properties": { - "defaultPromptParameter": { - "description": "Prompt to send to the assistant. This may be an instruction or question.", - "type": "string" - } - }, - "type": "object" - } - } - } - ], - "messages": [ - { - "role": "system", - "content": [ - { - "text": "Call chatbot" - } - ], - "usage": {} - }, - { - "role": "assistant", - "content": [ - { - "toolCall": { - "index": 0, - "id": "call_3", - "function": { - "name": "chatbot", - "arguments": "Input to chatbot1 on resume" - } - } - } - ], - "usage": {} - } - ] - }, - "pending": { - "call_3": { - "index": 0, - "id": "call_3", - "function": { - "name": "chatbot", - "arguments": "Input to chatbot1 on resume" - } - } - } - }, - "calls": { - "call_3": { - "toolID": "testdata/TestContextSubChat/test.gpt:chatbot", - "input": "Input to chatbot1 on resume" - } - } - }, - "subCalls": [ - { - "toolId": "testdata/TestContextSubChat/test.gpt:chatbot", - "callId": "call_3", - "state": { - "continuation": { - "state": { - "input": "Input to chatbot1 on resume", - "completion": { - "model": "gpt-4o", - "internalSystemPrompt": false, - "tools": [ - { - "function": { - "toolID": "sys.chat.finish", - "name": "chatFinish", - "description": "Concludes the conversation. This can not be used to ask a question.", - "parameters": { - "properties": { - "return": { - "description": "The instructed value to return or a summary of the dialog if no value is instructed", - "type": "string" - } - }, - "type": "object" - } - } - } - ], - "messages": [ - { - "role": "system", - "content": [ - { - "text": "This is a chatbot" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "Input to chatbot1 on resume" - } - ], - "usage": {} - }, - { - "role": "assistant", - "content": [ - { - "text": "Assistant Response 4 - from chatbot1" - } - ], - "usage": {} - } - ], - "chat": true - } - }, - "result": "Assistant Response 4 - from chatbot1" - }, - "continuationToolID": "testdata/TestContextSubChat/test.gpt:chatbot" - } - } - ], - "subCallID": "call_3" - }, - "inputContextContinuationInput": "User 1", - "inputContextContinuationResumeInput": "User 3" - } -}` diff --git a/pkg/tests/testdata/TestContextSubChat/step4.golden b/pkg/tests/testdata/TestContextSubChat/step4.golden deleted file mode 100644 index 5e95d626..00000000 --- a/pkg/tests/testdata/TestContextSubChat/step4.golden +++ /dev/null @@ -1,66 +0,0 @@ -`{ - "done": false, - "content": "Assistant Response 6 - from main chat tool resume", - "toolID": "testdata/TestContextSubChat/test.gpt:", - "state": { - "continuation": { - "state": { - "input": "User 1", - "completion": { - "model": "gpt-4o", - "internalSystemPrompt": false, - "messages": [ - { - "role": "system", - "content": [ - { - "text": "Assistant Response 5 - from context tool resume\nHello" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "User 1" - } - ], - "usage": {} - }, - { - "role": "assistant", - "content": [ - { - "text": "Assistant Response 3 - from main chat tool" - } - ], - "usage": {} - }, - { - "role": "user", - "content": [ - { - "text": "User 3" - } - ], - "usage": {} - }, - { - "role": "assistant", - "content": [ - { - "text": "Assistant Response 6 - from main chat tool resume" - } - ], - "usage": {} - } - ], - "chat": true - } - }, - "result": "Assistant Response 6 - from main chat tool resume" - }, - "continuationToolID": "testdata/TestContextSubChat/test.gpt:" - } -}` From 93e77066e8dc357070728825eaecc5136852b67c Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Tue, 27 Aug 2024 10:44:57 -0700 Subject: [PATCH 52/83] bug: handle case where node_modules is deleted in local tool dev --- pkg/repos/runtimes/node/node.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/repos/runtimes/node/node.go b/pkg/repos/runtimes/node/node.go index aa57b059..4d73c13b 100644 --- a/pkg/repos/runtimes/node/node.go +++ b/pkg/repos/runtimes/node/node.go @@ -26,6 +26,7 @@ var releasesData []byte const ( downloadURL = "https://nodejs.org/dist/%s/" packageJSON = "package.json" + nodeModules = "node_modules" ) type Runtime struct { @@ -64,8 +65,15 @@ func (r *Runtime) supports(testCmd string, cmd []string) bool { func (r *Runtime) GetHash(tool types.Tool) (string, error) { if !tool.Source.IsGit() && tool.WorkingDir != "" { + var prefix string + // This hashes if the node_modules directory was deleted + if s, err := os.Stat(filepath.Join(tool.WorkingDir, nodeModules)); err == nil { + prefix = hash.Digest(tool.WorkingDir + s.ModTime().String())[:7] + } else if s, err := os.Stat(tool.WorkingDir); err == nil { + prefix = hash.Digest(tool.WorkingDir + s.ModTime().String())[:7] + } if s, err := os.Stat(filepath.Join(tool.WorkingDir, packageJSON)); err == nil { - return hash.Digest(tool.WorkingDir + s.ModTime().String())[:7], nil + return prefix + hash.Digest(tool.WorkingDir + s.ModTime().String())[:7], nil } } return "", nil From 7d469cec1ce9b1bda6e7437e6ffe3fdc1028a6c2 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Tue, 27 Aug 2024 11:03:21 -0700 Subject: [PATCH 53/83] bug: respect "share context" from referenced tools --- pkg/tests/runner2_test.go | 23 +++++++++ .../TestContextShareBug/call1-resp.golden | 9 ++++ .../testdata/TestContextShareBug/call1.golden | 25 ++++++++++ .../testdata/TestContextShareBug/step1.golden | 48 +++++++++++++++++++ pkg/types/tool.go | 9 ++++ 5 files changed, 114 insertions(+) create mode 100644 pkg/tests/testdata/TestContextShareBug/call1-resp.golden create mode 100644 pkg/tests/testdata/TestContextShareBug/call1.golden create mode 100644 pkg/tests/testdata/TestContextShareBug/step1.golden diff --git a/pkg/tests/runner2_test.go b/pkg/tests/runner2_test.go index 12ac4fa0..27d4c226 100644 --- a/pkg/tests/runner2_test.go +++ b/pkg/tests/runner2_test.go @@ -32,3 +32,26 @@ echo This is the input: ${GPTSCRIPT_INPUT} resp, err = r.Chat(context.Background(), resp.State, prg, nil, "input 2") r.AssertStep(t, resp, err) } + +func TestContextShareBug(t *testing.T) { + r := tester.NewRunner(t) + prg, err := loader.ProgramFromSource(context.Background(), ` +chat: true +tools: sharecontext + +Say hi + +--- +name: sharecontext +share context: realcontext +--- +name: realcontext + +#!sys.echo + +Yo dawg`, "") + require.NoError(t, err) + + resp, err := r.Chat(context.Background(), nil, prg, nil, "input 1") + r.AssertStep(t, resp, err) +} diff --git a/pkg/tests/testdata/TestContextShareBug/call1-resp.golden b/pkg/tests/testdata/TestContextShareBug/call1-resp.golden new file mode 100644 index 00000000..2861a036 --- /dev/null +++ b/pkg/tests/testdata/TestContextShareBug/call1-resp.golden @@ -0,0 +1,9 @@ +`{ + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 1" + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestContextShareBug/call1.golden b/pkg/tests/testdata/TestContextShareBug/call1.golden new file mode 100644 index 00000000..0a46f0ca --- /dev/null +++ b/pkg/tests/testdata/TestContextShareBug/call1.golden @@ -0,0 +1,25 @@ +`{ + "model": "gpt-4o", + "internalSystemPrompt": false, + "messages": [ + { + "role": "system", + "content": [ + { + "text": "\nYo dawg\nSay hi" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "input 1" + } + ], + "usage": {} + } + ], + "chat": true +}` diff --git a/pkg/tests/testdata/TestContextShareBug/step1.golden b/pkg/tests/testdata/TestContextShareBug/step1.golden new file mode 100644 index 00000000..cb17be6d --- /dev/null +++ b/pkg/tests/testdata/TestContextShareBug/step1.golden @@ -0,0 +1,48 @@ +`{ + "done": false, + "content": "TEST RESULT CALL: 1", + "toolID": "inline:", + "state": { + "continuation": { + "state": { + "input": "input 1", + "completion": { + "model": "gpt-4o", + "internalSystemPrompt": false, + "messages": [ + { + "role": "system", + "content": [ + { + "text": "\nYo dawg\nSay hi" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "input 1" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 1" + } + ], + "usage": {} + } + ], + "chat": true + } + }, + "result": "TEST RESULT CALL: 1" + }, + "continuationToolID": "inline:" + } +}` diff --git a/pkg/types/tool.go b/pkg/types/tool.go index 57ce3fbf..0d2a5cc0 100644 --- a/pkg/types/tool.go +++ b/pkg/types/tool.go @@ -557,6 +557,15 @@ func (t Tool) GetContextTools(prg Program) ([]ToolReference, error) { result.Add(contextRef) } + exportOnlyTools, err := t.getCompletionToolRefs(prg, nil, ToolTypeDefault, ToolTypeContext) + if err != nil { + return nil, err + } + + for _, contextRef := range exportOnlyTools { + result.AddAll(prg.ToolSet[contextRef.ToolID].getExportedContext(prg)) + } + return result.List() } From e83fe6538b32e3e04d030c272ed01fce77ae98dd Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Tue, 27 Aug 2024 20:23:13 -0400 Subject: [PATCH 54/83] fix: complete the SDK server options on run Signed-off-by: Donnie Adams --- pkg/sdkserver/server.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/sdkserver/server.go b/pkg/sdkserver/server.go index 4ef28267..f72e7ae9 100644 --- a/pkg/sdkserver/server.go +++ b/pkg/sdkserver/server.go @@ -32,6 +32,8 @@ type Options struct { // Run will start the server and block until the server is shut down. func Run(ctx context.Context, opts Options) error { + opts = complete(opts) + listener, err := newListener(opts) if err != nil { return err From 5dfb195b0d8cfe555074ef2f9f96bf76f7c8364a Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Wed, 28 Aug 2024 00:13:50 -0700 Subject: [PATCH 55/83] bug: show error when config.json is malformed --- pkg/config/cliconfig.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/cliconfig.go b/pkg/config/cliconfig.go index 43649135..dd358d52 100644 --- a/pkg/config/cliconfig.go +++ b/pkg/config/cliconfig.go @@ -137,7 +137,7 @@ func ReadCLIConfig(gptscriptConfigFile string) (*CLIConfig, error) { location: gptscriptConfigFile, } if err := json.Unmarshal(data, result); err != nil { - return nil, err + return nil, fmt.Errorf("failed to unmarshal %s: %v", gptscriptConfigFile, err) } if result.CredentialsStore == "" { From 33741b12c1593aca5e1c423dbbf00b86a6f48996 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Wed, 28 Aug 2024 23:17:35 -0700 Subject: [PATCH 56/83] chore: refactor logic for tool sharing --- pkg/engine/engine.go | 6 +- pkg/runner/input.go | 3 +- pkg/runner/output.go | 3 +- pkg/runner/runner.go | 6 +- pkg/tests/testdata/TestAgentOnly/call2.golden | 8 +- pkg/tests/testdata/TestAgentOnly/step1.golden | 8 +- .../testdata/TestAgents/call3-resp.golden | 2 +- pkg/tests/testdata/TestAgents/call3.golden | 8 +- pkg/tests/testdata/TestAgents/step1.golden | 12 +- .../testdata/TestExport/call1-resp.golden | 2 +- pkg/tests/testdata/TestExport/call1.golden | 8 +- pkg/tests/testdata/TestExport/call3.golden | 12 +- .../testdata/TestExportContext/call1.golden | 2 +- .../testdata/TestToolRefAll/call1.golden | 18 +- pkg/types/completion.go | 20 +- pkg/types/set.go | 11 + pkg/types/tool.go | 395 +++++++----------- 17 files changed, 211 insertions(+), 313 deletions(-) diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 14b75e0a..d028d50b 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -204,7 +204,7 @@ func NewContext(ctx context.Context, prg *types.Program, input string) (Context, Input: input, } - agentGroup, err := callCtx.Tool.GetAgents(*prg) + agentGroup, err := callCtx.Tool.GetToolsByType(prg, types.ToolTypeAgent) if err != nil { return callCtx, err } @@ -225,7 +225,7 @@ func (c *Context) SubCallContext(ctx context.Context, input, toolID, callID stri callID = counter.Next() } - agentGroup, err := c.Tool.GetNextAgentGroup(*c.Program, c.AgentGroup, toolID) + agentGroup, err := c.Tool.GetNextAgentGroup(c.Program, c.AgentGroup, toolID) if err != nil { return Context{}, err } @@ -272,7 +272,7 @@ func populateMessageParams(ctx Context, completion *types.CompletionRequest, too } var err error - completion.Tools, err = tool.GetCompletionTools(*ctx.Program, ctx.AgentGroup...) + completion.Tools, err = tool.GetChatCompletionTools(*ctx.Program, ctx.AgentGroup...) if err != nil { return err } diff --git a/pkg/runner/input.go b/pkg/runner/input.go index 7d77330e..a211ec9d 100644 --- a/pkg/runner/input.go +++ b/pkg/runner/input.go @@ -5,10 +5,11 @@ import ( "fmt" "github.com/gptscript-ai/gptscript/pkg/engine" + "github.com/gptscript-ai/gptscript/pkg/types" ) func (r *Runner) handleInput(callCtx engine.Context, monitor Monitor, env []string, input string) (string, error) { - inputToolRefs, err := callCtx.Tool.GetInputFilterTools(*callCtx.Program) + inputToolRefs, err := callCtx.Tool.GetToolsByType(callCtx.Program, types.ToolTypeInput) if err != nil { return "", err } diff --git a/pkg/runner/output.go b/pkg/runner/output.go index d4cb4b9b..e5fe849d 100644 --- a/pkg/runner/output.go +++ b/pkg/runner/output.go @@ -6,10 +6,11 @@ import ( "fmt" "github.com/gptscript-ai/gptscript/pkg/engine" + "github.com/gptscript-ai/gptscript/pkg/types" ) func (r *Runner) handleOutput(callCtx engine.Context, monitor Monitor, env []string, state *State, retErr error) (*State, error) { - outputToolRefs, err := callCtx.Tool.GetOutputFilterTools(*callCtx.Program) + outputToolRefs, err := callCtx.Tool.GetToolsByType(callCtx.Program, types.ToolTypeOutput) if err != nil { return nil, err } diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 3035a1d1..c843b6b5 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -330,7 +330,7 @@ func getToolRefInput(prg *types.Program, ref types.ToolReference, input string) } func (r *Runner) getContext(callCtx engine.Context, state *State, monitor Monitor, env []string, input string) (result []engine.InputContext, _ error) { - toolRefs, err := callCtx.Tool.GetContextTools(*callCtx.Program) + toolRefs, err := callCtx.Tool.GetToolsByType(callCtx.Program, types.ToolTypeContext) if err != nil { return nil, err } @@ -387,7 +387,7 @@ func (r *Runner) start(callCtx engine.Context, state *State, monitor Monitor, en return nil, err } - credTools, err := callCtx.Tool.GetCredentialTools(*callCtx.Program, callCtx.AgentGroup) + credTools, err := callCtx.Tool.GetToolsByType(callCtx.Program, types.ToolTypeCredential) if err != nil { return nil, err } @@ -503,7 +503,7 @@ func (r *Runner) resume(callCtx engine.Context, monitor Monitor, env []string, s progress, progressClose := streamProgress(&callCtx, monitor) defer progressClose() - credTools, err := callCtx.Tool.GetCredentialTools(*callCtx.Program, callCtx.AgentGroup) + credTools, err := callCtx.Tool.GetToolsByType(callCtx.Program, types.ToolTypeCredential) if err != nil { return nil, err } diff --git a/pkg/tests/testdata/TestAgentOnly/call2.golden b/pkg/tests/testdata/TestAgentOnly/call2.golden index 82f95523..7f6b155b 100644 --- a/pkg/tests/testdata/TestAgentOnly/call2.golden +++ b/pkg/tests/testdata/TestAgentOnly/call2.golden @@ -4,8 +4,8 @@ "tools": [ { "function": { - "toolID": "testdata/TestAgentOnly/test.gpt:agent1", - "name": "agent1", + "toolID": "testdata/TestAgentOnly/test.gpt:agent3", + "name": "agent3", "parameters": { "properties": { "defaultPromptParameter": { @@ -19,8 +19,8 @@ }, { "function": { - "toolID": "testdata/TestAgentOnly/test.gpt:agent3", - "name": "agent3", + "toolID": "testdata/TestAgentOnly/test.gpt:agent1", + "name": "agent1", "parameters": { "properties": { "defaultPromptParameter": { diff --git a/pkg/tests/testdata/TestAgentOnly/step1.golden b/pkg/tests/testdata/TestAgentOnly/step1.golden index 662dbf04..2cda2025 100644 --- a/pkg/tests/testdata/TestAgentOnly/step1.golden +++ b/pkg/tests/testdata/TestAgentOnly/step1.golden @@ -96,8 +96,8 @@ "tools": [ { "function": { - "toolID": "testdata/TestAgentOnly/test.gpt:agent1", - "name": "agent1", + "toolID": "testdata/TestAgentOnly/test.gpt:agent3", + "name": "agent3", "parameters": { "properties": { "defaultPromptParameter": { @@ -111,8 +111,8 @@ }, { "function": { - "toolID": "testdata/TestAgentOnly/test.gpt:agent3", - "name": "agent3", + "toolID": "testdata/TestAgentOnly/test.gpt:agent1", + "name": "agent1", "parameters": { "properties": { "defaultPromptParameter": { diff --git a/pkg/tests/testdata/TestAgents/call3-resp.golden b/pkg/tests/testdata/TestAgents/call3-resp.golden index e2a65c99..7568fc69 100644 --- a/pkg/tests/testdata/TestAgents/call3-resp.golden +++ b/pkg/tests/testdata/TestAgents/call3-resp.golden @@ -3,7 +3,7 @@ "content": [ { "toolCall": { - "index": 1, + "index": 0, "id": "call_3", "function": { "name": "agent3" diff --git a/pkg/tests/testdata/TestAgents/call3.golden b/pkg/tests/testdata/TestAgents/call3.golden index f9b45a1b..5b1638e0 100644 --- a/pkg/tests/testdata/TestAgents/call3.golden +++ b/pkg/tests/testdata/TestAgents/call3.golden @@ -4,8 +4,8 @@ "tools": [ { "function": { - "toolID": "testdata/TestAgents/test.gpt:agent1", - "name": "agent1", + "toolID": "testdata/TestAgents/test.gpt:agent3", + "name": "agent3", "parameters": { "properties": { "defaultPromptParameter": { @@ -19,8 +19,8 @@ }, { "function": { - "toolID": "testdata/TestAgents/test.gpt:agent3", - "name": "agent3", + "toolID": "testdata/TestAgents/test.gpt:agent1", + "name": "agent1", "parameters": { "properties": { "defaultPromptParameter": { diff --git a/pkg/tests/testdata/TestAgents/step1.golden b/pkg/tests/testdata/TestAgents/step1.golden index 3047e695..72e01114 100644 --- a/pkg/tests/testdata/TestAgents/step1.golden +++ b/pkg/tests/testdata/TestAgents/step1.golden @@ -178,8 +178,8 @@ "tools": [ { "function": { - "toolID": "testdata/TestAgents/test.gpt:agent1", - "name": "agent1", + "toolID": "testdata/TestAgents/test.gpt:agent3", + "name": "agent3", "parameters": { "properties": { "defaultPromptParameter": { @@ -193,8 +193,8 @@ }, { "function": { - "toolID": "testdata/TestAgents/test.gpt:agent3", - "name": "agent3", + "toolID": "testdata/TestAgents/test.gpt:agent1", + "name": "agent1", "parameters": { "properties": { "defaultPromptParameter": { @@ -222,7 +222,7 @@ "content": [ { "toolCall": { - "index": 1, + "index": 0, "id": "call_3", "function": { "name": "agent3" @@ -237,7 +237,7 @@ }, "pending": { "call_3": { - "index": 1, + "index": 0, "id": "call_3", "function": { "name": "agent3" diff --git a/pkg/tests/testdata/TestExport/call1-resp.golden b/pkg/tests/testdata/TestExport/call1-resp.golden index 8462d188..7fe59586 100644 --- a/pkg/tests/testdata/TestExport/call1-resp.golden +++ b/pkg/tests/testdata/TestExport/call1-resp.golden @@ -3,7 +3,7 @@ "content": [ { "toolCall": { - "index": 2, + "index": 1, "id": "call_1", "function": { "name": "transient" diff --git a/pkg/tests/testdata/TestExport/call1.golden b/pkg/tests/testdata/TestExport/call1.golden index 9f8b650d..b700ee55 100644 --- a/pkg/tests/testdata/TestExport/call1.golden +++ b/pkg/tests/testdata/TestExport/call1.golden @@ -18,8 +18,8 @@ }, { "function": { - "toolID": "testdata/TestExport/parent.gpt:parent-local", - "name": "parentLocal", + "toolID": "testdata/TestExport/sub/child.gpt:transient", + "name": "transient", "parameters": { "properties": { "defaultPromptParameter": { @@ -33,8 +33,8 @@ }, { "function": { - "toolID": "testdata/TestExport/sub/child.gpt:transient", - "name": "transient", + "toolID": "testdata/TestExport/parent.gpt:parent-local", + "name": "parentLocal", "parameters": { "properties": { "defaultPromptParameter": { diff --git a/pkg/tests/testdata/TestExport/call3.golden b/pkg/tests/testdata/TestExport/call3.golden index ccf7e980..d2abca0c 100644 --- a/pkg/tests/testdata/TestExport/call3.golden +++ b/pkg/tests/testdata/TestExport/call3.golden @@ -18,8 +18,8 @@ }, { "function": { - "toolID": "testdata/TestExport/parent.gpt:parent-local", - "name": "parentLocal", + "toolID": "testdata/TestExport/sub/child.gpt:transient", + "name": "transient", "parameters": { "properties": { "defaultPromptParameter": { @@ -33,8 +33,8 @@ }, { "function": { - "toolID": "testdata/TestExport/sub/child.gpt:transient", - "name": "transient", + "toolID": "testdata/TestExport/parent.gpt:parent-local", + "name": "parentLocal", "parameters": { "properties": { "defaultPromptParameter": { @@ -62,7 +62,7 @@ "content": [ { "toolCall": { - "index": 2, + "index": 1, "id": "call_1", "function": { "name": "transient" @@ -80,7 +80,7 @@ } ], "toolCall": { - "index": 2, + "index": 1, "id": "call_1", "function": { "name": "transient" diff --git a/pkg/tests/testdata/TestExportContext/call1.golden b/pkg/tests/testdata/TestExportContext/call1.golden index bec15478..0ee8f9fe 100644 --- a/pkg/tests/testdata/TestExportContext/call1.golden +++ b/pkg/tests/testdata/TestExportContext/call1.golden @@ -38,7 +38,7 @@ "role": "system", "content": [ { - "text": "this is from external context\nthis is from context\nThis is from tool" + "text": "this is from context\nthis is from external context\nThis is from tool" } ], "usage": {} diff --git a/pkg/tests/testdata/TestToolRefAll/call1.golden b/pkg/tests/testdata/TestToolRefAll/call1.golden index ef36e3fb..9289affa 100644 --- a/pkg/tests/testdata/TestToolRefAll/call1.golden +++ b/pkg/tests/testdata/TestToolRefAll/call1.golden @@ -18,12 +18,12 @@ }, { "function": { - "toolID": "testdata/TestToolRefAll/test.gpt:none", - "name": "none", + "toolID": "testdata/TestToolRefAll/test.gpt:agentAssistant", + "name": "agentAssistant", "parameters": { "properties": { - "noneArg": { - "description": "stuff", + "defaultPromptParameter": { + "description": "Prompt to send to the tool. This may be an instruction or question.", "type": "string" } }, @@ -33,12 +33,12 @@ }, { "function": { - "toolID": "testdata/TestToolRefAll/test.gpt:agentAssistant", - "name": "agent", + "toolID": "testdata/TestToolRefAll/test.gpt:none", + "name": "none", "parameters": { "properties": { - "defaultPromptParameter": { - "description": "Prompt to send to the tool. This may be an instruction or question.", + "noneArg": { + "description": "stuff", "type": "string" } }, @@ -52,7 +52,7 @@ "role": "system", "content": [ { - "text": "\nShared context\n\nContext Body\nMain tool" + "text": "\nContext Body\n\nShared context\nMain tool" } ], "usage": {} diff --git a/pkg/types/completion.go b/pkg/types/completion.go index dd70ad50..5b3899c3 100644 --- a/pkg/types/completion.go +++ b/pkg/types/completion.go @@ -9,15 +9,15 @@ import ( ) type CompletionRequest struct { - Model string `json:"model,omitempty"` - InternalSystemPrompt *bool `json:"internalSystemPrompt,omitempty"` - Tools []CompletionTool `json:"tools,omitempty"` - Messages []CompletionMessage `json:"messages,omitempty"` - MaxTokens int `json:"maxTokens,omitempty"` - Chat bool `json:"chat,omitempty"` - Temperature *float32 `json:"temperature,omitempty"` - JSONResponse bool `json:"jsonResponse,omitempty"` - Cache *bool `json:"cache,omitempty"` + Model string `json:"model,omitempty"` + InternalSystemPrompt *bool `json:"internalSystemPrompt,omitempty"` + Tools []ChatCompletionTool `json:"tools,omitempty"` + Messages []CompletionMessage `json:"messages,omitempty"` + MaxTokens int `json:"maxTokens,omitempty"` + Chat bool `json:"chat,omitempty"` + Temperature *float32 `json:"temperature,omitempty"` + JSONResponse bool `json:"jsonResponse,omitempty"` + Cache *bool `json:"cache,omitempty"` } func (r *CompletionRequest) GetCache() bool { @@ -27,7 +27,7 @@ func (r *CompletionRequest) GetCache() bool { return *r.Cache } -type CompletionTool struct { +type ChatCompletionTool struct { Function CompletionFunctionDefinition `json:"function,omitempty"` } diff --git a/pkg/types/set.go b/pkg/types/set.go index 230e112b..65b73d22 100644 --- a/pkg/types/set.go +++ b/pkg/types/set.go @@ -19,6 +19,17 @@ func (t *toolRefSet) List() (result []ToolReference, err error) { return result, t.err } +func (t *toolRefSet) Contains(value ToolReference) bool { + key := toolRefKey{ + name: value.Named, + toolID: value.ToolID, + arg: value.Arg, + } + + _, ok := t.set[key] + return ok +} + func (t *toolRefSet) HasTool(toolID string) bool { for _, ref := range t.set { if ref.ToolID == toolID { diff --git a/pkg/types/tool.go b/pkg/types/tool.go index 0d2a5cc0..d9d59837 100644 --- a/pkg/types/tool.go +++ b/pkg/types/tool.go @@ -33,11 +33,15 @@ const ( ToolTypeAgent = ToolType("agent") ToolTypeOutput = ToolType("output") ToolTypeInput = ToolType("input") - ToolTypeAssistant = ToolType("assistant") ToolTypeTool = ToolType("tool") ToolTypeCredential = ToolType("credential") - ToolTypeProvider = ToolType("provider") ToolTypeDefault = ToolType("") + + // The following types logically exist but have no real code reference. These are kept + // here just so that we have a comprehensive list + + ToolTypeAssistant = ToolType("assistant") + ToolTypeProvider = ToolType("provider") ) type ErrToolNotFound struct { @@ -140,6 +144,28 @@ type Parameters struct { Type ToolType `json:"type,omitempty"` } +func (p Parameters) allExports() []string { + return slices.Concat( + p.ExportContext, + p.Export, + p.ExportCredentials, + p.ExportInputFilters, + p.ExportOutputFilters, + ) +} + +func (p Parameters) allReferences() []string { + return slices.Concat( + p.GlobalTools, + p.Tools, + p.Context, + p.Agents, + p.Credentials, + p.InputFilters, + p.OutputFilters, + ) +} + func (p Parameters) ToolRefNames() []string { return slices.Concat( p.Tools, @@ -335,39 +361,6 @@ func ParseCredentialArgs(toolName string, input string) (string, string, map[str return originalName, alias, args, nil } -func (t Tool) GetAgents(prg Program) (result []ToolReference, _ error) { - toolRefs, err := t.GetToolRefsFromNames(t.Agents) - if err != nil { - return nil, err - } - - genericToolRefs, err := t.getCompletionToolRefs(prg, nil, ToolTypeAgent) - if err != nil { - return nil, err - } - - toolRefs = append(toolRefs, genericToolRefs...) - - // Agent Tool refs must be named - for i, toolRef := range toolRefs { - if toolRef.Named != "" { - continue - } - tool := prg.ToolSet[toolRef.ToolID] - name := tool.Name - if name == "" { - name = toolRef.Reference - } - normed := ToolNormalizer(name) - if trimmed := strings.TrimSuffix(strings.TrimSuffix(normed, "Agent"), "Assistant"); trimmed != "" { - normed = trimmed - } - toolRefs[i].Named = normed - } - - return toolRefs, nil -} - func (t Tool) GetToolRefsFromNames(names []string) (result []ToolReference, _ error) { for _, toolName := range names { toolRefs, ok := t.ToolMapping[toolName] @@ -507,293 +500,185 @@ func (t ToolDef) String() string { return buf.String() } -func (t Tool) getExportedContext(prg Program) ([]ToolReference, error) { - result := &toolRefSet{} - - exportRefs, err := t.GetToolRefsFromNames(t.ExportContext) - if err != nil { - return nil, err - } - - for _, exportRef := range exportRefs { - result.Add(exportRef) - - tool := prg.ToolSet[exportRef.ToolID] - result.AddAll(tool.getExportedContext(prg)) - } - - return result.List() -} - -func (t Tool) getExportedTools(prg Program) ([]ToolReference, error) { - result := &toolRefSet{} - - exportRefs, err := t.GetToolRefsFromNames(t.Export) - if err != nil { - return nil, err - } - - for _, exportRef := range exportRefs { - result.Add(exportRef) - result.AddAll(prg.ToolSet[exportRef.ToolID].getExportedTools(prg)) - } - - return result.List() -} - -// GetContextTools returns all tools that are in the context of the tool including all the -// contexts that are exported by the context tools. This will recurse all exports. -func (t Tool) GetContextTools(prg Program) ([]ToolReference, error) { - result := &toolRefSet{} - result.AddAll(t.getDirectContextToolRefs(prg)) - - contextRefs, err := t.getCompletionToolRefs(prg, nil, ToolTypeContext) - if err != nil { - return nil, err - } - - for _, contextRef := range contextRefs { - result.AddAll(prg.ToolSet[contextRef.ToolID].getExportedContext(prg)) - result.Add(contextRef) - } - - exportOnlyTools, err := t.getCompletionToolRefs(prg, nil, ToolTypeDefault, ToolTypeContext) - if err != nil { - return nil, err - } +func (t Tool) GetNextAgentGroup(prg *Program, agentGroup []ToolReference, toolID string) (result []ToolReference, _ error) { + newAgentGroup := toolRefSet{} + newAgentGroup.AddAll(t.GetToolsByType(prg, ToolTypeAgent)) - for _, contextRef := range exportOnlyTools { - result.AddAll(prg.ToolSet[contextRef.ToolID].getExportedContext(prg)) + if newAgentGroup.HasTool(toolID) { + // Join new agent group + return newAgentGroup.List() } - return result.List() + return agentGroup, nil } -// GetContextTools returns all tools that are in the context of the tool including all the -// contexts that are exported by the context tools. This will recurse all exports. -func (t Tool) getDirectContextToolRefs(prg Program) ([]ToolReference, error) { - result := &toolRefSet{} - - contextRefs, err := t.GetToolRefsFromNames(t.Context) +func (t Tool) getAgents(prg *Program) (result []ToolReference, _ error) { + toolRefs, err := t.GetToolRefsFromNames(t.Agents) if err != nil { return nil, err } - for _, contextRef := range contextRefs { - result.AddAll(prg.ToolSet[contextRef.ToolID].getExportedContext(prg)) - result.Add(contextRef) + // Agent Tool refs must be named + for i, toolRef := range toolRefs { + if toolRef.Named != "" { + continue + } + tool := prg.ToolSet[toolRef.ToolID] + name := tool.Name + if name == "" { + name = toolRef.Reference + } + normed := ToolNormalizer(name) + if trimmed := strings.TrimSuffix(strings.TrimSuffix(normed, "Agent"), "Assistant"); trimmed != "" { + normed = trimmed + } + toolRefs[i].Named = normed } - return result.List() + return toolRefs, nil } -func (t Tool) GetOutputFilterTools(program Program) ([]ToolReference, error) { - result := &toolRefSet{} - - outputFilterRefs, err := t.GetToolRefsFromNames(t.OutputFilters) - if err != nil { - return nil, err - } - - for _, outputFilterRef := range outputFilterRefs { - result.Add(outputFilterRef) - } - - result.AddAll(t.getCompletionToolRefs(program, nil, ToolTypeOutput)) - - contextRefs, err := t.getDirectContextToolRefs(program) - if err != nil { - return nil, err +func (t Tool) GetToolsByType(prg *Program, toolType ToolType) ([]ToolReference, error) { + if toolType == ToolTypeAgent { + // Agents are special, they can only be sourced from direct references and not the generic 'tool:' or shared by references + return t.getAgents(prg) } - for _, contextRef := range contextRefs { - contextTool := program.ToolSet[contextRef.ToolID] - result.AddAll(contextTool.GetToolRefsFromNames(contextTool.ExportOutputFilters)) - } - - return result.List() -} - -func (t Tool) GetInputFilterTools(program Program) ([]ToolReference, error) { - result := &toolRefSet{} + toolSet := &toolRefSet{} - inputFilterRefs, err := t.GetToolRefsFromNames(t.InputFilters) - if err != nil { - return nil, err - } + var ( + directRefs []string + toolsListFilterType = []ToolType{toolType} + ) - for _, inputFilterRef := range inputFilterRefs { - result.Add(inputFilterRef) + switch toolType { + case ToolTypeContext: + directRefs = t.Context + case ToolTypeOutput: + directRefs = t.OutputFilters + case ToolTypeInput: + directRefs = t.InputFilters + case ToolTypeTool: + toolsListFilterType = append(toolsListFilterType, ToolTypeDefault, ToolTypeAgent) + case ToolTypeCredential: + directRefs = t.Credentials + default: + return nil, fmt.Errorf("unknown tool type %v", toolType) } - result.AddAll(t.getCompletionToolRefs(program, nil, ToolTypeInput)) + toolSet.AddAll(t.GetToolRefsFromNames(directRefs)) - contextRefs, err := t.getDirectContextToolRefs(program) + toolRefs, err := t.GetToolRefsFromNames(t.Tools) if err != nil { return nil, err } - for _, contextRef := range contextRefs { - contextTool := program.ToolSet[contextRef.ToolID] - result.AddAll(contextTool.GetToolRefsFromNames(contextTool.ExportInputFilters)) - } - - return result.List() -} - -func (t Tool) GetNextAgentGroup(prg Program, agentGroup []ToolReference, toolID string) (result []ToolReference, _ error) { - newAgentGroup := toolRefSet{} - if err := t.addAgents(prg, &newAgentGroup); err != nil { - return nil, err - } - - if newAgentGroup.HasTool(toolID) { - // Join new agent group - return newAgentGroup.List() - } - - return agentGroup, nil -} - -func filterRefs(prg Program, refs []ToolReference, types ...ToolType) (result []ToolReference) { - for _, ref := range refs { - if slices.Contains(types, prg.ToolSet[ref.ToolID].Type) { - result = append(result, ref) + for _, toolRef := range toolRefs { + tool, ok := prg.ToolSet[toolRef.ToolID] + if !ok { + continue + } + if slices.Contains(toolsListFilterType, tool.Type) { + toolSet.Add(toolRef) } - } - return -} - -func (t Tool) GetCompletionTools(prg Program, agentGroup ...ToolReference) (result []CompletionTool, err error) { - toolSet := &toolRefSet{} - toolSet.AddAll(t.getCompletionToolRefs(prg, agentGroup, ToolTypeDefault, ToolTypeTool)) - - if err := t.addAgents(prg, toolSet); err != nil { - return nil, err } - refs, err := toolSet.List() + exportSources, err := t.getExportSources(prg) if err != nil { return nil, err } - return toolRefsToCompletionTools(refs, prg), nil -} - -func (t Tool) addAgents(prg Program, result *toolRefSet) error { - subToolRefs, err := t.GetAgents(prg) - if err != nil { - return err - } - - for _, subToolRef := range subToolRefs { - // don't add yourself - if subToolRef.ToolID != t.ID { - // Add the tool itself and no exports - result.Add(subToolRef) + for _, exportSource := range exportSources { + var ( + tool = prg.ToolSet[exportSource.ToolID] + exportRefs []string + ) + + switch toolType { + case ToolTypeContext: + exportRefs = tool.ExportContext + case ToolTypeOutput: + exportRefs = tool.ExportOutputFilters + case ToolTypeInput: + exportRefs = tool.ExportInputFilters + case ToolTypeTool: + exportRefs = tool.Export + case ToolTypeCredential: + exportRefs = tool.ExportCredentials + default: + return nil, fmt.Errorf("unknown tool type %v", toolType) } + toolSet.AddAll(tool.GetToolRefsFromNames(exportRefs)) } - return nil + return toolSet.List() } -func (t Tool) addReferencedTools(prg Program, result *toolRefSet) error { - subToolRefs, err := t.GetToolRefsFromNames(t.Parameters.Tools) +func (t Tool) addExportsRecursively(prg *Program, toolSet *toolRefSet) error { + toolRefs, err := t.GetToolRefsFromNames(t.allExports()) if err != nil { return err } - for _, subToolRef := range subToolRefs { - // Add the tool - result.Add(subToolRef) + for _, toolRef := range toolRefs { + if toolSet.Contains(toolRef) { + continue + } - // Get all tools exports - result.AddAll(prg.ToolSet[subToolRef.ToolID].getExportedTools(prg)) + toolSet.Add(toolRef) + if err := prg.ToolSet[toolRef.ToolID].addExportsRecursively(prg, toolSet); err != nil { + return err + } } return nil } -func (t Tool) addContextExportedTools(prg Program, result *toolRefSet) error { - contextTools, err := t.getDirectContextToolRefs(prg) +func (t Tool) getExportSources(prg *Program) ([]ToolReference, error) { + // We start first with all references from this tool. This gives us the + // initial set of export sources. + // Then all tools in the export sources in the set we look for exports of those tools recursively. + // So a share of a share of a share should be added. + + toolSet := toolRefSet{} + toolRefs, err := t.GetToolRefsFromNames(t.allReferences()) if err != nil { - return err + return nil, err } - for _, contextTool := range contextTools { - result.AddAll(prg.ToolSet[contextTool.ToolID].getExportedTools(prg)) + for _, toolRef := range toolRefs { + if err := prg.ToolSet[toolRef.ToolID].addExportsRecursively(prg, &toolSet); err != nil { + return nil, err + } + toolSet.Add(toolRef) } - return nil + return toolSet.List() } -func (t Tool) getCompletionToolRefs(prg Program, agentGroup []ToolReference, types ...ToolType) ([]ToolReference, error) { - if len(types) == 0 { - types = []ToolType{ToolTypeDefault, ToolTypeTool} - } - - result := toolRefSet{} +func (t Tool) GetChatCompletionTools(prg Program, agentGroup ...ToolReference) (result []ChatCompletionTool, err error) { + toolSet := &toolRefSet{} + toolSet.AddAll(t.GetToolsByType(&prg, ToolTypeTool)) + toolSet.AddAll(t.GetToolsByType(&prg, ToolTypeAgent)) if t.Chat { for _, agent := range agentGroup { // don't add yourself if agent.ToolID != t.ID { - result.Add(agent) + toolSet.Add(agent) } } } - if err := t.addReferencedTools(prg, &result); err != nil { - return nil, err - } - - if err := t.addContextExportedTools(prg, &result); err != nil { - return nil, err - } - - refs, err := result.List() - return filterRefs(prg, refs, types...), err -} - -func (t Tool) GetCredentialTools(prg Program, agentGroup []ToolReference) ([]ToolReference, error) { - result := toolRefSet{} - - result.AddAll(t.GetToolRefsFromNames(t.Credentials)) - - result.AddAll(t.getCompletionToolRefs(prg, nil, ToolTypeCredential)) - - toolRefs, err := result.List() - if err != nil { - return nil, err - } - for _, toolRef := range toolRefs { - referencedTool := prg.ToolSet[toolRef.ToolID] - result.AddAll(referencedTool.GetToolRefsFromNames(referencedTool.ExportCredentials)) - } - - toolRefs, err = t.getCompletionToolRefs(prg, agentGroup) - if err != nil { - return nil, err - } - for _, toolRef := range toolRefs { - referencedTool := prg.ToolSet[toolRef.ToolID] - result.AddAll(referencedTool.GetToolRefsFromNames(referencedTool.ExportCredentials)) - } - - contextToolRefs, err := t.GetContextTools(prg) + refs, err := toolSet.List() if err != nil { return nil, err } - for _, contextToolRef := range contextToolRefs { - contextTool := prg.ToolSet[contextToolRef.ToolID] - result.AddAll(contextTool.GetToolRefsFromNames(contextTool.ExportCredentials)) - } - - return result.List() + return toolRefsToCompletionTools(refs, prg), nil } -func toolRefsToCompletionTools(completionTools []ToolReference, prg Program) (result []CompletionTool) { +func toolRefsToCompletionTools(completionTools []ToolReference, prg Program) (result []ChatCompletionTool) { toolNames := map[string]struct{}{} for _, subToolRef := range completionTools { @@ -814,7 +699,7 @@ func toolRefsToCompletionTools(completionTools []ToolReference, prg Program) (re if subTool.Instructions == "" { log.Debugf("Skipping zero instruction tool %s (%s)", subToolName, subTool.ID) } else { - result = append(result, CompletionTool{ + result = append(result, ChatCompletionTool{ Function: CompletionFunctionDefinition{ ToolID: subTool.ID, Name: PickToolName(subToolName, toolNames), From 1ad818bb3b6edc01be3b280ace1ab50f0781b32e Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Fri, 30 Aug 2024 09:31:47 -0400 Subject: [PATCH 57/83] enhance: avoid context limit (#832) Signed-off-by: Grant Linville --- pkg/openai/client.go | 46 ++++++++++++++++++++++++++++++++++++++++++++ pkg/openai/count.go | 34 ++++++++++++++++++++++++-------- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/pkg/openai/client.go b/pkg/openai/client.go index 42a1a39e..61a7ec77 100644 --- a/pkg/openai/client.go +++ b/pkg/openai/client.go @@ -2,6 +2,7 @@ package openai import ( "context" + "errors" "io" "log/slog" "os" @@ -24,6 +25,7 @@ import ( const ( DefaultModel = openai.GPT4o BuiltinCredName = "sys.openai" + TooLongMessage = "Error: tool call output is too long" ) var ( @@ -317,6 +319,14 @@ func (c *Client) Call(ctx context.Context, messageRequest types.CompletionReques } if messageRequest.Chat { + // Check the last message. If it is from a tool call, and if it takes up more than 80% of the budget on its own, reject it. + lastMessage := msgs[len(msgs)-1] + if lastMessage.Role == string(types.CompletionMessageRoleTypeTool) && countMessage(lastMessage) > int(float64(getBudget(messageRequest.MaxTokens))*0.8) { + // We need to update it in the msgs slice for right now and in the messageRequest for future calls. + msgs[len(msgs)-1].Content = TooLongMessage + messageRequest.Messages[len(messageRequest.Messages)-1].Content = types.Text(TooLongMessage) + } + msgs = dropMessagesOverCount(messageRequest.MaxTokens, msgs) } @@ -383,6 +393,16 @@ func (c *Client) Call(ctx context.Context, messageRequest types.CompletionReques return nil, err } else if !ok { response, err = c.call(ctx, request, id, status) + + // If we got back a context length exceeded error, keep retrying and shrinking the message history until we pass. + var apiError *openai.APIError + if errors.As(err, &apiError) && apiError.Code == "context_length_exceeded" && messageRequest.Chat { + // Decrease maxTokens by 10% to make garbage collection more aggressive. + // The retry loop will further decrease maxTokens if needed. + maxTokens := decreaseTenPercent(messageRequest.MaxTokens) + response, err = c.contextLimitRetryLoop(ctx, request, id, maxTokens, status) + } + if err != nil { return nil, err } @@ -421,6 +441,32 @@ func (c *Client) Call(ctx context.Context, messageRequest types.CompletionReques return &result, nil } +func (c *Client) contextLimitRetryLoop(ctx context.Context, request openai.ChatCompletionRequest, id string, maxTokens int, status chan<- types.CompletionStatus) ([]openai.ChatCompletionStreamResponse, error) { + var ( + response []openai.ChatCompletionStreamResponse + err error + ) + + for range 10 { // maximum 10 tries + // Try to drop older messages again, with a decreased max tokens. + request.Messages = dropMessagesOverCount(maxTokens, request.Messages) + response, err = c.call(ctx, request, id, status) + if err == nil { + return response, nil + } + + var apiError *openai.APIError + if errors.As(err, &apiError) && apiError.Code == "context_length_exceeded" { + // Decrease maxTokens and try again + maxTokens = decreaseTenPercent(maxTokens) + continue + } + return nil, err + } + + return nil, err +} + func appendMessage(msg types.CompletionMessage, response openai.ChatCompletionStreamResponse) types.CompletionMessage { msg.Usage.CompletionTokens = types.FirstSet(msg.Usage.CompletionTokens, response.Usage.CompletionTokens) msg.Usage.PromptTokens = types.FirstSet(msg.Usage.PromptTokens, response.Usage.PromptTokens) diff --git a/pkg/openai/count.go b/pkg/openai/count.go index 47c5c9bd..ffd902e5 100644 --- a/pkg/openai/count.go +++ b/pkg/openai/count.go @@ -1,20 +1,30 @@ package openai -import openai "github.com/gptscript-ai/chat-completion-client" +import ( + openai "github.com/gptscript-ai/chat-completion-client" +) + +const DefaultMaxTokens = 128_000 + +func decreaseTenPercent(maxTokens int) int { + maxTokens = getBudget(maxTokens) + return int(float64(maxTokens) * 0.9) +} + +func getBudget(maxTokens int) int { + if maxTokens == 0 { + return DefaultMaxTokens + } + return maxTokens +} func dropMessagesOverCount(maxTokens int, msgs []openai.ChatCompletionMessage) (result []openai.ChatCompletionMessage) { var ( lastSystem int withinBudget int - budget = maxTokens + budget = getBudget(maxTokens) ) - if maxTokens == 0 { - budget = 300_000 - } else { - budget *= 3 - } - for i, msg := range msgs { if msg.Role == openai.ChatMessageRoleSystem { budget -= countMessage(msg) @@ -33,6 +43,14 @@ func dropMessagesOverCount(maxTokens int, msgs []openai.ChatCompletionMessage) ( } } + // OpenAI gets upset if there is a tool message without a tool call preceding it. + // Check the oldest message within budget, and if it is a tool message, just drop it. + // We do this in a loop because it is possible for multiple tool messages to be in a row, + // due to parallel tool calls. + for withinBudget < len(msgs) && msgs[withinBudget].Role == openai.ChatMessageRoleTool { + withinBudget++ + } + if withinBudget == len(msgs)-1 { // We are going to drop all non system messages, which seems useless, so just return them // all and let it fail From 1d2b70cf492504c15cc1aab1cd5176f2f1c78888 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Tue, 3 Sep 2024 11:29:13 -0400 Subject: [PATCH 58/83] chore: sys.read: improve description (#834) Signed-off-by: Grant Linville --- pkg/builtin/builtin.go | 2 +- pkg/tests/testdata/TestToolsChange/call1.golden | 2 +- pkg/tests/testdata/TestToolsChange/step1.golden | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/builtin/builtin.go b/pkg/builtin/builtin.go index f972d14c..f8f44ba9 100644 --- a/pkg/builtin/builtin.go +++ b/pkg/builtin/builtin.go @@ -59,7 +59,7 @@ var tools = map[string]types.Tool{ "sys.read": { ToolDef: types.ToolDef{ Parameters: types.Parameters{ - Description: "Reads the contents of a file", + Description: "Reads the contents of a file. Can only read plain text files, not binary files", Arguments: types.ObjectSchema( "filename", "The name of the file to read"), }, diff --git a/pkg/tests/testdata/TestToolsChange/call1.golden b/pkg/tests/testdata/TestToolsChange/call1.golden index 6c7c2d55..69ab3d03 100644 --- a/pkg/tests/testdata/TestToolsChange/call1.golden +++ b/pkg/tests/testdata/TestToolsChange/call1.golden @@ -22,7 +22,7 @@ "function": { "toolID": "sys.read", "name": "read", - "description": "Reads the contents of a file", + "description": "Reads the contents of a file. Can only read plain text files, not binary files", "parameters": { "properties": { "filename": { diff --git a/pkg/tests/testdata/TestToolsChange/step1.golden b/pkg/tests/testdata/TestToolsChange/step1.golden index 1aae05d1..e26862ae 100644 --- a/pkg/tests/testdata/TestToolsChange/step1.golden +++ b/pkg/tests/testdata/TestToolsChange/step1.golden @@ -30,7 +30,7 @@ "function": { "toolID": "sys.read", "name": "read", - "description": "Reads the contents of a file", + "description": "Reads the contents of a file. Can only read plain text files, not binary files", "parameters": { "properties": { "filename": { From eaaf0cdb72cae37f84ce600bb49ae1f49fc86be9 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Tue, 3 Sep 2024 12:28:49 -0400 Subject: [PATCH 59/83] fix: sys.read: never read files that contain a null byte (#837) Signed-off-by: Grant Linville --- pkg/builtin/builtin.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/builtin/builtin.go b/pkg/builtin/builtin.go index f8f44ba9..c17f2c80 100644 --- a/pkg/builtin/builtin.go +++ b/pkg/builtin/builtin.go @@ -489,6 +489,12 @@ func SysRead(_ context.Context, _ []string, input string, _ chan<- string) (stri if len(data) == 0 { return fmt.Sprintf("The file %s has no contents", params.Filename), nil } + + // Assume the file is not text if it contains a null byte + if bytes.IndexByte(data, 0) != -1 { + return fmt.Sprintf("The file %s cannot be read because it is not a plaintext file", params.Filename), nil + } + return string(data), nil } From 6d92974e0c1c976a023a4b2e240c2e1e990c8212 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Thu, 5 Sep 2024 20:42:18 -0700 Subject: [PATCH 60/83] bug: "share tools:" should support all tool types, not just tool --- pkg/types/tool.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pkg/types/tool.go b/pkg/types/tool.go index d9d59837..f10788b4 100644 --- a/pkg/types/tool.go +++ b/pkg/types/tool.go @@ -602,13 +602,27 @@ func (t Tool) GetToolsByType(prg *Program, toolType ToolType) ([]ToolReference, case ToolTypeInput: exportRefs = tool.ExportInputFilters case ToolTypeTool: - exportRefs = tool.Export case ToolTypeCredential: exportRefs = tool.ExportCredentials default: return nil, fmt.Errorf("unknown tool type %v", toolType) } toolSet.AddAll(tool.GetToolRefsFromNames(exportRefs)) + + toolRefs, err := tool.GetToolRefsFromNames(tool.Export) + if err != nil { + return nil, err + } + + for _, toolRef := range toolRefs { + tool, ok := prg.ToolSet[toolRef.ToolID] + if !ok { + continue + } + if slices.Contains(toolsListFilterType, tool.Type) { + toolSet.Add(toolRef) + } + } } return toolSet.List() From d97d564e243bafc2a495c21a363202a759c28330 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Thu, 5 Sep 2024 20:17:58 -0400 Subject: [PATCH 61/83] improve error messages Signed-off-by: Grant Linville --- pkg/engine/openapi.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/engine/openapi.go b/pkg/engine/openapi.go index 0bd5f599..a951bd37 100644 --- a/pkg/engine/openapi.go +++ b/pkg/engine/openapi.go @@ -66,7 +66,7 @@ func (e *Engine) runOpenAPIRevamp(tool types.Tool, input string) (*Return, error } else if !match { // Report to the LLM that the operation was not found return &Return{ - Result: ptr(fmt.Sprintf("operation %s not found", operation)), + Result: ptr(fmt.Sprintf("ERROR: operation %s not found", operation)), }, nil } } @@ -92,7 +92,7 @@ func (e *Engine) runOpenAPIRevamp(tool types.Tool, input string) (*Return, error if !found { // Report to the LLM that the operation was not found return &Return{ - Result: ptr(fmt.Sprintf("operation %s not found", operation)), + Result: ptr(fmt.Sprintf("ERROR: operation %s not found", operation)), }, nil } @@ -115,7 +115,7 @@ func (e *Engine) runOpenAPIRevamp(tool types.Tool, input string) (*Return, error } else if !match { // Report to the LLM that the operation was not found return &Return{ - Result: ptr(fmt.Sprintf("operation %s not found", operation)), + Result: ptr(fmt.Sprintf("ERROR: operation %s not found", operation)), }, nil } } @@ -140,7 +140,7 @@ func (e *Engine) runOpenAPIRevamp(tool types.Tool, input string) (*Return, error } else if !found { // Report to the LLM that the operation was not found return &Return{ - Result: ptr(fmt.Sprintf("operation %s not found", operation)), + Result: ptr(fmt.Sprintf("ERROR: operation %s not found", operation)), }, nil } From 8128bbc5bc2ee65503829022c0e2e9bc320f1715 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Thu, 5 Sep 2024 20:49:13 -0700 Subject: [PATCH 62/83] bug: always prefer tool's given name over the referenced name --- pkg/tests/testdata/TestCase2/call1.golden | 2 +- pkg/types/tool.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/tests/testdata/TestCase2/call1.golden b/pkg/tests/testdata/TestCase2/call1.golden index 581e03f5..d9b446d0 100644 --- a/pkg/tests/testdata/TestCase2/call1.golden +++ b/pkg/tests/testdata/TestCase2/call1.golden @@ -4,7 +4,7 @@ { "function": { "toolID": "testdata/TestCase2/test.gpt:bob", - "name": "Bob", + "name": "bob", "description": "I'm Bob, a friendly guy.", "parameters": { "properties": { diff --git a/pkg/types/tool.go b/pkg/types/tool.go index f10788b4..0bd7bc02 100644 --- a/pkg/types/tool.go +++ b/pkg/types/tool.go @@ -698,7 +698,10 @@ func toolRefsToCompletionTools(completionTools []ToolReference, prg Program) (re for _, subToolRef := range completionTools { subTool := prg.ToolSet[subToolRef.ToolID] - subToolName := subToolRef.Reference + subToolName := subTool.Name + if subToolName == "" { + subToolName = subToolRef.Reference + } if subToolRef.Named != "" { subToolName = subToolRef.Named } From b8f6209f679751d83009a50d0a0137459765368f Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Fri, 6 Sep 2024 12:11:52 -0400 Subject: [PATCH 63/83] fix: allocate new storage for env vars on each tool call (#841) Signed-off-by: Grant Linville --- pkg/engine/cmd.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/engine/cmd.go b/pkg/engine/cmd.go index 960bcfe8..c7d21a2b 100644 --- a/pkg/engine/cmd.go +++ b/pkg/engine/cmd.go @@ -217,30 +217,34 @@ func appendInputAsEnv(env []string, input string) []string { dec := json.NewDecoder(bytes.NewReader([]byte(input))) dec.UseNumber() - env = appendEnv(env, "GPTSCRIPT_INPUT", input) + // If we don't create a new slice here, then parallel tool calls can end up getting messed up. + newEnv := make([]string, len(env), cap(env)+1+len(data)) + copy(newEnv, env) + + newEnv = appendEnv(newEnv, "GPTSCRIPT_INPUT", input) if err := json.Unmarshal([]byte(input), &data); err != nil { // ignore invalid JSON - return env + return newEnv } for k, v := range data { switch val := v.(type) { case string: - env = appendEnv(env, k, val) + newEnv = appendEnv(newEnv, k, val) case json.Number: - env = appendEnv(env, k, string(val)) + newEnv = appendEnv(newEnv, k, string(val)) case bool: - env = appendEnv(env, k, fmt.Sprint(val)) + newEnv = appendEnv(newEnv, k, fmt.Sprint(val)) default: data, err := json.Marshal(val) if err == nil { - env = appendEnv(env, k, string(data)) + newEnv = appendEnv(newEnv, k, string(data)) } } } - return env + return newEnv } func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.Tool, input string, useShell bool) (*exec.Cmd, func(), error) { @@ -248,7 +252,7 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T useShell = false } - envvars := append(e.Env[:], extraEnv...) + envvars := append(e.Env, extraEnv...) envvars = appendInputAsEnv(envvars, input) if log.IsDebug() { envvars = append(envvars, "GPTSCRIPT_DEBUG=true") From c441cb4cf785ba51c0b61b862b118ed7dc46e64a Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Fri, 6 Sep 2024 15:18:56 -0400 Subject: [PATCH 64/83] fix: openapi revamp: return error to LLM if args are invalid JSON (#843) Signed-off-by: Grant Linville Co-authored-by: Tyler Slaton <54378333+tylerslaton@users.noreply.github.com> --- pkg/openapi/run.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/openapi/run.go b/pkg/openapi/run.go index fb3b746c..159ee6fe 100644 --- a/pkg/openapi/run.go +++ b/pkg/openapi/run.go @@ -38,7 +38,8 @@ func Run(operationID, defaultHost, args string, t *openapi3.T, envs []string) (s // Validate args against the schema. validationResult, err := gojsonschema.Validate(gojsonschema.NewStringLoader(schemaJSON), gojsonschema.NewStringLoader(args)) if err != nil { - return "", false, err + // We don't return an error here because we want the LLM to be able to maintain control and try again. + return fmt.Sprintf("ERROR: failed to validate arguments. Make sure your arguments are valid JSON. %v", err), false, nil } if !validationResult.Valid() { From dbf46d154265e31889ab9fd082ef252b738b69b9 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Mon, 9 Sep 2024 11:12:15 -0400 Subject: [PATCH 65/83] fix: openapi revamp: fix incorrect error message when JSON args are invalid (#844) Signed-off-by: Grant Linville --- pkg/openapi/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/openapi/run.go b/pkg/openapi/run.go index 159ee6fe..ac1ec660 100644 --- a/pkg/openapi/run.go +++ b/pkg/openapi/run.go @@ -39,7 +39,7 @@ func Run(operationID, defaultHost, args string, t *openapi3.T, envs []string) (s validationResult, err := gojsonschema.Validate(gojsonschema.NewStringLoader(schemaJSON), gojsonschema.NewStringLoader(args)) if err != nil { // We don't return an error here because we want the LLM to be able to maintain control and try again. - return fmt.Sprintf("ERROR: failed to validate arguments. Make sure your arguments are valid JSON. %v", err), false, nil + return fmt.Sprintf("ERROR: failed to validate arguments. Make sure your arguments are valid JSON. %v", err), true, nil } if !validationResult.Valid() { From c0f116cf9b455658861fabbbb45fefebc9ed4513 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Wed, 11 Sep 2024 16:32:16 -0400 Subject: [PATCH 66/83] chore: bubble up errors when downloading credential helper fails (#845) Signed-off-by: Donnie Adams --- pkg/repos/get.go | 5 ++++ pkg/repos/runtimes/golang/golang.go | 37 ++++++++++++++++------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/pkg/repos/get.go b/pkg/repos/get.go index 8981d1fa..8346b8cf 100644 --- a/pkg/repos/get.go +++ b/pkg/repos/get.go @@ -146,6 +146,11 @@ func (m *Manager) deferredSetUpCredentialHelpers(ctx context.Context, cliCfg *co } tool := types.Tool{ + ToolDef: types.ToolDef{ + Parameters: types.Parameters{ + Name: "gptscript-credential-helpers", + }, + }, Source: types.ToolSource{ Repo: &types.Repo{ Root: runtimeEnv.VarOrDefault("GPTSCRIPT_CRED_HELPERS_ROOT", "https://github.com/gptscript-ai/gptscript-credential-helpers.git"), diff --git a/pkg/repos/runtimes/golang/golang.go b/pkg/repos/runtimes/golang/golang.go index 2601f521..47e8461f 100644 --- a/pkg/repos/runtimes/golang/golang.go +++ b/pkg/repos/runtimes/golang/golang.go @@ -97,21 +97,26 @@ type tag struct { } func GetLatestTag(tool types.Tool) (string, error) { - r, ok := getLatestRelease(tool) + r, ok, err := getLatestRelease(tool) + if err != nil { + return "", err + } + if !ok { return "", fmt.Errorf("failed to get latest release for %s", tool.Name) } + return r.label, nil } -func getLatestRelease(tool types.Tool) (*release, bool) { +func getLatestRelease(tool types.Tool) (*release, bool, error) { if tool.Source.Repo == nil || !strings.HasPrefix(tool.Source.Repo.Root, "https://github.com/") { - return nil, false + return nil, false, nil } parts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(tool.Source.Repo.Root, ".git"), "https://"), "/") if len(parts) != 3 { - return nil, false + return nil, false, fmt.Errorf("invalid GitHub URL: %s", tool.Source.Repo.Root) } client := http.Client{ @@ -124,17 +129,16 @@ func getLatestRelease(tool types.Tool) (*release, bool) { resp, err := client.Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/tags", account, repo)) if err != nil { - // ignore error - return nil, false + return nil, false, fmt.Errorf("failed to get tags: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, false + return nil, false, fmt.Errorf("unexpected status when getting tags: %s", resp.Status) } var tags []tag if err := json.NewDecoder(resp.Body).Decode(&tags); err != nil { - return nil, false + return nil, false, fmt.Errorf("failed to decode GitHub tags: %w", err) } for _, tag := range tags { if tag.Commit.Sha == tool.Source.Repo.Revision { @@ -142,23 +146,22 @@ func getLatestRelease(tool types.Tool) (*release, bool) { account: account, repo: repo, label: tag.Name, - }, true + }, true, nil } } resp, err = client.Get(fmt.Sprintf("https://github.com/%s/%s/releases/latest", account, repo)) if err != nil { - // ignore error - return nil, false + return nil, false, fmt.Errorf("failed to get latest release: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusFound { - return nil, false + return nil, false, fmt.Errorf("unexpected status when getting latest release: %s", resp.Status) } target := resp.Header.Get("Location") if target == "" { - return nil, false + return nil, false, nil } parts = strings.Split(target, "/") @@ -168,7 +171,7 @@ func getLatestRelease(tool types.Tool) (*release, bool) { account: account, repo: repo, label: label, - }, true + }, true, nil } func get(ctx context.Context, url string) (*http.Response, error) { @@ -249,7 +252,8 @@ func (r *Runtime) Binary(ctx context.Context, tool types.Tool, _, toolSource str return false, nil, nil } - rel, ok := getLatestRelease(tool) + // ignore the error + rel, ok, _ := getLatestRelease(tool) if !ok { return false, nil, nil } @@ -286,7 +290,8 @@ func (r *Runtime) DownloadCredentialHelper(ctx context.Context, tool types.Tool, return nil } - rel, ok := getLatestRelease(tool) + // ignore the error + rel, ok, _ := getLatestRelease(tool) if !ok { return fmt.Errorf("failed to find %s release", r.ID()) } From 90e7868a95cb4501bfdf706a64d06245e3c55f5c Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Fri, 13 Sep 2024 12:57:17 -0400 Subject: [PATCH 67/83] feat: sdkserver: add credential routes (#846) Signed-off-by: Grant Linville --- pkg/cli/credential.go | 2 +- pkg/credentials/store.go | 9 +- pkg/gptscript/gptscript.go | 2 +- pkg/sdkserver/credentials.go | 176 +++++++++++++++++++++++++++++++++++ pkg/sdkserver/routes.go | 5 + pkg/sdkserver/types.go | 7 ++ 6 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 pkg/sdkserver/credentials.go diff --git a/pkg/cli/credential.go b/pkg/cli/credential.go index cb000125..733590c4 100644 --- a/pkg/cli/credential.go +++ b/pkg/cli/credential.go @@ -45,7 +45,7 @@ func (c *Credential) Run(cmd *cobra.Command, _ []string) error { ctx := c.root.CredentialContext if c.AllContexts { - ctx = "*" + ctx = credentials.AllCredentialContexts } opts, err := c.root.NewGPTScriptOpts() diff --git a/pkg/credentials/store.go b/pkg/credentials/store.go index 3940184b..c8558f3a 100644 --- a/pkg/credentials/store.go +++ b/pkg/credentials/store.go @@ -11,6 +11,11 @@ import ( "github.com/gptscript-ai/gptscript/pkg/config" ) +const ( + DefaultCredentialContext = "default" + AllCredentialContexts = "*" +) + type CredentialBuilder interface { EnsureCredentialHelpers(ctx context.Context) error } @@ -105,7 +110,7 @@ func (s Store) List(ctx context.Context) ([]Credential, error) { if err != nil { return nil, err } - if s.credCtx == "*" || c.Context == s.credCtx { + if s.credCtx == AllCredentialContexts || c.Context == s.credCtx { creds = append(creds, c) } } @@ -139,7 +144,7 @@ func validateCredentialCtx(ctx string) error { return fmt.Errorf("credential context cannot be empty") } - if ctx == "*" { // this represents "all contexts" and is allowed + if ctx == AllCredentialContexts { return nil } diff --git a/pkg/gptscript/gptscript.go b/pkg/gptscript/gptscript.go index 755fe632..abae80ac 100644 --- a/pkg/gptscript/gptscript.go +++ b/pkg/gptscript/gptscript.go @@ -75,7 +75,7 @@ func Complete(opts ...Options) Options { result.Env = os.Environ() } if result.CredentialContext == "" { - result.CredentialContext = "default" + result.CredentialContext = credentials.DefaultCredentialContext } return result diff --git a/pkg/sdkserver/credentials.go b/pkg/sdkserver/credentials.go new file mode 100644 index 00000000..adbaacdc --- /dev/null +++ b/pkg/sdkserver/credentials.go @@ -0,0 +1,176 @@ +package sdkserver + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/gptscript-ai/gptscript/pkg/config" + gcontext "github.com/gptscript-ai/gptscript/pkg/context" + "github.com/gptscript-ai/gptscript/pkg/credentials" + "github.com/gptscript-ai/gptscript/pkg/repos/runtimes" +) + +func (s *server) initializeCredentialStore(ctx string) (credentials.CredentialStore, error) { + cfg, err := config.ReadCLIConfig(s.gptscriptOpts.OpenAI.ConfigFile) + if err != nil { + return nil, fmt.Errorf("failed to read CLI config: %w", err) + } + + // TODO - are we sure we want to always use runtimes.Default here? + store, err := credentials.NewStore(cfg, runtimes.Default(s.gptscriptOpts.Cache.CacheDir), ctx, s.gptscriptOpts.Cache.CacheDir) + if err != nil { + return nil, fmt.Errorf("failed to initialize credential store: %w", err) + } + + return store, nil +} + +func (s *server) listCredentials(w http.ResponseWriter, r *http.Request) { + logger := gcontext.GetLogger(r.Context()) + req := new(credentialsRequest) + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + writeError(logger, w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err)) + return + } + + if req.AllContexts { + req.Context = credentials.AllCredentialContexts + } else if req.Context == "" { + req.Context = credentials.DefaultCredentialContext + } + + store, err := s.initializeCredentialStore(req.Context) + if err != nil { + writeError(logger, w, http.StatusInternalServerError, err) + return + } + + creds, err := store.List(r.Context()) + if err != nil { + writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to list credentials: %w", err)) + return + } + + // Remove the environment variable values (which are secrets) and refresh tokens from the response. + for i := range creds { + for k := range creds[i].Env { + creds[i].Env[k] = "" + } + creds[i].RefreshToken = "" + } + + writeResponse(logger, w, map[string]any{"stdout": creds}) +} + +func (s *server) createCredential(w http.ResponseWriter, r *http.Request) { + logger := gcontext.GetLogger(r.Context()) + req := new(credentialsRequest) + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + writeError(logger, w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err)) + return + } + + cred := new(credentials.Credential) + if err := json.Unmarshal([]byte(req.Content), cred); err != nil { + writeError(logger, w, http.StatusBadRequest, fmt.Errorf("invalid credential: %w", err)) + return + } + + if cred.Context == "" { + cred.Context = credentials.DefaultCredentialContext + } + + store, err := s.initializeCredentialStore(cred.Context) + if err != nil { + writeError(logger, w, http.StatusInternalServerError, err) + return + } + + if err := store.Add(r.Context(), *cred); err != nil { + writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to create credential: %w", err)) + return + } + + writeResponse(logger, w, map[string]any{"stdout": "Credential created successfully"}) +} + +func (s *server) revealCredential(w http.ResponseWriter, r *http.Request) { + logger := gcontext.GetLogger(r.Context()) + req := new(credentialsRequest) + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + writeError(logger, w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err)) + return + } + + if req.Name == "" { + writeError(logger, w, http.StatusBadRequest, fmt.Errorf("missing credential name")) + return + } + + if req.AllContexts || req.Context == credentials.AllCredentialContexts { + writeError(logger, w, http.StatusBadRequest, fmt.Errorf("allContexts is not supported for credential retrieval; please specify the specific context that the credential is in")) + return + } else if req.Context == "" { + req.Context = credentials.DefaultCredentialContext + } + + store, err := s.initializeCredentialStore(req.Context) + if err != nil { + writeError(logger, w, http.StatusInternalServerError, err) + return + } + + cred, ok, err := store.Get(r.Context(), req.Name) + if err != nil { + writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to get credential: %w", err)) + return + } else if !ok { + writeError(logger, w, http.StatusNotFound, fmt.Errorf("credential not found")) + return + } + + writeResponse(logger, w, map[string]any{"stdout": cred}) +} + +func (s *server) deleteCredential(w http.ResponseWriter, r *http.Request) { + logger := gcontext.GetLogger(r.Context()) + req := new(credentialsRequest) + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + writeError(logger, w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err)) + } + + if req.Name == "" { + writeError(logger, w, http.StatusBadRequest, fmt.Errorf("missing credential name")) + return + } + + if req.AllContexts || req.Context == credentials.AllCredentialContexts { + writeError(logger, w, http.StatusBadRequest, fmt.Errorf("allContexts is not supported for credential deletion; please specify the specific context that the credential is in")) + return + } else if req.Context == "" { + req.Context = credentials.DefaultCredentialContext + } + + store, err := s.initializeCredentialStore(req.Context) + if err != nil { + writeError(logger, w, http.StatusInternalServerError, err) + return + } + + // Check to see if a cred exists so we can return a 404 if it doesn't. + if _, ok, err := store.Get(r.Context(), req.Name); err != nil { + writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to get credential: %w", err)) + return + } else if !ok { + writeError(logger, w, http.StatusNotFound, fmt.Errorf("credential not found")) + return + } + + if err := store.Remove(r.Context(), req.Name); err != nil { + writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to delete credential: %w", err)) + return + } + + writeResponse(logger, w, map[string]any{"stdout": "Credential deleted successfully"}) +} diff --git a/pkg/sdkserver/routes.go b/pkg/sdkserver/routes.go index 6cb1e620..c180097e 100644 --- a/pkg/sdkserver/routes.go +++ b/pkg/sdkserver/routes.go @@ -58,6 +58,11 @@ func (s *server) addRoutes(mux *http.ServeMux) { mux.HandleFunc("POST /confirm/{id}", s.confirm) mux.HandleFunc("POST /prompt/{id}", s.prompt) mux.HandleFunc("POST /prompt-response/{id}", s.promptResponse) + + mux.HandleFunc("POST /credentials", s.listCredentials) + mux.HandleFunc("POST /credentials/create", s.createCredential) + mux.HandleFunc("POST /credentials/reveal", s.revealCredential) + mux.HandleFunc("POST /credentials/delete", s.deleteCredential) } // health just provides an endpoint for checking whether the server is running and accessible. diff --git a/pkg/sdkserver/types.go b/pkg/sdkserver/types.go index 2889626b..7ed7da78 100644 --- a/pkg/sdkserver/types.go +++ b/pkg/sdkserver/types.go @@ -252,3 +252,10 @@ type prompt struct { Type runner.EventType `json:"type,omitempty"` Time time.Time `json:"time,omitempty"` } + +type credentialsRequest struct { + content `json:",inline"` + AllContexts bool `json:"allContexts"` + Context string `json:"context"` + Name string `json:"name"` +} From 45d444f810600d9584951ec3aba98da9b5e02db5 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Fri, 13 Sep 2024 17:30:45 -0400 Subject: [PATCH 68/83] fix: sdkserver: credentials: ensure credential helpers exist (#848) Signed-off-by: Grant Linville --- pkg/sdkserver/credentials.go | 22 ++++++++++++++-------- pkg/sdkserver/routes.go | 3 +++ pkg/sdkserver/server.go | 2 ++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/pkg/sdkserver/credentials.go b/pkg/sdkserver/credentials.go index adbaacdc..d3f86b1f 100644 --- a/pkg/sdkserver/credentials.go +++ b/pkg/sdkserver/credentials.go @@ -1,6 +1,7 @@ package sdkserver import ( + "context" "encoding/json" "fmt" "net/http" @@ -8,17 +9,22 @@ import ( "github.com/gptscript-ai/gptscript/pkg/config" gcontext "github.com/gptscript-ai/gptscript/pkg/context" "github.com/gptscript-ai/gptscript/pkg/credentials" - "github.com/gptscript-ai/gptscript/pkg/repos/runtimes" ) -func (s *server) initializeCredentialStore(ctx string) (credentials.CredentialStore, error) { +func (s *server) initializeCredentialStore(ctx context.Context, credCtx string) (credentials.CredentialStore, error) { cfg, err := config.ReadCLIConfig(s.gptscriptOpts.OpenAI.ConfigFile) if err != nil { return nil, fmt.Errorf("failed to read CLI config: %w", err) } - // TODO - are we sure we want to always use runtimes.Default here? - store, err := credentials.NewStore(cfg, runtimes.Default(s.gptscriptOpts.Cache.CacheDir), ctx, s.gptscriptOpts.Cache.CacheDir) + if err := s.runtimeManager.SetUpCredentialHelpers(ctx, cfg); err != nil { + return nil, fmt.Errorf("failed to set up credential helpers: %w", err) + } + if err := s.runtimeManager.EnsureCredentialHelpers(ctx); err != nil { + return nil, fmt.Errorf("failed to ensure credential helpers: %w", err) + } + + store, err := credentials.NewStore(cfg, s.runtimeManager, credCtx, s.gptscriptOpts.Cache.CacheDir) if err != nil { return nil, fmt.Errorf("failed to initialize credential store: %w", err) } @@ -40,7 +46,7 @@ func (s *server) listCredentials(w http.ResponseWriter, r *http.Request) { req.Context = credentials.DefaultCredentialContext } - store, err := s.initializeCredentialStore(req.Context) + store, err := s.initializeCredentialStore(r.Context(), req.Context) if err != nil { writeError(logger, w, http.StatusInternalServerError, err) return @@ -81,7 +87,7 @@ func (s *server) createCredential(w http.ResponseWriter, r *http.Request) { cred.Context = credentials.DefaultCredentialContext } - store, err := s.initializeCredentialStore(cred.Context) + store, err := s.initializeCredentialStore(r.Context(), cred.Context) if err != nil { writeError(logger, w, http.StatusInternalServerError, err) return @@ -115,7 +121,7 @@ func (s *server) revealCredential(w http.ResponseWriter, r *http.Request) { req.Context = credentials.DefaultCredentialContext } - store, err := s.initializeCredentialStore(req.Context) + store, err := s.initializeCredentialStore(r.Context(), req.Context) if err != nil { writeError(logger, w, http.StatusInternalServerError, err) return @@ -152,7 +158,7 @@ func (s *server) deleteCredential(w http.ResponseWriter, r *http.Request) { req.Context = credentials.DefaultCredentialContext } - store, err := s.initializeCredentialStore(req.Context) + store, err := s.initializeCredentialStore(r.Context(), req.Context) if err != nil { writeError(logger, w, http.StatusInternalServerError, err) return diff --git a/pkg/sdkserver/routes.go b/pkg/sdkserver/routes.go index c180097e..f82fa8a7 100644 --- a/pkg/sdkserver/routes.go +++ b/pkg/sdkserver/routes.go @@ -13,6 +13,7 @@ import ( "github.com/gptscript-ai/broadcaster" "github.com/gptscript-ai/gptscript/pkg/cache" gcontext "github.com/gptscript-ai/gptscript/pkg/context" + "github.com/gptscript-ai/gptscript/pkg/engine" "github.com/gptscript-ai/gptscript/pkg/gptscript" "github.com/gptscript-ai/gptscript/pkg/input" "github.com/gptscript-ai/gptscript/pkg/loader" @@ -30,6 +31,8 @@ type server struct { client *gptscript.GPTScript events *broadcaster.Broadcaster[event] + runtimeManager engine.RuntimeManager + lock sync.RWMutex waitingToConfirm map[string]chan runner.AuthorizerResponse waitingToPrompt map[string]chan map[string]string diff --git a/pkg/sdkserver/server.go b/pkg/sdkserver/server.go index f72e7ae9..0a68f0fa 100644 --- a/pkg/sdkserver/server.go +++ b/pkg/sdkserver/server.go @@ -17,6 +17,7 @@ import ( "github.com/gptscript-ai/broadcaster" "github.com/gptscript-ai/gptscript/pkg/gptscript" "github.com/gptscript-ai/gptscript/pkg/mvl" + "github.com/gptscript-ai/gptscript/pkg/repos/runtimes" "github.com/gptscript-ai/gptscript/pkg/runner" "github.com/gptscript-ai/gptscript/pkg/types" "github.com/rs/cors" @@ -108,6 +109,7 @@ func run(ctx context.Context, listener net.Listener, opts Options) error { token: token, client: g, events: events, + runtimeManager: runtimes.Default(opts.Options.Cache.CacheDir), // TODO - do we always want to use runtimes.Default here? waitingToConfirm: make(map[string]chan runner.AuthorizerResponse), waitingToPrompt: make(map[string]chan map[string]string), } From 780e07eaeb409a2b47b13d6b4d7de9c2e7d38e8f Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Wed, 18 Sep 2024 09:19:30 -0400 Subject: [PATCH 69/83] feat: add stacked credential contexts (#849) Signed-off-by: Grant Linville --- docs/docs/03-tools/04-credential-tools.md | 52 +++++++++ .../04-command-line-reference/gptscript.md | 2 +- .../gptscript_credential.md | 2 +- .../gptscript_credential_delete.md | 2 +- .../gptscript_credential_show.md | 2 +- .../gptscript_eval.md | 2 +- .../gptscript_fmt.md | 2 +- .../gptscript_getenv.md | 2 +- .../gptscript_parse.md | 2 +- integration/cred_test.go | 54 +++++++++ integration/scripts/cred_stacked.gpt | 36 ++++++ pkg/cli/credential.go | 22 ++-- pkg/cli/credential_delete.go | 8 +- pkg/cli/credential_show.go | 8 +- pkg/cli/gptscript.go | 4 +- pkg/credentials/store.go | 104 ++++++++++++++---- pkg/credentials/util.go | 7 ++ pkg/gptscript/gptscript.go | 10 +- pkg/sdkserver/credentials.go | 25 +++-- pkg/sdkserver/routes.go | 10 +- pkg/sdkserver/types.go | 8 +- 21 files changed, 283 insertions(+), 81 deletions(-) create mode 100644 integration/scripts/cred_stacked.gpt diff --git a/docs/docs/03-tools/04-credential-tools.md b/docs/docs/03-tools/04-credential-tools.md index 1911dc34..46a0e69e 100644 --- a/docs/docs/03-tools/04-credential-tools.md +++ b/docs/docs/03-tools/04-credential-tools.md @@ -222,3 +222,55 @@ import os print("myCred expires at " + os.getenv("GPTSCRIPT_CREDENTIAL_EXPIRATION", "")) ``` + +## Stacked Credential Contexts (Advanced) + +When setting the `--credential-context` argument in GPTScript, you can specify multiple contexts separated by commas. +We refer to this as "stacked credential contexts", or just stacked contexts for short. This allows you to specify an order +of priority for credential contexts. This is best explained by example. + +### Example: stacked contexts when running a script that uses a credential + +Let's say you have two contexts, `one` and `two`, and you specify them like this: + +```bash +gptscript --credential-context one,two my-script.gpt +``` + +``` +Credential: my-credential-tool.gpt as myCred + + +``` + +When GPTScript runs, it will first look for a credential called `myCred` in the `one` context. +If it doesn't find it there, it will look for it in the `two` context. If it also doesn't find it there, +it will run the `my-credential-tool.gpt` tool to get the credential. It will then store the new credential into the `one` +context, since that has the highest priority. + +### Example: stacked contexts when listing credentials + +```bash +gptscript --credential-context one,two credentials +``` + +When you list credentials like this, GPTScript will print out the information for all credentials in contexts one and two, +with one exception. If there is a credential name that exists in both contexts, GPTScript will only print the information +for the credential in the context with the highest priority, which in this case is `one`. + +(To see all credentials in all contexts, you can still use the `--all-contexts` flag, and it will show all credentials, +regardless of whether the same name appears in another context.) + +### Example: stacked contexts when showing credentials + +```bash +gptscript --credential-context one,two credential show myCred +``` + +When you show a credential like this, GPTScript will first look for `myCred` in the `one` context. If it doesn't find it +there, it will look for it in the `two` context. If it doesn't find it in either context, it will print an error message. + +:::note +You cannot specify stacked contexts when doing `gptscript credential delete`. GPTScript will return an error if +more than one context is specified for this command. +::: diff --git a/docs/docs/04-command-line-reference/gptscript.md b/docs/docs/04-command-line-reference/gptscript.md index b7de5e86..8a726c64 100644 --- a/docs/docs/04-command-line-reference/gptscript.md +++ b/docs/docs/04-command-line-reference/gptscript.md @@ -18,7 +18,7 @@ gptscript [flags] PROGRAM_FILE [INPUT...] --color Use color in output (default true) ($GPTSCRIPT_COLOR) --config string Path to GPTScript config file ($GPTSCRIPT_CONFIG) --confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM) - --credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default") + --credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) --credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE) --debug Enable debug logging ($GPTSCRIPT_DEBUG) --debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES) diff --git a/docs/docs/04-command-line-reference/gptscript_credential.md b/docs/docs/04-command-line-reference/gptscript_credential.md index 435ba6e5..eb5781f4 100644 --- a/docs/docs/04-command-line-reference/gptscript_credential.md +++ b/docs/docs/04-command-line-reference/gptscript_credential.md @@ -20,7 +20,7 @@ gptscript credential [flags] ### Options inherited from parent commands ``` - --credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default") + --credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) ``` ### SEE ALSO diff --git a/docs/docs/04-command-line-reference/gptscript_credential_delete.md b/docs/docs/04-command-line-reference/gptscript_credential_delete.md index c2f78e88..c9cffdd3 100644 --- a/docs/docs/04-command-line-reference/gptscript_credential_delete.md +++ b/docs/docs/04-command-line-reference/gptscript_credential_delete.md @@ -18,7 +18,7 @@ gptscript credential delete [flags] ### Options inherited from parent commands ``` - --credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default") + --credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) ``` ### SEE ALSO diff --git a/docs/docs/04-command-line-reference/gptscript_credential_show.md b/docs/docs/04-command-line-reference/gptscript_credential_show.md index f5fb11af..f89df87a 100644 --- a/docs/docs/04-command-line-reference/gptscript_credential_show.md +++ b/docs/docs/04-command-line-reference/gptscript_credential_show.md @@ -18,7 +18,7 @@ gptscript credential show [flags] ### Options inherited from parent commands ``` - --credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default") + --credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) ``` ### SEE ALSO diff --git a/docs/docs/04-command-line-reference/gptscript_eval.md b/docs/docs/04-command-line-reference/gptscript_eval.md index ff9e6446..257cf609 100644 --- a/docs/docs/04-command-line-reference/gptscript_eval.md +++ b/docs/docs/04-command-line-reference/gptscript_eval.md @@ -30,7 +30,7 @@ gptscript eval [flags] --color Use color in output (default true) ($GPTSCRIPT_COLOR) --config string Path to GPTScript config file ($GPTSCRIPT_CONFIG) --confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM) - --credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default") + --credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) --credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE) --debug Enable debug logging ($GPTSCRIPT_DEBUG) --debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES) diff --git a/docs/docs/04-command-line-reference/gptscript_fmt.md b/docs/docs/04-command-line-reference/gptscript_fmt.md index 7aceb957..1175a1f1 100644 --- a/docs/docs/04-command-line-reference/gptscript_fmt.md +++ b/docs/docs/04-command-line-reference/gptscript_fmt.md @@ -24,7 +24,7 @@ gptscript fmt [flags] --color Use color in output (default true) ($GPTSCRIPT_COLOR) --config string Path to GPTScript config file ($GPTSCRIPT_CONFIG) --confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM) - --credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default") + --credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) --credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE) --debug Enable debug logging ($GPTSCRIPT_DEBUG) --debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES) diff --git a/docs/docs/04-command-line-reference/gptscript_getenv.md b/docs/docs/04-command-line-reference/gptscript_getenv.md index 80fea614..4a688439 100644 --- a/docs/docs/04-command-line-reference/gptscript_getenv.md +++ b/docs/docs/04-command-line-reference/gptscript_getenv.md @@ -23,7 +23,7 @@ gptscript getenv [flags] KEY [DEFAULT] --color Use color in output (default true) ($GPTSCRIPT_COLOR) --config string Path to GPTScript config file ($GPTSCRIPT_CONFIG) --confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM) - --credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default") + --credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) --credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE) --debug Enable debug logging ($GPTSCRIPT_DEBUG) --debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES) diff --git a/docs/docs/04-command-line-reference/gptscript_parse.md b/docs/docs/04-command-line-reference/gptscript_parse.md index 3d84622b..66d2791c 100644 --- a/docs/docs/04-command-line-reference/gptscript_parse.md +++ b/docs/docs/04-command-line-reference/gptscript_parse.md @@ -24,7 +24,7 @@ gptscript parse [flags] --color Use color in output (default true) ($GPTSCRIPT_COLOR) --config string Path to GPTScript config file ($GPTSCRIPT_CONFIG) --confirm Prompt before running potentially dangerous commands ($GPTSCRIPT_CONFIRM) - --credential-context string Context name in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) (default "default") + --credential-context strings Context name(s) in which to store credentials ($GPTSCRIPT_CREDENTIAL_CONTEXT) --credential-override strings Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234) ($GPTSCRIPT_CREDENTIAL_OVERRIDE) --debug Enable debug logging ($GPTSCRIPT_DEBUG) --debug-messages Enable logging of chat completion calls ($GPTSCRIPT_DEBUG_MESSAGES) diff --git a/integration/cred_test.go b/integration/cred_test.go index d77f096c..1ea73d35 100644 --- a/integration/cred_test.go +++ b/integration/cred_test.go @@ -45,3 +45,57 @@ func TestCredentialExpirationEnv(t *testing.T) { } } } + +// TestStackedCredentialContexts tests creating, using, listing, showing, and deleting credentials when there are multiple contexts. +func TestStackedCredentialContexts(t *testing.T) { + // First, test credential creation. We will create a credential called testcred in two different contexts called one and two. + _, err := RunScript("scripts/cred_stacked.gpt", "--sub-tool", "testcred_one", "--credential-context", "one,two") + require.NoError(t, err) + + _, err = RunScript("scripts/cred_stacked.gpt", "--sub-tool", "testcred_two", "--credential-context", "two") + require.NoError(t, err) + + // Next, we try running the testcred_one tool. It should print the value of "testcred" in whichever context it finds the cred first. + out, err := RunScript("scripts/cred_stacked.gpt", "--sub-tool", "testcred_one", "--credential-context", "one,two") + require.NoError(t, err) + require.Contains(t, out, "one") + require.NotContains(t, out, "two") + + out, err = RunScript("scripts/cred_stacked.gpt", "--sub-tool", "testcred_one", "--credential-context", "two,one") + require.NoError(t, err) + require.Contains(t, out, "two") + require.NotContains(t, out, "one") + + // Next, list credentials and specify both contexts. We should get the credential from the first specified context. + out, err = GPTScriptExec("--credential-context", "one,two", "cred") + require.NoError(t, err) + require.Contains(t, out, "one") + require.NotContains(t, out, "two") + + out, err = GPTScriptExec("--credential-context", "two,one", "cred") + require.NoError(t, err) + require.Contains(t, out, "two") + require.NotContains(t, out, "one") + + // Next, try showing the credentials. + out, err = GPTScriptExec("--credential-context", "one,two", "cred", "show", "testcred") + require.NoError(t, err) + require.Contains(t, out, "one") + require.NotContains(t, out, "two") + + out, err = GPTScriptExec("--credential-context", "two,one", "cred", "show", "testcred") + require.NoError(t, err) + require.Contains(t, out, "two") + require.NotContains(t, out, "one") + + // Make sure we get an error if we try to delete a credential with multiple contexts specified. + _, err = GPTScriptExec("--credential-context", "one,two", "cred", "delete", "testcred") + require.Error(t, err) + + // Now actually delete the credentials. + _, err = GPTScriptExec("--credential-context", "one", "cred", "delete", "testcred") + require.NoError(t, err) + + _, err = GPTScriptExec("--credential-context", "two", "cred", "delete", "testcred") + require.NoError(t, err) +} diff --git a/integration/scripts/cred_stacked.gpt b/integration/scripts/cred_stacked.gpt new file mode 100644 index 00000000..1072ca7b --- /dev/null +++ b/integration/scripts/cred_stacked.gpt @@ -0,0 +1,36 @@ +name: testcred_one +credential: cred_one as testcred + +#!python3 + +import os + +print(os.environ.get("VALUE")) + +--- +name: testcred_two +credential: cred_two as testcred + +#!python3 + +import os + +print(os.environ.get("VALUE")) + +--- +name: cred_one + +#!python3 + +import json + +print(json.dumps({"env": {"VALUE": "one"}})) + +--- +name: cred_two + +#!python3 + +import json + +print(json.dumps({"env": {"VALUE": "two"}})) diff --git a/pkg/cli/credential.go b/pkg/cli/credential.go index 733590c4..674160b9 100644 --- a/pkg/cli/credential.go +++ b/pkg/cli/credential.go @@ -9,11 +9,10 @@ import ( "time" cmd2 "github.com/gptscript-ai/cmd" - "github.com/gptscript-ai/gptscript/pkg/cache" "github.com/gptscript-ai/gptscript/pkg/config" "github.com/gptscript-ai/gptscript/pkg/credentials" + "github.com/gptscript-ai/gptscript/pkg/gptscript" "github.com/gptscript-ai/gptscript/pkg/repos/runtimes" - "github.com/gptscript-ai/gptscript/pkg/runner" "github.com/spf13/cobra" ) @@ -43,27 +42,26 @@ func (c *Credential) Run(cmd *cobra.Command, _ []string) error { return fmt.Errorf("failed to read CLI config: %w", err) } - ctx := c.root.CredentialContext - if c.AllContexts { - ctx = credentials.AllCredentialContexts - } - opts, err := c.root.NewGPTScriptOpts() if err != nil { return err } - opts.Cache = cache.Complete(opts.Cache) - opts.Runner = runner.Complete(opts.Runner) + opts = gptscript.Complete(opts) if opts.Runner.RuntimeManager == nil { opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir) } + ctxs := opts.CredentialContexts + if c.AllContexts { + ctxs = []string{credentials.AllCredentialContexts} + } + if err = opts.Runner.RuntimeManager.SetUpCredentialHelpers(cmd.Context(), cfg); err != nil { return err } // Initialize the credential store and get all the credentials. - store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, ctx, opts.Cache.CacheDir) + store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, ctxs, opts.Cache.CacheDir) if err != nil { return fmt.Errorf("failed to get credentials store: %w", err) } @@ -77,7 +75,7 @@ func (c *Credential) Run(cmd *cobra.Command, _ []string) error { defer w.Flush() // Sort credentials and print column names, depending on the options. - if c.AllContexts { + if c.AllContexts || len(c.root.CredentialContext) > 1 { // Sort credentials by context sort.Slice(creds, func(i, j int) bool { if creds[i].Context == creds[j].Context { @@ -114,7 +112,7 @@ func (c *Credential) Run(cmd *cobra.Command, _ []string) error { } var fields []any - if c.AllContexts { + if c.AllContexts || len(c.root.CredentialContext) > 1 { fields = []any{cred.Context, cred.ToolName, expires} } else { fields = []any{cred.ToolName, expires} diff --git a/pkg/cli/credential_delete.go b/pkg/cli/credential_delete.go index 4e9919df..b17ae851 100644 --- a/pkg/cli/credential_delete.go +++ b/pkg/cli/credential_delete.go @@ -3,11 +3,10 @@ package cli import ( "fmt" - "github.com/gptscript-ai/gptscript/pkg/cache" "github.com/gptscript-ai/gptscript/pkg/config" "github.com/gptscript-ai/gptscript/pkg/credentials" + "github.com/gptscript-ai/gptscript/pkg/gptscript" "github.com/gptscript-ai/gptscript/pkg/repos/runtimes" - "github.com/gptscript-ai/gptscript/pkg/runner" "github.com/spf13/cobra" ) @@ -34,8 +33,7 @@ func (c *Delete) Run(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to read CLI config: %w", err) } - opts.Cache = cache.Complete(opts.Cache) - opts.Runner = runner.Complete(opts.Runner) + opts = gptscript.Complete(opts) if opts.Runner.RuntimeManager == nil { opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir) } @@ -44,7 +42,7 @@ func (c *Delete) Run(cmd *cobra.Command, args []string) error { return err } - store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, c.root.CredentialContext, opts.Cache.CacheDir) + store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, opts.CredentialContexts, opts.Cache.CacheDir) if err != nil { return fmt.Errorf("failed to get credentials store: %w", err) } diff --git a/pkg/cli/credential_show.go b/pkg/cli/credential_show.go index fac1b719..d8ea980b 100644 --- a/pkg/cli/credential_show.go +++ b/pkg/cli/credential_show.go @@ -5,11 +5,10 @@ import ( "os" "text/tabwriter" - "github.com/gptscript-ai/gptscript/pkg/cache" "github.com/gptscript-ai/gptscript/pkg/config" "github.com/gptscript-ai/gptscript/pkg/credentials" + "github.com/gptscript-ai/gptscript/pkg/gptscript" "github.com/gptscript-ai/gptscript/pkg/repos/runtimes" - "github.com/gptscript-ai/gptscript/pkg/runner" "github.com/spf13/cobra" ) @@ -36,8 +35,7 @@ func (c *Show) Run(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to read CLI config: %w", err) } - opts.Cache = cache.Complete(opts.Cache) - opts.Runner = runner.Complete(opts.Runner) + opts = gptscript.Complete(opts) if opts.Runner.RuntimeManager == nil { opts.Runner.RuntimeManager = runtimes.Default(opts.Cache.CacheDir) } @@ -46,7 +44,7 @@ func (c *Show) Run(cmd *cobra.Command, args []string) error { return err } - store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, c.root.CredentialContext, opts.Cache.CacheDir) + store, err := credentials.NewStore(cfg, opts.Runner.RuntimeManager, opts.CredentialContexts, opts.Cache.CacheDir) if err != nil { return fmt.Errorf("failed to get credentials store: %w", err) } diff --git a/pkg/cli/gptscript.go b/pkg/cli/gptscript.go index 2d7e90d9..66719adc 100644 --- a/pkg/cli/gptscript.go +++ b/pkg/cli/gptscript.go @@ -64,7 +64,7 @@ type GPTScript struct { Chdir string `usage:"Change current working directory" short:"C"` Daemon bool `usage:"Run tool as a daemon" local:"true" hidden:"true"` Ports string `usage:"The port range to use for ephemeral daemon ports (ex: 11000-12000)" hidden:"true"` - CredentialContext string `usage:"Context name in which to store credentials" default:"default"` + CredentialContext []string `usage:"Context name(s) in which to store credentials"` CredentialOverride []string `usage:"Credentials to override (ex: --credential-override github.com/example/cred-tool:API_TOKEN=1234)"` ChatState string `usage:"The chat state to continue, or null to start a new chat and return the state" local:"true"` ForceChat bool `usage:"Force an interactive chat session if even the top level tool is not a chat tool" local:"true"` @@ -142,7 +142,7 @@ func (r *GPTScript) NewGPTScriptOpts() (gptscript.Options, error) { }, Quiet: r.Quiet, Env: os.Environ(), - CredentialContext: r.CredentialContext, + CredentialContexts: r.CredentialContext, Workspace: r.Workspace, DisablePromptServer: r.UI, DefaultModelProvider: r.DefaultModelProvider, diff --git a/pkg/credentials/store.go b/pkg/credentials/store.go index c8558f3a..749aba3a 100644 --- a/pkg/credentials/store.go +++ b/pkg/credentials/store.go @@ -8,7 +8,10 @@ import ( "strings" "github.com/docker/cli/cli/config/credentials" + "github.com/docker/cli/cli/config/types" + credentials2 "github.com/docker/docker-credential-helpers/credentials" "github.com/gptscript-ai/gptscript/pkg/config" + "golang.org/x/exp/maps" ) const ( @@ -28,18 +31,18 @@ type CredentialStore interface { } type Store struct { - credCtx string + credCtxs []string credBuilder CredentialBuilder credHelperDirs CredentialHelperDirs cfg *config.CLIConfig } -func NewStore(cfg *config.CLIConfig, credentialBuilder CredentialBuilder, credCtx, cacheDir string) (CredentialStore, error) { - if err := validateCredentialCtx(credCtx); err != nil { +func NewStore(cfg *config.CLIConfig, credentialBuilder CredentialBuilder, credCtxs []string, cacheDir string) (CredentialStore, error) { + if err := validateCredentialCtx(credCtxs); err != nil { return nil, err } return Store{ - credCtx: credCtx, + credCtxs: credCtxs, credBuilder: credentialBuilder, credHelperDirs: GetCredentialHelperDirs(cacheDir), cfg: cfg, @@ -47,22 +50,45 @@ func NewStore(cfg *config.CLIConfig, credentialBuilder CredentialBuilder, credCt } func (s Store) Get(ctx context.Context, toolName string) (*Credential, bool, error) { + if first(s.credCtxs) == AllCredentialContexts { + return nil, false, fmt.Errorf("cannot get a credential with context %q", AllCredentialContexts) + } + store, err := s.getStore(ctx) if err != nil { return nil, false, err } - auth, err := store.Get(toolNameWithCtx(toolName, s.credCtx)) - if err != nil { - return nil, false, err - } else if auth.Password == "" { + + var ( + authCfg types.AuthConfig + credCtx string + ) + for _, c := range s.credCtxs { + auth, err := store.Get(toolNameWithCtx(toolName, c)) + if err != nil { + if credentials2.IsErrCredentialsNotFound(err) { + continue + } + return nil, false, err + } else if auth.Password == "" { + continue + } + + authCfg = auth + credCtx = c + break + } + + if credCtx == "" { + // Didn't find the credential return nil, false, nil } - if auth.ServerAddress == "" { - auth.ServerAddress = toolNameWithCtx(toolName, s.credCtx) // Not sure why we have to do this, but we do. + if authCfg.ServerAddress == "" { + authCfg.ServerAddress = toolNameWithCtx(toolName, credCtx) // Not sure why we have to do this, but we do. } - cred, err := credentialFromDockerAuthConfig(auth) + cred, err := credentialFromDockerAuthConfig(authCfg) if err != nil { return nil, false, err } @@ -70,7 +96,12 @@ func (s Store) Get(ctx context.Context, toolName string) (*Credential, bool, err } func (s Store) Add(ctx context.Context, cred Credential) error { - cred.Context = s.credCtx + first := first(s.credCtxs) + if first == AllCredentialContexts { + return fmt.Errorf("cannot add a credential with context %q", AllCredentialContexts) + } + cred.Context = first + store, err := s.getStore(ctx) if err != nil { return err @@ -83,11 +114,17 @@ func (s Store) Add(ctx context.Context, cred Credential) error { } func (s Store) Remove(ctx context.Context, toolName string) error { + first := first(s.credCtxs) + if len(s.credCtxs) > 1 || first == AllCredentialContexts { + return fmt.Errorf("error: credential deletion is not supported when multiple credential contexts are provided") + } + store, err := s.getStore(ctx) if err != nil { return err } - return store.Erase(toolNameWithCtx(toolName, s.credCtx)) + + return store.Erase(toolNameWithCtx(toolName, first)) } func (s Store) List(ctx context.Context) ([]Credential, error) { @@ -100,7 +137,8 @@ func (s Store) List(ctx context.Context) ([]Credential, error) { return nil, err } - var creds []Credential + credsByContext := make(map[string][]Credential) + allCreds := make([]Credential, 0) for serverAddress, authCfg := range list { if authCfg.ServerAddress == "" { authCfg.ServerAddress = serverAddress // Not sure why we have to do this, but we do. @@ -110,12 +148,29 @@ func (s Store) List(ctx context.Context) ([]Credential, error) { if err != nil { return nil, err } - if s.credCtx == AllCredentialContexts || c.Context == s.credCtx { - creds = append(creds, c) + + allCreds = append(allCreds, c) + + if credsByContext[c.Context] == nil { + credsByContext[c.Context] = []Credential{c} + } else { + credsByContext[c.Context] = append(credsByContext[c.Context], c) + } + } + + if first(s.credCtxs) == AllCredentialContexts { + return allCreds, nil + } + + // Go through the contexts in reverse order so that higher priority contexts override lower ones. + credsByName := make(map[string]Credential) + for i := len(s.credCtxs) - 1; i >= 0; i-- { + for _, c := range credsByContext[s.credCtxs[i]] { + credsByName[c.ToolName] = c } } - return creds, nil + return maps.Values(credsByName), nil } func (s *Store) getStore(ctx context.Context) (credentials.Store, error) { @@ -139,19 +194,22 @@ func (s *Store) getStoreByHelper(ctx context.Context, helper string) (credential return NewHelper(s.cfg, helper) } -func validateCredentialCtx(ctx string) error { - if ctx == "" { - return fmt.Errorf("credential context cannot be empty") +func validateCredentialCtx(ctxs []string) error { + if len(ctxs) == 0 { + return fmt.Errorf("credential contexts must be provided") } - if ctx == AllCredentialContexts { + if len(ctxs) == 1 && ctxs[0] == AllCredentialContexts { return nil } // check alphanumeric r := regexp.MustCompile("^[a-zA-Z0-9]+$") - if !r.MatchString(ctx) { - return fmt.Errorf("credential context must be alphanumeric") + for _, c := range ctxs { + if !r.MatchString(c) { + return fmt.Errorf("credential contexts must be alphanumeric") + } } + return nil } diff --git a/pkg/credentials/util.go b/pkg/credentials/util.go index 70f31e97..39200369 100644 --- a/pkg/credentials/util.go +++ b/pkg/credentials/util.go @@ -15,3 +15,10 @@ func GetCredentialHelperDirs(cacheDir string) CredentialHelperDirs { BinDir: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "bin"), } } + +func first(s []string) string { + if len(s) == 0 { + return "" + } + return s[0] +} diff --git a/pkg/gptscript/gptscript.go b/pkg/gptscript/gptscript.go index abae80ac..7a10eda2 100644 --- a/pkg/gptscript/gptscript.go +++ b/pkg/gptscript/gptscript.go @@ -45,7 +45,7 @@ type Options struct { Monitor monitor.Options Runner runner.Options DefaultModelProvider string - CredentialContext string + CredentialContexts []string Quiet *bool Workspace string DisablePromptServer bool @@ -60,7 +60,7 @@ func Complete(opts ...Options) Options { result.Runner = runner.Complete(result.Runner, opt.Runner) result.OpenAI = openai.Complete(result.OpenAI, opt.OpenAI) - result.CredentialContext = types.FirstSet(opt.CredentialContext, result.CredentialContext) + result.CredentialContexts = append(result.CredentialContexts, opt.CredentialContexts...) result.Quiet = types.FirstSet(opt.Quiet, result.Quiet) result.Workspace = types.FirstSet(opt.Workspace, result.Workspace) result.Env = append(result.Env, opt.Env...) @@ -74,8 +74,8 @@ func Complete(opts ...Options) Options { if len(result.Env) == 0 { result.Env = os.Environ() } - if result.CredentialContext == "" { - result.CredentialContext = credentials.DefaultCredentialContext + if len(result.CredentialContexts) == 0 { + result.CredentialContexts = []string{credentials.DefaultCredentialContext} } return result @@ -103,7 +103,7 @@ func New(ctx context.Context, o ...Options) (*GPTScript, error) { return nil, err } - credStore, err := credentials.NewStore(cliCfg, opts.Runner.RuntimeManager, opts.CredentialContext, cacheClient.CacheDir()) + credStore, err := credentials.NewStore(cliCfg, opts.Runner.RuntimeManager, opts.CredentialContexts, cacheClient.CacheDir()) if err != nil { return nil, err } diff --git a/pkg/sdkserver/credentials.go b/pkg/sdkserver/credentials.go index d3f86b1f..b0246621 100644 --- a/pkg/sdkserver/credentials.go +++ b/pkg/sdkserver/credentials.go @@ -5,13 +5,14 @@ import ( "encoding/json" "fmt" "net/http" + "slices" "github.com/gptscript-ai/gptscript/pkg/config" gcontext "github.com/gptscript-ai/gptscript/pkg/context" "github.com/gptscript-ai/gptscript/pkg/credentials" ) -func (s *server) initializeCredentialStore(ctx context.Context, credCtx string) (credentials.CredentialStore, error) { +func (s *server) initializeCredentialStore(ctx context.Context, credCtxs []string) (credentials.CredentialStore, error) { cfg, err := config.ReadCLIConfig(s.gptscriptOpts.OpenAI.ConfigFile) if err != nil { return nil, fmt.Errorf("failed to read CLI config: %w", err) @@ -24,7 +25,7 @@ func (s *server) initializeCredentialStore(ctx context.Context, credCtx string) return nil, fmt.Errorf("failed to ensure credential helpers: %w", err) } - store, err := credentials.NewStore(cfg, s.runtimeManager, credCtx, s.gptscriptOpts.Cache.CacheDir) + store, err := credentials.NewStore(cfg, s.runtimeManager, credCtxs, s.gptscriptOpts.Cache.CacheDir) if err != nil { return nil, fmt.Errorf("failed to initialize credential store: %w", err) } @@ -41,9 +42,9 @@ func (s *server) listCredentials(w http.ResponseWriter, r *http.Request) { } if req.AllContexts { - req.Context = credentials.AllCredentialContexts - } else if req.Context == "" { - req.Context = credentials.DefaultCredentialContext + req.Context = []string{credentials.AllCredentialContexts} + } else if len(req.Context) == 0 { + req.Context = []string{credentials.DefaultCredentialContext} } store, err := s.initializeCredentialStore(r.Context(), req.Context) @@ -87,7 +88,7 @@ func (s *server) createCredential(w http.ResponseWriter, r *http.Request) { cred.Context = credentials.DefaultCredentialContext } - store, err := s.initializeCredentialStore(r.Context(), cred.Context) + store, err := s.initializeCredentialStore(r.Context(), []string{cred.Context}) if err != nil { writeError(logger, w, http.StatusInternalServerError, err) return @@ -114,11 +115,11 @@ func (s *server) revealCredential(w http.ResponseWriter, r *http.Request) { return } - if req.AllContexts || req.Context == credentials.AllCredentialContexts { + if req.AllContexts || slices.Contains(req.Context, credentials.AllCredentialContexts) { writeError(logger, w, http.StatusBadRequest, fmt.Errorf("allContexts is not supported for credential retrieval; please specify the specific context that the credential is in")) return - } else if req.Context == "" { - req.Context = credentials.DefaultCredentialContext + } else if len(req.Context) == 0 { + req.Context = []string{credentials.DefaultCredentialContext} } store, err := s.initializeCredentialStore(r.Context(), req.Context) @@ -151,11 +152,11 @@ func (s *server) deleteCredential(w http.ResponseWriter, r *http.Request) { return } - if req.AllContexts || req.Context == credentials.AllCredentialContexts { + if req.AllContexts || slices.Contains(req.Context, credentials.AllCredentialContexts) { writeError(logger, w, http.StatusBadRequest, fmt.Errorf("allContexts is not supported for credential deletion; please specify the specific context that the credential is in")) return - } else if req.Context == "" { - req.Context = credentials.DefaultCredentialContext + } else if len(req.Context) == 0 { + req.Context = []string{credentials.DefaultCredentialContext} } store, err := s.initializeCredentialStore(r.Context(), req.Context) diff --git a/pkg/sdkserver/routes.go b/pkg/sdkserver/routes.go index f82fa8a7..484a6fa1 100644 --- a/pkg/sdkserver/routes.go +++ b/pkg/sdkserver/routes.go @@ -184,11 +184,11 @@ func (s *server) execHandler(w http.ResponseWriter, r *http.Request) { } opts := gptscript.Options{ - Cache: cache.Options(reqObject.cacheOptions), - OpenAI: openai.Options(reqObject.openAIOptions), - Env: reqObject.Env, - Workspace: reqObject.Workspace, - CredentialContext: reqObject.CredentialContext, + Cache: cache.Options(reqObject.cacheOptions), + OpenAI: openai.Options(reqObject.openAIOptions), + Env: reqObject.Env, + Workspace: reqObject.Workspace, + CredentialContexts: reqObject.CredentialContext, Runner: runner.Options{ // Set the monitor factory so that we can get events from the server. MonitorFactory: NewSessionFactory(s.events), diff --git a/pkg/sdkserver/types.go b/pkg/sdkserver/types.go index 7ed7da78..65a0c049 100644 --- a/pkg/sdkserver/types.go +++ b/pkg/sdkserver/types.go @@ -58,7 +58,7 @@ type toolOrFileRequest struct { ChatState string `json:"chatState"` Workspace string `json:"workspace"` Env []string `json:"env"` - CredentialContext string `json:"credentialContext"` + CredentialContext []string `json:"credentialContext"` CredentialOverrides []string `json:"credentialOverrides"` Confirm bool `json:"confirm"` Location string `json:"location,omitempty"` @@ -255,7 +255,7 @@ type prompt struct { type credentialsRequest struct { content `json:",inline"` - AllContexts bool `json:"allContexts"` - Context string `json:"context"` - Name string `json:"name"` + AllContexts bool `json:"allContexts"` + Context []string `json:"context"` + Name string `json:"name"` } From f6dcb5fe08b7db4f9fe8ffe5834e19f74163bb44 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Wed, 18 Sep 2024 10:50:55 -0700 Subject: [PATCH 70/83] chore: don't capture stderr in tool output (#847) --- pkg/engine/cmd.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pkg/engine/cmd.go b/pkg/engine/cmd.go index c7d21a2b..317f0d6a 100644 --- a/pkg/engine/cmd.go +++ b/pkg/engine/cmd.go @@ -148,13 +148,8 @@ func (e *Engine) runCommand(ctx Context, tool types.Tool, input string, toolCate ) cmd.Stdout = io.MultiWriter(stdout, stdoutAndErr, progressOut) - if toolCategory == NoCategory || toolCategory == ContextToolCategory { - cmd.Stderr = io.MultiWriter(stdoutAndErr, progressOut) - result = stdoutAndErr - } else { - cmd.Stderr = io.MultiWriter(stdoutAndErr, progressOut, os.Stderr) - result = stdout - } + cmd.Stderr = io.MultiWriter(stdoutAndErr, progressOut, os.Stderr) + result = stdout if err := cmd.Run(); err != nil { if toolCategory == NoCategory { From f30e865ba54cfb66c82f8ff7489bf58736026778 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Wed, 18 Sep 2024 10:49:23 -0700 Subject: [PATCH 71/83] chore: support chattable code tools --- pkg/engine/cmd.go | 8 ++++++-- pkg/engine/engine.go | 44 ++++++++++++++++++++++++++++---------------- pkg/runner/runner.go | 17 ++++++++++++++--- 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/pkg/engine/cmd.go b/pkg/engine/cmd.go index 317f0d6a..33a67640 100644 --- a/pkg/engine/cmd.go +++ b/pkg/engine/cmd.go @@ -68,12 +68,14 @@ func compressEnv(envs []string) (result []string) { func (e *Engine) runCommand(ctx Context, tool types.Tool, input string, toolCategory ToolCategory) (cmdOut string, cmdErr error) { id := counter.Next() + var combinedOutput string defer func() { e.Progress <- types.CompletionStatus{ CompletionID: id, Response: map[string]any{ - "output": cmdOut, - "err": cmdErr, + "output": cmdOut, + "fullOutput": combinedOutput, + "err": cmdErr, }, } }() @@ -156,9 +158,11 @@ func (e *Engine) runCommand(ctx Context, tool types.Tool, input string, toolCate return fmt.Sprintf("ERROR: got (%v) while running tool, OUTPUT: %s", err, stdoutAndErr), nil } log.Errorf("failed to run tool [%s] cmd %v: %v", tool.Parameters.Name, cmd.Args, err) + combinedOutput = stdoutAndErr.String() return "", fmt.Errorf("ERROR: %s: %w", result, err) } + combinedOutput = stdoutAndErr.String() return result.String(), IsChatFinishMessage(result.String()) } diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index d028d50b..0665991c 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -281,6 +281,25 @@ func populateMessageParams(ctx Context, completion *types.CompletionRequest, too return nil } +func (e *Engine) runCommandTools(ctx Context, tool types.Tool, input string) (*Return, error) { + if tool.IsHTTP() { + return e.runHTTP(ctx.Ctx, ctx.Program, tool, input) + } else if tool.IsDaemon() { + return e.runDaemon(ctx.Ctx, ctx.Program, tool, input) + } else if tool.IsOpenAPI() { + return e.runOpenAPI(tool, input) + } else if tool.IsEcho() { + return e.runEcho(tool) + } + s, err := e.runCommand(ctx, tool, input, ctx.ToolCategory) + if err != nil { + return nil, err + } + return &Return{ + Result: &s, + }, nil +} + func (e *Engine) Start(ctx Context, input string) (ret *Return, _ error) { tool := ctx.Tool @@ -291,22 +310,7 @@ func (e *Engine) Start(ctx Context, input string) (ret *Return, _ error) { }() if tool.IsCommand() { - if tool.IsHTTP() { - return e.runHTTP(ctx.Ctx, ctx.Program, tool, input) - } else if tool.IsDaemon() { - return e.runDaemon(ctx.Ctx, ctx.Program, tool, input) - } else if tool.IsOpenAPI() { - return e.runOpenAPI(tool, input) - } else if tool.IsEcho() { - return e.runEcho(tool) - } - s, err := e.runCommand(ctx, tool, input, ctx.ToolCategory) - if err != nil { - return nil, err - } - return &Return{ - Result: &s, - }, nil + return e.runCommandTools(ctx, tool, input) } if ctx.ToolCategory == CredentialToolCategory { @@ -431,6 +435,14 @@ func (e *Engine) complete(ctx context.Context, state *State) (*Return, error) { } func (e *Engine) Continue(ctx Context, state *State, results ...CallResult) (*Return, error) { + if ctx.Tool.IsCommand() { + var input string + if len(results) == 1 { + input = results[0].User + } + return e.runCommandTools(ctx, ctx.Tool, input) + } + if state == nil { return nil, fmt.Errorf("invalid continue call, missing state") } diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index c843b6b5..e6318c15 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -179,7 +179,6 @@ func (r *Runner) Chat(ctx context.Context, prevState ChatState, prg types.Progra } } else { state = state.WithResumeInput(&input) - state.ResumeInput = &input } state, err = r.resume(callCtx, monitor, env, state) @@ -683,7 +682,13 @@ func (r *Runner) subCall(ctx context.Context, parentContext engine.Context, moni }, nil } - return r.call(callCtx, monitor, env, input) + state, err := r.call(callCtx, monitor, env, input) + if finishErr := (*engine.ErrChatFinish)(nil); errors.As(err, &finishErr) && callCtx.Tool.Chat { + return &State{ + Result: &finishErr.Message, + }, nil + } + return state, err } func (r *Runner) subCallResume(ctx context.Context, parentContext engine.Context, monitor Monitor, env []string, toolID, callID string, state *State, toolCategory engine.ToolCategory) (*State, error) { @@ -692,7 +697,13 @@ func (r *Runner) subCallResume(ctx context.Context, parentContext engine.Context return nil, err } - return r.resume(callCtx, monitor, env, state) + state, err = r.resume(callCtx, monitor, env, state) + if finishErr := (*engine.ErrChatFinish)(nil); errors.As(err, &finishErr) && callCtx.Tool.Chat { + return &State{ + Result: &finishErr.Message, + }, nil + } + return state, err } type SubCallResult struct { From a15945e9c34393018c19427778c12298bd6823d6 Mon Sep 17 00:00:00 2001 From: Atulpriya Sharma Date: Fri, 20 Sep 2024 01:56:45 +0530 Subject: [PATCH 72/83] Add Testkube GPT example (#850) --- examples/testkube.gpt | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 examples/testkube.gpt diff --git a/examples/testkube.gpt b/examples/testkube.gpt new file mode 100644 index 00000000..758d9b34 --- /dev/null +++ b/examples/testkube.gpt @@ -0,0 +1,38 @@ +Name: Testkube +Description: A tool to help you perform testing of your application on your Kubernetes clusters using Testkube. +Context: learn-testkube, learn-kubectl +Tools: sys.exec, sys.http.html2text?, sys.find, sys.read, sys.write, github.com/gptscript-ai/browse-web-page +chat:true + +You are an assistant for Testkube and help the user create, manage and execute test workflows. You can also perform kubernetes related tasks. + +Rules +1. Access the testkube workflow docs at https://docs.testkube.io/articles/test-workflows and remember the latest specification to create testworkflows. +2. Use testkube CLI to interact with Testkube. +3. Use kubectl CLI to interact with the Kubernetes cluster. +4. Based on the user's request, perform actions on the Kubernetes cluster and create, manage, delete test workflows. + + +--- + +Name: learn-testkube +Description: A tool to help you learn testkube cli +#!/bin/bash +testkube --help +testkube create --help +testkube create testworkflow --help +testkube run --help + +--- + +Name: learn-kubectl +Description: A tool to help you learn k8s and related commands +#!/bin/bash + +CMDS="kubectl helm" +echo 'The additional CLI commands are available locally, use the `exec` tool to invoke them:' +for i in $CMDS; do + if [ -e "$(command -v $i)" ]; then + echo ' ' $i + fi +done From 354541cb0fa6d8bc48949285527b791454b80666 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Thu, 19 Sep 2024 17:28:09 -0400 Subject: [PATCH 73/83] fix: stop always adding the default cred context (#854) Signed-off-by: Grant Linville --- pkg/gptscript/gptscript.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gptscript/gptscript.go b/pkg/gptscript/gptscript.go index 7a10eda2..11afb7d6 100644 --- a/pkg/gptscript/gptscript.go +++ b/pkg/gptscript/gptscript.go @@ -60,7 +60,7 @@ func Complete(opts ...Options) Options { result.Runner = runner.Complete(result.Runner, opt.Runner) result.OpenAI = openai.Complete(result.OpenAI, opt.OpenAI) - result.CredentialContexts = append(result.CredentialContexts, opt.CredentialContexts...) + result.CredentialContexts = opt.CredentialContexts result.Quiet = types.FirstSet(opt.Quiet, result.Quiet) result.Workspace = types.FirstSet(opt.Workspace, result.Workspace) result.Env = append(result.Env, opt.Env...) From 8ecad90f7ca24b34a42811dd1b49caa046f102e1 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Thu, 19 Sep 2024 20:00:35 -0400 Subject: [PATCH 74/83] fix: sdkserver: rename credentialContext to credentialContexts (#855) Signed-off-by: Grant Linville --- pkg/sdkserver/routes.go | 2 +- pkg/sdkserver/types.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/sdkserver/routes.go b/pkg/sdkserver/routes.go index 484a6fa1..fc69a08c 100644 --- a/pkg/sdkserver/routes.go +++ b/pkg/sdkserver/routes.go @@ -188,7 +188,7 @@ func (s *server) execHandler(w http.ResponseWriter, r *http.Request) { OpenAI: openai.Options(reqObject.openAIOptions), Env: reqObject.Env, Workspace: reqObject.Workspace, - CredentialContexts: reqObject.CredentialContext, + CredentialContexts: reqObject.CredentialContexts, Runner: runner.Options{ // Set the monitor factory so that we can get events from the server. MonitorFactory: NewSessionFactory(s.events), diff --git a/pkg/sdkserver/types.go b/pkg/sdkserver/types.go index 65a0c049..42b2bb64 100644 --- a/pkg/sdkserver/types.go +++ b/pkg/sdkserver/types.go @@ -58,7 +58,7 @@ type toolOrFileRequest struct { ChatState string `json:"chatState"` Workspace string `json:"workspace"` Env []string `json:"env"` - CredentialContext []string `json:"credentialContext"` + CredentialContexts []string `json:"credentialContexts"` CredentialOverrides []string `json:"credentialOverrides"` Confirm bool `json:"confirm"` Location string `json:"location,omitempty"` From 32e544c993c71d74c95a3607622caa7a5233cb97 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Mon, 23 Sep 2024 11:57:36 -0700 Subject: [PATCH 75/83] bug: load vcs support in embedded server always (#858) * bug: load vcs support in embedded server always * chore: try always running exec for "sh -c" in engine --- main.go | 2 -- pkg/engine/cmd.go | 2 +- pkg/gptscript/gptscript.go | 3 +++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 33ab4278..02923925 100644 --- a/main.go +++ b/main.go @@ -2,8 +2,6 @@ package main import ( "github.com/gptscript-ai/gptscript/pkg/cli" - // Load all VCS - _ "github.com/gptscript-ai/gptscript/pkg/loader/vcs" ) func main() { diff --git a/pkg/engine/cmd.go b/pkg/engine/cmd.go index 33a67640..5b27a579 100644 --- a/pkg/engine/cmd.go +++ b/pkg/engine/cmd.go @@ -328,7 +328,7 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T } if useShell { - args = append([]string{"/bin/sh", "-c"}, strings.Join(args, " ")) + args = append([]string{"/bin/sh", "-c"}, "exec "+strings.Join(args, " ")) } else { args[0] = env.Lookup(envvars, args[0]) } diff --git a/pkg/gptscript/gptscript.go b/pkg/gptscript/gptscript.go index 11afb7d6..679eb503 100644 --- a/pkg/gptscript/gptscript.go +++ b/pkg/gptscript/gptscript.go @@ -25,6 +25,9 @@ import ( "github.com/gptscript-ai/gptscript/pkg/repos/runtimes" "github.com/gptscript-ai/gptscript/pkg/runner" "github.com/gptscript-ai/gptscript/pkg/types" + + // Load all VCS + _ "github.com/gptscript-ai/gptscript/pkg/loader/vcs" ) var log = mvl.Package() From 2eafb08d985cfc63c74304e99c51c9c139729151 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Mon, 23 Sep 2024 12:27:52 -0700 Subject: [PATCH 76/83] chore: bump tui (#859) --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 83146eeb..42a1e76f 100644 --- a/go.mod +++ b/go.mod @@ -17,8 +17,8 @@ require ( github.com/gptscript-ai/broadcaster v0.0.0-20240625175512-c43682019b86 github.com/gptscript-ai/chat-completion-client v0.0.0-20240813051153-a440ada7e3c3 github.com/gptscript-ai/cmd v0.0.0-20240802230653-326b7baf6fcb - github.com/gptscript-ai/go-gptscript v0.9.4-0.20240801203434-840b14393b17 - github.com/gptscript-ai/tui v0.0.0-20240804004233-efc5673dc76e + github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240920232051-64eaa0ac8caf + github.com/gptscript-ai/tui v0.0.0-20240923192013-172e51ccf1d6 github.com/hexops/autogold/v2 v2.2.1 github.com/hexops/valast v1.4.4 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 diff --git a/go.sum b/go.sum index 85a3f76e..c7cb9d5c 100644 --- a/go.sum +++ b/go.sum @@ -204,10 +204,10 @@ github.com/gptscript-ai/chat-completion-client v0.0.0-20240813051153-a440ada7e3c github.com/gptscript-ai/chat-completion-client v0.0.0-20240813051153-a440ada7e3c3/go.mod h1:7P/o6/IWa1KqsntVf68hSnLKuu3+xuqm6lYhch1w4jo= github.com/gptscript-ai/cmd v0.0.0-20240802230653-326b7baf6fcb h1:ky2J2CzBOskC7Jgm2VJAQi2x3p7FVGa+2/PcywkFJuc= github.com/gptscript-ai/cmd v0.0.0-20240802230653-326b7baf6fcb/go.mod h1:DJAo1xTht1LDkNYFNydVjTHd576TC7MlpsVRl3oloVw= -github.com/gptscript-ai/go-gptscript v0.9.4-0.20240801203434-840b14393b17 h1:BTfJ6ls31Roq42lznlZnuPzRf0wrT8jT+tWcvq7wDXY= -github.com/gptscript-ai/go-gptscript v0.9.4-0.20240801203434-840b14393b17/go.mod h1:Dh6vYRAiVcyC3ElZIGzTvNF1FxtYwA07BHfSiFKQY7s= -github.com/gptscript-ai/tui v0.0.0-20240804004233-efc5673dc76e h1:OO/b8gGQi3jIpDoII+jf7fc4ssqOZdFcb9zB+QjsxRQ= -github.com/gptscript-ai/tui v0.0.0-20240804004233-efc5673dc76e/go.mod h1:KGtCo7cjH6qR6Wp6AyI1dL1R8bln8wVpdDEoopRUckY= +github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240920232051-64eaa0ac8caf h1:3uBPUYBuCIWgUxQPD3d3bHHr/0zgCsdzk628FJZCmno= +github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240920232051-64eaa0ac8caf/go.mod h1:/FVuLwhz+sIfsWUgUHWKi32qT0i6+IXlUlzs70KKt/Q= +github.com/gptscript-ai/tui v0.0.0-20240923192013-172e51ccf1d6 h1:vkgNZVWQgbE33VD3z9WKDwuu7B/eJVVMMPM62ixfCR8= +github.com/gptscript-ai/tui v0.0.0-20240923192013-172e51ccf1d6/go.mod h1:frrl/B+ZH3VSs3Tqk2qxEIIWTONExX3tuUa4JsVnqx4= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= From ec0c0198345bd28463e4d99e747da744b7476692 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Mon, 23 Sep 2024 16:09:15 -0400 Subject: [PATCH 77/83] fix: modify credential refresh to support stacked contexts (#856) Signed-off-by: Grant Linville --- pkg/credentials/noop.go | 4 ++++ pkg/credentials/store.go | 21 +++++++++++++++++++ pkg/runner/runner.go | 44 +++++++++++++++++++++++++++++----------- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/pkg/credentials/noop.go b/pkg/credentials/noop.go index 5f3cc5ad..3a13b907 100644 --- a/pkg/credentials/noop.go +++ b/pkg/credentials/noop.go @@ -12,6 +12,10 @@ func (s NoopStore) Add(context.Context, Credential) error { return nil } +func (s NoopStore) Refresh(context.Context, Credential) error { + return nil +} + func (s NoopStore) Remove(context.Context, string) error { return nil } diff --git a/pkg/credentials/store.go b/pkg/credentials/store.go index 749aba3a..2414e1e8 100644 --- a/pkg/credentials/store.go +++ b/pkg/credentials/store.go @@ -5,6 +5,7 @@ import ( "fmt" "path/filepath" "regexp" + "slices" "strings" "github.com/docker/cli/cli/config/credentials" @@ -26,6 +27,7 @@ type CredentialBuilder interface { type CredentialStore interface { Get(ctx context.Context, toolName string) (*Credential, bool, error) Add(ctx context.Context, cred Credential) error + Refresh(ctx context.Context, cred Credential) error Remove(ctx context.Context, toolName string) error List(ctx context.Context) ([]Credential, error) } @@ -95,6 +97,8 @@ func (s Store) Get(ctx context.Context, toolName string) (*Credential, bool, err return &cred, true, nil } +// Add adds a new credential to the credential store. +// Any context set on the credential object will be overwritten with the first context of the credential store. func (s Store) Add(ctx context.Context, cred Credential) error { first := first(s.credCtxs) if first == AllCredentialContexts { @@ -113,6 +117,23 @@ func (s Store) Add(ctx context.Context, cred Credential) error { return store.Store(auth) } +// Refresh updates an existing credential in the credential store. +func (s Store) Refresh(ctx context.Context, cred Credential) error { + if !slices.Contains(s.credCtxs, cred.Context) { + return fmt.Errorf("context %q not in list of valid contexts for this credential store", cred.Context) + } + + store, err := s.getStore(ctx) + if err != nil { + return err + } + auth, err := cred.toDockerAuthConfig() + if err != nil { + return err + } + return store.Store(auth) +} + func (s Store) Remove(ctx context.Context, toolName string) error { first := first(s.credCtxs) if len(s.credCtxs) > 1 || first == AllCredentialContexts { diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index e6318c15..7ac9fae0 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -854,8 +854,10 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env } var ( - c *credentials.Credential - exists bool + c *credentials.Credential + resultCredential credentials.Credential + exists bool + refresh bool ) rm := runtimeWithLogger(callCtx, monitor, r.runtimeManager) @@ -886,6 +888,7 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env if !exists || c.IsExpired() { // If the existing credential is expired, we need to provide it to the cred tool through the environment. if exists && c.IsExpired() { + refresh = true credJSON, err := json.Marshal(c) if err != nil { return nil, fmt.Errorf("failed to marshal credential: %w", err) @@ -916,39 +919,56 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env continue } - if err := json.Unmarshal([]byte(*res.Result), &c); err != nil { + if err := json.Unmarshal([]byte(*res.Result), &resultCredential); err != nil { return nil, fmt.Errorf("failed to unmarshal credential tool %s response: %w", ref.Reference, err) } - c.ToolName = credName - c.Type = credentials.CredentialTypeTool + resultCredential.ToolName = credName + resultCredential.Type = credentials.CredentialTypeTool + + if refresh { + // If this is a credential refresh, we need to make sure we use the same context. + resultCredential.Context = c.Context + } else { + // If it is a new credential, let the credential store determine the context. + resultCredential.Context = "" + } isEmpty := true - for _, v := range c.Env { + for _, v := range resultCredential.Env { if v != "" { isEmpty = false break } } - if !c.Ephemeral { + if !resultCredential.Ephemeral { // Only store the credential if the tool is on GitHub or has an alias, and the credential is non-empty. if (isGitHubTool(toolName) && callCtx.Program.ToolSet[ref.ToolID].Source.Repo != nil) || credentialAlias != "" { if isEmpty { log.Warnf("Not saving empty credential for tool %s", toolName) - } else if err := r.credStore.Add(callCtx.Ctx, *c); err != nil { - return nil, fmt.Errorf("failed to add credential for tool %s: %w", toolName, err) + } else { + if refresh { + err = r.credStore.Refresh(callCtx.Ctx, resultCredential) + } else { + err = r.credStore.Add(callCtx.Ctx, resultCredential) + } + if err != nil { + return nil, fmt.Errorf("failed to save credential for tool %s: %w", toolName, err) + } } } else { log.Warnf("Not saving credential for tool %s - credentials will only be saved for tools from GitHub, or tools that use aliases.", toolName) } } + } else { + resultCredential = *c } - if c.ExpiresAt != nil && (nearestExpiration == nil || nearestExpiration.After(*c.ExpiresAt)) { - nearestExpiration = c.ExpiresAt + if resultCredential.ExpiresAt != nil && (nearestExpiration == nil || nearestExpiration.After(*resultCredential.ExpiresAt)) { + nearestExpiration = resultCredential.ExpiresAt } - for k, v := range c.Env { + for k, v := range resultCredential.Env { env = append(env, fmt.Sprintf("%s=%s", k, v)) } } From f922de17ed3b14b9872b2a9ceb6ce092ec1a1d64 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Wed, 25 Sep 2024 14:23:47 -0400 Subject: [PATCH 78/83] fix: pass Usage, ChatResponseCached, and ToolResults to SDKs (#860) Signed-off-by: Donnie Adams --- pkg/sdkserver/types.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/pkg/sdkserver/types.go b/pkg/sdkserver/types.go index 42b2bb64..a4332557 100644 --- a/pkg/sdkserver/types.go +++ b/pkg/sdkserver/types.go @@ -173,6 +173,9 @@ func (r *runInfo) process(e event) map[string]any { if e.Content != "" { call.Input = e.Content } + if e.ToolResults > 0 { + call.ToolResults = e.ToolResults + } case runner.EventTypeCallSubCalls: call.setSubCalls(e.ToolSubCalls) @@ -185,6 +188,8 @@ func (r *runInfo) process(e event) map[string]any { call.setOutput(e.Content) case runner.EventTypeChat: + call.Usage = e.Usage + call.ChatResponseCached = e.ChatResponseCached if e.ChatRequest != nil { call.LLMRequest = e.ChatRequest } @@ -210,14 +215,16 @@ func (r *runInfo) processStdout(cs runner.ChatResponse) { type call struct { engine.CallContext `json:",inline"` - Type runner.EventType `json:"type"` - Start time.Time `json:"start"` - End time.Time `json:"end"` - Input string `json:"input"` - Output []output `json:"output"` - Usage types.Usage `json:"usage"` - LLMRequest any `json:"llmRequest"` - LLMResponse any `json:"llmResponse"` + Type runner.EventType `json:"type"` + Start time.Time `json:"start"` + End time.Time `json:"end"` + Input string `json:"input"` + Output []output `json:"output"` + Usage types.Usage `json:"usage"` + ChatResponseCached bool `json:"chatResponseCached"` + ToolResults int `json:"toolResults"` + LLMRequest any `json:"llmRequest"` + LLMResponse any `json:"llmResponse"` } func (c *call) setSubCalls(subCalls map[string]engine.Call) { From dd4ba0dffc5b47eb0dcade3359b8bfa6a36444e1 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Wed, 25 Sep 2024 17:16:02 -0700 Subject: [PATCH 79/83] chore: support params on input filters besides just input --- pkg/runner/input.go | 8 ++-- pkg/tests/runner2_test.go | 24 ++++++++++ .../TestInputFilterMoreArgs/call1-resp.golden | 9 ++++ .../TestInputFilterMoreArgs/call1.golden | 25 ++++++++++ .../TestInputFilterMoreArgs/call2-resp.golden | 9 ++++ .../TestInputFilterMoreArgs/call2.golden | 25 ++++++++++ .../TestInputFilterMoreArgs/step1.golden | 48 +++++++++++++++++++ .../TestInputFilterMoreArgs/step2.golden | 48 +++++++++++++++++++ 8 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 pkg/tests/testdata/TestInputFilterMoreArgs/call1-resp.golden create mode 100644 pkg/tests/testdata/TestInputFilterMoreArgs/call1.golden create mode 100644 pkg/tests/testdata/TestInputFilterMoreArgs/call2-resp.golden create mode 100644 pkg/tests/testdata/TestInputFilterMoreArgs/call2.golden create mode 100644 pkg/tests/testdata/TestInputFilterMoreArgs/step1.golden create mode 100644 pkg/tests/testdata/TestInputFilterMoreArgs/step2.golden diff --git a/pkg/runner/input.go b/pkg/runner/input.go index a211ec9d..23228813 100644 --- a/pkg/runner/input.go +++ b/pkg/runner/input.go @@ -15,12 +15,14 @@ func (r *Runner) handleInput(callCtx engine.Context, monitor Monitor, env []stri } for _, inputToolRef := range inputToolRefs { - inputData, err := json.Marshal(map[string]any{ - "input": input, - }) + data := map[string]any{} + _ = json.Unmarshal([]byte(input), &data) + data["input"] = input + inputData, err := json.Marshal(data) if err != nil { return "", fmt.Errorf("failed to marshal input: %w", err) } + res, err := r.subCall(callCtx.Ctx, callCtx, monitor, env, inputToolRef.ToolID, string(inputData), "", engine.InputToolCategory) if err != nil { return "", err diff --git a/pkg/tests/runner2_test.go b/pkg/tests/runner2_test.go index 27d4c226..93899c84 100644 --- a/pkg/tests/runner2_test.go +++ b/pkg/tests/runner2_test.go @@ -55,3 +55,27 @@ Yo dawg`, "") resp, err := r.Chat(context.Background(), nil, prg, nil, "input 1") r.AssertStep(t, resp, err) } + +func TestInputFilterMoreArgs(t *testing.T) { + r := tester.NewRunner(t) + prg, err := loader.ProgramFromSource(context.Background(), ` +chat: true +inputfilters: stuff + +Say hi + +--- +name: stuff +params: foo: bar +params: input: baz + +#!/bin/bash +echo ${FOO}:${INPUT} +`, "") + require.NoError(t, err) + + resp, err := r.Chat(context.Background(), nil, prg, nil, `{"foo":"123"}`) + r.AssertStep(t, resp, err) + resp, err = r.Chat(context.Background(), nil, prg, nil, `"foo":"123"}`) + r.AssertStep(t, resp, err) +} diff --git a/pkg/tests/testdata/TestInputFilterMoreArgs/call1-resp.golden b/pkg/tests/testdata/TestInputFilterMoreArgs/call1-resp.golden new file mode 100644 index 00000000..2861a036 --- /dev/null +++ b/pkg/tests/testdata/TestInputFilterMoreArgs/call1-resp.golden @@ -0,0 +1,9 @@ +`{ + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 1" + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestInputFilterMoreArgs/call1.golden b/pkg/tests/testdata/TestInputFilterMoreArgs/call1.golden new file mode 100644 index 00000000..30693444 --- /dev/null +++ b/pkg/tests/testdata/TestInputFilterMoreArgs/call1.golden @@ -0,0 +1,25 @@ +`{ + "model": "gpt-4o", + "internalSystemPrompt": false, + "messages": [ + { + "role": "system", + "content": [ + { + "text": "Say hi" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "123:{\"foo\":\"123\"}\n" + } + ], + "usage": {} + } + ], + "chat": true +}` diff --git a/pkg/tests/testdata/TestInputFilterMoreArgs/call2-resp.golden b/pkg/tests/testdata/TestInputFilterMoreArgs/call2-resp.golden new file mode 100644 index 00000000..997ca1b9 --- /dev/null +++ b/pkg/tests/testdata/TestInputFilterMoreArgs/call2-resp.golden @@ -0,0 +1,9 @@ +`{ + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 2" + } + ], + "usage": {} +}` diff --git a/pkg/tests/testdata/TestInputFilterMoreArgs/call2.golden b/pkg/tests/testdata/TestInputFilterMoreArgs/call2.golden new file mode 100644 index 00000000..5a39730d --- /dev/null +++ b/pkg/tests/testdata/TestInputFilterMoreArgs/call2.golden @@ -0,0 +1,25 @@ +`{ + "model": "gpt-4o", + "internalSystemPrompt": false, + "messages": [ + { + "role": "system", + "content": [ + { + "text": "Say hi" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": ":\"foo\":\"123\"}\n" + } + ], + "usage": {} + } + ], + "chat": true +}` diff --git a/pkg/tests/testdata/TestInputFilterMoreArgs/step1.golden b/pkg/tests/testdata/TestInputFilterMoreArgs/step1.golden new file mode 100644 index 00000000..a04d4508 --- /dev/null +++ b/pkg/tests/testdata/TestInputFilterMoreArgs/step1.golden @@ -0,0 +1,48 @@ +`{ + "done": false, + "content": "TEST RESULT CALL: 1", + "toolID": "inline:", + "state": { + "continuation": { + "state": { + "input": "123:{\"foo\":\"123\"}\n", + "completion": { + "model": "gpt-4o", + "internalSystemPrompt": false, + "messages": [ + { + "role": "system", + "content": [ + { + "text": "Say hi" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": "123:{\"foo\":\"123\"}\n" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 1" + } + ], + "usage": {} + } + ], + "chat": true + } + }, + "result": "TEST RESULT CALL: 1" + }, + "continuationToolID": "inline:" + } +}` diff --git a/pkg/tests/testdata/TestInputFilterMoreArgs/step2.golden b/pkg/tests/testdata/TestInputFilterMoreArgs/step2.golden new file mode 100644 index 00000000..aa41f1dd --- /dev/null +++ b/pkg/tests/testdata/TestInputFilterMoreArgs/step2.golden @@ -0,0 +1,48 @@ +`{ + "done": false, + "content": "TEST RESULT CALL: 2", + "toolID": "inline:", + "state": { + "continuation": { + "state": { + "input": ":\"foo\":\"123\"}\n", + "completion": { + "model": "gpt-4o", + "internalSystemPrompt": false, + "messages": [ + { + "role": "system", + "content": [ + { + "text": "Say hi" + } + ], + "usage": {} + }, + { + "role": "user", + "content": [ + { + "text": ":\"foo\":\"123\"}\n" + } + ], + "usage": {} + }, + { + "role": "assistant", + "content": [ + { + "text": "TEST RESULT CALL: 2" + } + ], + "usage": {} + } + ], + "chat": true + } + }, + "result": "TEST RESULT CALL: 2" + }, + "continuationToolID": "inline:" + } +}` From fde592070a94e3a8a3c3eff30b74140ac73bfbc2 Mon Sep 17 00:00:00 2001 From: Grant Linville Date: Thu, 26 Sep 2024 14:58:26 -0400 Subject: [PATCH 80/83] feat: support sqlite credential helper (#857) Signed-off-by: Grant Linville --- pkg/config/cliconfig.go | 43 ++++++++++++++++++------- pkg/credentials/store.go | 4 +-- pkg/credentials/util.go | 33 ++++++++++++++++--- pkg/repos/get.go | 50 +++++++++++++++++------------ pkg/repos/runtimes/golang/golang.go | 3 +- 5 files changed, 93 insertions(+), 40 deletions(-) diff --git a/pkg/config/cliconfig.go b/pkg/config/cliconfig.go index dd358d52..7a82b58a 100644 --- a/pkg/config/cliconfig.go +++ b/pkg/config/cliconfig.go @@ -15,13 +15,32 @@ import ( "github.com/docker/cli/cli/config/types" ) +const ( + WincredCredHelper = "wincred" + OsxkeychainCredHelper = "osxkeychain" + SecretserviceCredHelper = "secretservice" + PassCredHelper = "pass" + FileCredHelper = "file" + SqliteCredHelper = "sqlite" + + GPTScriptHelperPrefix = "gptscript-credential-" +) + var ( - darwinHelpers = []string{"osxkeychain", "file"} - windowsHelpers = []string{"wincred", "file"} - linuxHelpers = []string{"secretservice", "pass", "file"} + darwinHelpers = []string{OsxkeychainCredHelper, FileCredHelper, SqliteCredHelper} + windowsHelpers = []string{WincredCredHelper, FileCredHelper} + linuxHelpers = []string{SecretserviceCredHelper, PassCredHelper, FileCredHelper, SqliteCredHelper} ) -const GPTScriptHelperPrefix = "gptscript-credential-" +func listAsString(helpers []string) string { + if len(helpers) == 0 { + return "" + } else if len(helpers) == 1 { + return helpers[0] + } + + return strings.Join(helpers[:len(helpers)-1], ", ") + " or " + helpers[len(helpers)-1] +} type AuthConfig types.AuthConfig @@ -150,13 +169,13 @@ func ReadCLIConfig(gptscriptConfigFile string) (*CLIConfig, error) { errMsg := fmt.Sprintf("invalid credential store '%s'", result.CredentialsStore) switch runtime.GOOS { case "darwin": - errMsg += " (use 'osxkeychain' or 'file')" + errMsg += fmt.Sprintf(" (use %s)", listAsString(darwinHelpers)) case "windows": - errMsg += " (use 'wincred' or 'file')" + errMsg += fmt.Sprintf(" (use %s)", listAsString(windowsHelpers)) case "linux": - errMsg += " (use 'secretservice', 'pass', or 'file')" + errMsg += fmt.Sprintf(" (use %s)", listAsString(linuxHelpers)) default: - errMsg += " (use 'file')" + errMsg += " (use file)" } errMsg += fmt.Sprintf("\nPlease edit your config file at %s to fix this.", result.location) @@ -169,11 +188,11 @@ func ReadCLIConfig(gptscriptConfigFile string) (*CLIConfig, error) { func (c *CLIConfig) setDefaultCredentialsStore() error { switch runtime.GOOS { case "darwin": - c.CredentialsStore = "osxkeychain" + c.CredentialsStore = OsxkeychainCredHelper case "windows": - c.CredentialsStore = "wincred" + c.CredentialsStore = WincredCredHelper default: - c.CredentialsStore = "file" + c.CredentialsStore = FileCredHelper } return c.Save() } @@ -187,7 +206,7 @@ func isValidCredentialHelper(helper string) bool { case "linux": return slices.Contains(linuxHelpers, helper) default: - return helper == "file" + return helper == FileCredHelper } } diff --git a/pkg/credentials/store.go b/pkg/credentials/store.go index 2414e1e8..9827b147 100644 --- a/pkg/credentials/store.go +++ b/pkg/credentials/store.go @@ -46,7 +46,7 @@ func NewStore(cfg *config.CLIConfig, credentialBuilder CredentialBuilder, credCt return Store{ credCtxs: credCtxs, credBuilder: credentialBuilder, - credHelperDirs: GetCredentialHelperDirs(cacheDir), + credHelperDirs: GetCredentialHelperDirs(cacheDir, cfg.CredentialsStore), cfg: cfg, }, nil } @@ -199,7 +199,7 @@ func (s *Store) getStore(ctx context.Context) (credentials.Store, error) { } func (s *Store) getStoreByHelper(ctx context.Context, helper string) (credentials.Store, error) { - if helper == "" || helper == config.GPTScriptHelperPrefix+"file" { + if helper == "" || helper == config.GPTScriptHelperPrefix+config.FileCredHelper { return credentials.NewFileStore(s.cfg), nil } diff --git a/pkg/credentials/util.go b/pkg/credentials/util.go index 39200369..72f9eab9 100644 --- a/pkg/credentials/util.go +++ b/pkg/credentials/util.go @@ -1,18 +1,43 @@ package credentials import ( + "fmt" "path/filepath" + + "github.com/gptscript-ai/gptscript/pkg/config" + runtimeEnv "github.com/gptscript-ai/gptscript/pkg/env" ) type CredentialHelperDirs struct { RevisionFile, LastCheckedFile, BinDir string } -func GetCredentialHelperDirs(cacheDir string) CredentialHelperDirs { +func RepoNameForCredentialStore(store string) string { + switch store { + case config.SqliteCredHelper: + return "gptscript-credential-sqlite" + default: + return "gptscript-credential-helpers" + } +} + +func GitURLForRepoName(repoName string) (string, error) { + switch repoName { + case "gptscript-credential-sqlite": + return runtimeEnv.VarOrDefault("GPTSCRIPT_CRED_SQLITE_ROOT", "https://github.com/gptscript-ai/gptscript-credential-sqlite.git"), nil + case "gptscript-credential-helpers": + return runtimeEnv.VarOrDefault("GPTSCRIPT_CRED_HELPERS_ROOT", "https://github.com/gptscript-ai/gptscript-credential-helpers.git"), nil + default: + return "", fmt.Errorf("unknown repo name: %s", repoName) + } +} + +func GetCredentialHelperDirs(cacheDir, store string) CredentialHelperDirs { + repoName := RepoNameForCredentialStore(store) return CredentialHelperDirs{ - RevisionFile: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "revision"), - LastCheckedFile: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "last-checked"), - BinDir: filepath.Join(cacheDir, "repos", "gptscript-credential-helpers", "bin"), + RevisionFile: filepath.Join(cacheDir, "repos", repoName, "revision"), + LastCheckedFile: filepath.Join(cacheDir, "repos", repoName, "last-checked"), + BinDir: filepath.Join(cacheDir, "repos", repoName, "bin"), } } diff --git a/pkg/repos/get.go b/pkg/repos/get.go index 8346b8cf..a36c2fe0 100644 --- a/pkg/repos/get.go +++ b/pkg/repos/get.go @@ -16,7 +16,6 @@ import ( "github.com/BurntSushi/locker" "github.com/gptscript-ai/gptscript/pkg/config" "github.com/gptscript-ai/gptscript/pkg/credentials" - runtimeEnv "github.com/gptscript-ai/gptscript/pkg/env" "github.com/gptscript-ai/gptscript/pkg/hash" "github.com/gptscript-ai/gptscript/pkg/repos/git" "github.com/gptscript-ai/gptscript/pkg/repos/runtimes/golang" @@ -55,10 +54,10 @@ func (n noopRuntime) Setup(_ context.Context, _ types.Tool, _, _ string, _ []str } type Manager struct { + cacheDir string storageDir string gitDir string runtimeDir string - credHelperDirs credentials.CredentialHelperDirs runtimes []Runtime credHelperConfig *credHelperConfig } @@ -72,11 +71,11 @@ type credHelperConfig struct { func New(cacheDir string, runtimes ...Runtime) *Manager { root := filepath.Join(cacheDir, "repos") return &Manager{ - storageDir: root, - gitDir: filepath.Join(root, "git"), - runtimeDir: filepath.Join(root, "runtimes"), - credHelperDirs: credentials.GetCredentialHelperDirs(cacheDir), - runtimes: runtimes, + cacheDir: cacheDir, + storageDir: root, + gitDir: filepath.Join(root, "git"), + runtimeDir: filepath.Join(root, "runtimes"), + runtimes: runtimes, } } @@ -110,50 +109,59 @@ func (m *Manager) deferredSetUpCredentialHelpers(ctx context.Context, cliCfg *co distInfo, suffix string ) // The file helper is built-in and does not need to be downloaded. - if helperName == "file" { + if helperName == config.FileCredHelper { return nil } switch helperName { - case "wincred": + case config.WincredCredHelper: suffix = ".exe" default: distInfo = fmt.Sprintf("-%s-%s", runtime.GOOS, runtime.GOARCH) } - locker.Lock("gptscript-credential-helpers") - defer locker.Unlock("gptscript-credential-helpers") + repoName := credentials.RepoNameForCredentialStore(helperName) + + locker.Lock(repoName) + defer locker.Unlock(repoName) + + credHelperDirs := credentials.GetCredentialHelperDirs(m.cacheDir, helperName) // Load the last-checked file to make sure we haven't checked the repo in the last 24 hours. now := time.Now() - lastChecked, err := os.ReadFile(m.credHelperDirs.LastCheckedFile) + lastChecked, err := os.ReadFile(credHelperDirs.LastCheckedFile) if err == nil { if t, err := time.Parse(time.RFC3339, strings.TrimSpace(string(lastChecked))); err == nil && now.Sub(t) < 24*time.Hour { // Make sure the binary still exists, and if it does, return. - if _, err := os.Stat(filepath.Join(m.credHelperDirs.BinDir, "gptscript-credential-"+helperName+suffix)); err == nil { + if _, err := os.Stat(filepath.Join(credHelperDirs.BinDir, "gptscript-credential-"+helperName+suffix)); err == nil { log.Debugf("Credential helper %s up-to-date as of %v, checking for updates after %v", helperName, t, t.Add(24*time.Hour)) return nil } } } - if err := os.MkdirAll(filepath.Dir(m.credHelperDirs.LastCheckedFile), 0755); err != nil { + if err := os.MkdirAll(filepath.Dir(credHelperDirs.LastCheckedFile), 0755); err != nil { return err } // Update the last-checked file. - if err := os.WriteFile(m.credHelperDirs.LastCheckedFile, []byte(now.Format(time.RFC3339)), 0644); err != nil { + if err := os.WriteFile(credHelperDirs.LastCheckedFile, []byte(now.Format(time.RFC3339)), 0644); err != nil { + return err + } + + gitURL, err := credentials.GitURLForRepoName(repoName) + if err != nil { return err } tool := types.Tool{ ToolDef: types.ToolDef{ Parameters: types.Parameters{ - Name: "gptscript-credential-helpers", + Name: repoName, }, }, Source: types.ToolSource{ Repo: &types.Repo{ - Root: runtimeEnv.VarOrDefault("GPTSCRIPT_CRED_HELPERS_ROOT", "https://github.com/gptscript-ai/gptscript-credential-helpers.git"), + Root: gitURL, }, }, } @@ -164,12 +172,12 @@ func (m *Manager) deferredSetUpCredentialHelpers(ctx context.Context, cliCfg *co var needsDownloaded bool // Check the last revision shasum and see if it is different from the current one. - lastRevision, err := os.ReadFile(m.credHelperDirs.RevisionFile) + lastRevision, err := os.ReadFile(credHelperDirs.RevisionFile) if (err == nil && strings.TrimSpace(string(lastRevision)) != tool.Source.Repo.Root+tag) || errors.Is(err, fs.ErrNotExist) { // Need to pull the latest version. needsDownloaded = true // Update the revision file to the new revision. - if err = os.WriteFile(m.credHelperDirs.RevisionFile, []byte(tool.Source.Repo.Root+tag), 0644); err != nil { + if err = os.WriteFile(credHelperDirs.RevisionFile, []byte(tool.Source.Repo.Root+tag), 0644); err != nil { return err } } else if err != nil { @@ -179,7 +187,7 @@ func (m *Manager) deferredSetUpCredentialHelpers(ctx context.Context, cliCfg *co if !needsDownloaded { // Check for the existence of the credential helper binary. // If it's there, we have no need to download it and can just return. - if _, err = os.Stat(filepath.Join(m.credHelperDirs.BinDir, "gptscript-credential-"+helperName+suffix)); err == nil { + if _, err = os.Stat(filepath.Join(credHelperDirs.BinDir, "gptscript-credential-"+helperName+suffix)); err == nil { return nil } } @@ -187,7 +195,7 @@ func (m *Manager) deferredSetUpCredentialHelpers(ctx context.Context, cliCfg *co // Find the Go runtime and use it to build the credential helper. for _, rt := range m.runtimes { if strings.HasPrefix(rt.ID(), "go") { - return rt.(*golang.Runtime).DownloadCredentialHelper(ctx, tool, helperName, distInfo, suffix, m.credHelperDirs.BinDir) + return rt.(*golang.Runtime).DownloadCredentialHelper(ctx, tool, helperName, distInfo, suffix, credHelperDirs.BinDir) } } diff --git a/pkg/repos/runtimes/golang/golang.go b/pkg/repos/runtimes/golang/golang.go index 47e8461f..f86fa88d 100644 --- a/pkg/repos/runtimes/golang/golang.go +++ b/pkg/repos/runtimes/golang/golang.go @@ -18,6 +18,7 @@ import ( "runtime" "strings" + "github.com/gptscript-ai/gptscript/pkg/config" "github.com/gptscript-ai/gptscript/pkg/debugcmd" runtimeEnv "github.com/gptscript-ai/gptscript/pkg/env" "github.com/gptscript-ai/gptscript/pkg/hash" @@ -286,7 +287,7 @@ func (r *Runtime) Setup(ctx context.Context, _ types.Tool, dataRoot, toolSource } func (r *Runtime) DownloadCredentialHelper(ctx context.Context, tool types.Tool, helperName, distInfo, suffix string, binDir string) error { - if helperName == "file" { + if helperName == config.FileCredHelper { return nil } From c61a7cae73c1b57c131be866f4c995776ba82d3c Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Fri, 27 Sep 2024 13:49:22 -0400 Subject: [PATCH 81/83] fix: send proper SSE for stderr message in SDK server (#862) Signed-off-by: Donnie Adams --- pkg/sdkserver/run.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/sdkserver/run.go b/pkg/sdkserver/run.go index 0d055614..b6b5a049 100644 --- a/pkg/sdkserver/run.go +++ b/pkg/sdkserver/run.go @@ -75,7 +75,9 @@ func processEventStreamOutput(ctx context.Context, logger mvl.Logger, w http.Res "stdout": out, }) case err := <-errChan: - writeError(logger, w, http.StatusInternalServerError, fmt.Errorf("failed to run file: %w", err)) + writeServerSentEvent(logger, w, map[string]any{ + "stderr": fmt.Sprintf("failed to run: %v", err), + }) } // Now that we have received all events, send the DONE event. From 9d9f8591a9416ecb0a2276c251e759cb54124119 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Fri, 27 Sep 2024 17:00:51 -0400 Subject: [PATCH 82/83] chore: bump go-gptscript and Go versions (#863) Signed-off-by: Donnie Adams --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 42a1e76f..6e22348f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/gptscript-ai/gptscript -go 1.23.0 +go 1.23.1 require ( github.com/AlecAivazis/survey/v2 v2.3.7 @@ -17,7 +17,7 @@ require ( github.com/gptscript-ai/broadcaster v0.0.0-20240625175512-c43682019b86 github.com/gptscript-ai/chat-completion-client v0.0.0-20240813051153-a440ada7e3c3 github.com/gptscript-ai/cmd v0.0.0-20240802230653-326b7baf6fcb - github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240920232051-64eaa0ac8caf + github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240927194651-15782507bdff github.com/gptscript-ai/tui v0.0.0-20240923192013-172e51ccf1d6 github.com/hexops/autogold/v2 v2.2.1 github.com/hexops/valast v1.4.4 diff --git a/go.sum b/go.sum index c7cb9d5c..6e3f4a21 100644 --- a/go.sum +++ b/go.sum @@ -204,8 +204,8 @@ github.com/gptscript-ai/chat-completion-client v0.0.0-20240813051153-a440ada7e3c github.com/gptscript-ai/chat-completion-client v0.0.0-20240813051153-a440ada7e3c3/go.mod h1:7P/o6/IWa1KqsntVf68hSnLKuu3+xuqm6lYhch1w4jo= github.com/gptscript-ai/cmd v0.0.0-20240802230653-326b7baf6fcb h1:ky2J2CzBOskC7Jgm2VJAQi2x3p7FVGa+2/PcywkFJuc= github.com/gptscript-ai/cmd v0.0.0-20240802230653-326b7baf6fcb/go.mod h1:DJAo1xTht1LDkNYFNydVjTHd576TC7MlpsVRl3oloVw= -github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240920232051-64eaa0ac8caf h1:3uBPUYBuCIWgUxQPD3d3bHHr/0zgCsdzk628FJZCmno= -github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240920232051-64eaa0ac8caf/go.mod h1:/FVuLwhz+sIfsWUgUHWKi32qT0i6+IXlUlzs70KKt/Q= +github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240927194651-15782507bdff h1:GnbVti8eAH8iecIo5cY5GoXhz/ZChdyA1c2SmukaoeA= +github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240927194651-15782507bdff/go.mod h1:/FVuLwhz+sIfsWUgUHWKi32qT0i6+IXlUlzs70KKt/Q= github.com/gptscript-ai/tui v0.0.0-20240923192013-172e51ccf1d6 h1:vkgNZVWQgbE33VD3z9WKDwuu7B/eJVVMMPM62ixfCR8= github.com/gptscript-ai/tui v0.0.0-20240923192013-172e51ccf1d6/go.mod h1:frrl/B+ZH3VSs3Tqk2qxEIIWTONExX3tuUa4JsVnqx4= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= From cc5e5ed5463d9e9d5fd09bea76755019cd4f75b0 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Fri, 27 Sep 2024 20:07:51 -0400 Subject: [PATCH 83/83] chore: bump go-gptscript to 326b7baf6fcb to pick up env var fixes (#864) Signed-off-by: Donnie Adams --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6e22348f..4a95a521 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/gptscript-ai/broadcaster v0.0.0-20240625175512-c43682019b86 github.com/gptscript-ai/chat-completion-client v0.0.0-20240813051153-a440ada7e3c3 github.com/gptscript-ai/cmd v0.0.0-20240802230653-326b7baf6fcb - github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240927194651-15782507bdff + github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240927213153-2af51434b93e github.com/gptscript-ai/tui v0.0.0-20240923192013-172e51ccf1d6 github.com/hexops/autogold/v2 v2.2.1 github.com/hexops/valast v1.4.4 diff --git a/go.sum b/go.sum index 6e3f4a21..80cbcea1 100644 --- a/go.sum +++ b/go.sum @@ -204,8 +204,8 @@ github.com/gptscript-ai/chat-completion-client v0.0.0-20240813051153-a440ada7e3c github.com/gptscript-ai/chat-completion-client v0.0.0-20240813051153-a440ada7e3c3/go.mod h1:7P/o6/IWa1KqsntVf68hSnLKuu3+xuqm6lYhch1w4jo= github.com/gptscript-ai/cmd v0.0.0-20240802230653-326b7baf6fcb h1:ky2J2CzBOskC7Jgm2VJAQi2x3p7FVGa+2/PcywkFJuc= github.com/gptscript-ai/cmd v0.0.0-20240802230653-326b7baf6fcb/go.mod h1:DJAo1xTht1LDkNYFNydVjTHd576TC7MlpsVRl3oloVw= -github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240927194651-15782507bdff h1:GnbVti8eAH8iecIo5cY5GoXhz/ZChdyA1c2SmukaoeA= -github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240927194651-15782507bdff/go.mod h1:/FVuLwhz+sIfsWUgUHWKi32qT0i6+IXlUlzs70KKt/Q= +github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240927213153-2af51434b93e h1:WpNae0NBx+Ri8RB3SxF8DhadDKU7h+jfWPQterDpbJA= +github.com/gptscript-ai/go-gptscript v0.9.5-rc5.0.20240927213153-2af51434b93e/go.mod h1:/FVuLwhz+sIfsWUgUHWKi32qT0i6+IXlUlzs70KKt/Q= github.com/gptscript-ai/tui v0.0.0-20240923192013-172e51ccf1d6 h1:vkgNZVWQgbE33VD3z9WKDwuu7B/eJVVMMPM62ixfCR8= github.com/gptscript-ai/tui v0.0.0-20240923192013-172e51ccf1d6/go.mod h1:frrl/B+ZH3VSs3Tqk2qxEIIWTONExX3tuUa4JsVnqx4= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=