From f3c7f43936e6349965783aa78d4be94c4647d61e Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Wed, 17 Apr 2024 17:14:56 -0700 Subject: [PATCH] feat: add chat --- go.mod | 17 +- go.sum | 67 ++- pkg/builtin/builtin.go | 29 + pkg/chat/chat.go | 83 +++ pkg/chat/readline.go | 66 +++ pkg/cli/eval.go | 9 + pkg/cli/gptscript.go | 45 +- pkg/engine/cmd.go | 4 +- pkg/engine/engine.go | 125 +++-- pkg/engine/print.go | 26 + pkg/gptscript/gptscript.go | 4 + pkg/loader/loader.go | 14 + pkg/monitor/display.go | 41 +- pkg/mvl/log.go | 14 + pkg/openai/client.go | 13 +- pkg/parser/parser.go | 11 +- pkg/runner/parallel.go | 57 ++ pkg/runner/runner.go | 403 +++++++++++--- pkg/tests/runner_test.go | 509 ++++++++++++++++++ pkg/tests/testdata/TestChat/call1.golden | 28 + pkg/tests/testdata/TestChat/call2.golden | 44 ++ pkg/tests/testdata/TestChat/test.gpt | 3 + .../testdata/TestChatRunError/call1.golden | 20 + pkg/tests/testdata/TestChatRunError/test.gpt | 3 + .../testdata/TestDualSubChat/call1.golden | 43 ++ .../testdata/TestDualSubChat/call2.golden | 45 ++ .../testdata/TestDualSubChat/call3.golden | 45 ++ .../testdata/TestDualSubChat/call4.golden | 61 +++ .../testdata/TestDualSubChat/call5.golden | 61 +++ .../testdata/TestDualSubChat/call6.golden | 77 +++ .../call7.golden} | 53 +- .../testdata/TestDualSubChat/step1.golden | 240 +++++++++ .../testdata/TestDualSubChat/step2.golden | 182 +++++++ .../testdata/TestDualSubChat/step3.golden | 198 +++++++ .../testdata/TestDualSubChat/step4.golden | 6 + pkg/tests/testdata/TestDualSubChat/test.gpt | 17 + pkg/tests/testdata/TestExport/call1.golden | 2 +- pkg/tests/testdata/TestExport/call3.golden | 2 +- pkg/tests/testdata/TestSubChat/call1.golden | 36 ++ pkg/tests/testdata/TestSubChat/call2.golden | 20 + pkg/tests/testdata/TestSubChat/call3.golden | 36 ++ pkg/tests/testdata/TestSubChat/test.gpt | 9 + pkg/tests/tester/runner.go | 43 +- pkg/types/tool.go | 25 +- pkg/types/toolname.go | 20 +- pkg/types/toolname_test.go | 4 +- scripts/ci.gpt | 11 - scripts/gen-docs.gpt | 14 - scripts/upload-s3.gpt | 12 - scripts/upload-s3.sh | 6 - 50 files changed, 2658 insertions(+), 245 deletions(-) create mode 100644 pkg/chat/chat.go create mode 100644 pkg/chat/readline.go create mode 100644 pkg/engine/print.go create mode 100644 pkg/runner/parallel.go create mode 100644 pkg/tests/testdata/TestChat/call1.golden create mode 100644 pkg/tests/testdata/TestChat/call2.golden create mode 100644 pkg/tests/testdata/TestChat/test.gpt create mode 100644 pkg/tests/testdata/TestChatRunError/call1.golden create mode 100644 pkg/tests/testdata/TestChatRunError/test.gpt create mode 100644 pkg/tests/testdata/TestDualSubChat/call1.golden create mode 100644 pkg/tests/testdata/TestDualSubChat/call2.golden create mode 100644 pkg/tests/testdata/TestDualSubChat/call3.golden create mode 100644 pkg/tests/testdata/TestDualSubChat/call4.golden create mode 100644 pkg/tests/testdata/TestDualSubChat/call5.golden create mode 100644 pkg/tests/testdata/TestDualSubChat/call6.golden rename pkg/tests/testdata/{TestCwd.golden => TestDualSubChat/call7.golden} (65%) create mode 100644 pkg/tests/testdata/TestDualSubChat/step1.golden create mode 100644 pkg/tests/testdata/TestDualSubChat/step2.golden create mode 100644 pkg/tests/testdata/TestDualSubChat/step3.golden create mode 100644 pkg/tests/testdata/TestDualSubChat/step4.golden create mode 100644 pkg/tests/testdata/TestDualSubChat/test.gpt create mode 100644 pkg/tests/testdata/TestSubChat/call1.golden create mode 100644 pkg/tests/testdata/TestSubChat/call2.golden create mode 100644 pkg/tests/testdata/TestSubChat/call3.golden create mode 100644 pkg/tests/testdata/TestSubChat/test.gpt delete mode 100644 scripts/ci.gpt delete mode 100644 scripts/gen-docs.gpt delete mode 100644 scripts/upload-s3.gpt delete mode 100755 scripts/upload-s3.sh diff --git a/go.mod b/go.mod index 69849f66..33c490d7 100644 --- a/go.mod +++ b/go.mod @@ -8,13 +8,14 @@ require ( github.com/acorn-io/broadcaster v0.0.0-20240105011354-bfadd4a7b45d github.com/acorn-io/cmd v0.0.0-20240404013709-34f690bde37b github.com/adrg/xdg v0.4.0 + github.com/chzyer/readline v1.5.1 github.com/docker/cli v26.0.0+incompatible github.com/docker/docker-credential-helpers v0.8.1 github.com/fatih/color v1.16.0 github.com/getkin/kin-openapi v0.123.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gptscript-ai/chat-completion-client v0.0.0-20240404013040-49eb8f6affa1 - github.com/hexops/autogold/v2 v2.1.0 + github.com/hexops/autogold/v2 v2.2.1 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 github.com/mholt/archiver/v4 v4.0.0-alpha.8 github.com/olahol/melody v1.1.4 @@ -26,8 +27,8 @@ require ( github.com/stretchr/testify v1.8.4 github.com/tidwall/gjson v1.17.1 golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc - golang.org/x/sync v0.6.0 - golang.org/x/term v0.16.0 + golang.org/x/sync v0.7.0 + golang.org/x/term v0.19.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -48,7 +49,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hexops/autogold v1.3.1 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect - github.com/hexops/valast v1.4.3 // indirect + github.com/hexops/valast v1.4.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -75,11 +76,11 @@ require ( github.com/tidwall/pretty v1.2.0 // indirect github.com/ulikunitz/xz v0.5.10 // indirect go4.org v0.0.0-20200411211856-f5505b9728dd // indirect - golang.org/x/mod v0.15.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.17.0 // indirect + golang.org/x/tools v0.20.0 // indirect gotest.tools/v3 v3.5.1 // indirect mvdan.cc/gofumpt v0.6.0 // indirect ) diff --git a/go.sum b/go.sum index 5bc013f3..844f293f 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,14 @@ 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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +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/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw= github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA= @@ -61,9 +67,11 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf 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= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8= @@ -126,12 +134,13 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hexops/autogold v0.8.1/go.mod h1:97HLDXyG23akzAoRYJh/2OBs3kd80eHyKPvZw0S5ZBY= github.com/hexops/autogold v1.3.1 h1:YgxF9OHWbEIUjhDbpnLhgVsjUDsiHDTyDfy2lrfdlzo= github.com/hexops/autogold v1.3.1/go.mod h1:sQO+mQUCVfxOKPht+ipDSkJ2SCJ7BNJVHZexsXqWMx4= -github.com/hexops/autogold/v2 v2.1.0 h1:5s9J6CROngFPkgowSkV20bIflBrImSdDqIpoXJeZSkU= -github.com/hexops/autogold/v2 v2.1.0/go.mod h1:cYVc0tJn6v9Uf9xMOHvmH6scuTxsVJSxGcKR/yOVPzY= +github.com/hexops/autogold/v2 v2.2.1 h1:JPUXuZQGkcQMv7eeDXuNMovjfoRYaa0yVcm+F3voaGY= +github.com/hexops/autogold/v2 v2.2.1/go.mod h1:IJwxtUfj1BGLm0YsR/k+dIxYi6xbeLjqGke2bzcOTMI= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/hexops/valast v1.4.3 h1:oBoGERMJh6UZdRc6cduE1CTPK+VAdXA59Y1HFgu3sm0= github.com/hexops/valast v1.4.3/go.mod h1:Iqx2kLj3Jn47wuXpj3wX40xn6F93QNFBHuiKBerkTGA= +github.com/hexops/valast v1.4.4 h1:rETyycw+/L2ZVJHHNxEBgh8KUn+87WugH9MxcEv9PGs= +github.com/hexops/valast v1.4.4/go.mod h1:Jcy1pNH7LNraVaAZDLyv21hHg2WBv9Nf9FL6fGxU7o4= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -172,6 +181,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= @@ -205,6 +216,7 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= @@ -253,6 +265,7 @@ 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.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 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= @@ -282,8 +295,11 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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/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= @@ -301,8 +317,12 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +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/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= @@ -317,8 +337,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -338,20 +359,29 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w 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-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.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= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +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.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= 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= @@ -360,6 +390,9 @@ golang.org/x/text v0.3.3/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.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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -390,8 +423,11 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +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/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= @@ -446,6 +482,7 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= +mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/pkg/builtin/builtin.go b/pkg/builtin/builtin.go index 668faa9e..67e59a60 100644 --- a/pkg/builtin/builtin.go +++ b/pkg/builtin/builtin.go @@ -85,6 +85,15 @@ var tools = map[string]types.Tool{ }, BuiltinFunc: SysAbort, }, + "sys.chat.finish": { + Parameters: types.Parameters{ + Description: "Concludes the conversation. This can not be used to ask a question.", + Arguments: types.ObjectSchema( + "summary", "A summary of the dialog", + ), + }, + BuiltinFunc: SysChatFinish, + }, "sys.http.post": { Parameters: types.Parameters{ Description: "Write contents to a http or https URL using the POST method", @@ -524,6 +533,26 @@ func SysGetenv(ctx context.Context, env []string, input string) (string, error) return os.Getenv(params.Name), nil } +type ErrChatFinish struct { + Message string +} + +func (e *ErrChatFinish) Error() string { + return fmt.Sprintf("CHAT FINISH: %s", e.Message) +} + +func SysChatFinish(ctx context.Context, env []string, input string) (string, error) { + var params struct { + Message string `json:"message,omitempty"` + } + if err := json.Unmarshal([]byte(input), ¶ms); err != nil { + return "", err + } + return "", &ErrChatFinish{ + Message: params.Message, + } +} + func SysAbort(ctx context.Context, env []string, input string) (string, error) { var params struct { Message string `json:"message,omitempty"` diff --git a/pkg/chat/chat.go b/pkg/chat/chat.go new file mode 100644 index 00000000..b2852c7a --- /dev/null +++ b/pkg/chat/chat.go @@ -0,0 +1,83 @@ +package chat + +import ( + "context" + + "github.com/fatih/color" + "github.com/gptscript-ai/gptscript/pkg/runner" + "github.com/gptscript-ai/gptscript/pkg/types" +) + +type Prompter interface { + Readline() (string, bool, error) + Printf(format string, args ...interface{}) (int, error) + SetPrompt(p string) + Close() error +} + +type Chatter interface { + Chat(ctx context.Context, prevState runner.ChatState, prg types.Program, env []string, input string) (resp runner.ChatResponse, err error) +} + +type GetProgram func() (types.Program, error) + +func getPrompt(prg types.Program, resp runner.ChatResponse) string { + name := prg.ChatName() + if newName := prg.ToolSet[resp.ToolID].Name; newName != "" { + name = newName + } + + return color.GreenString("%s> ", name) +} + +func Start(ctx context.Context, prevState runner.ChatState, chatter Chatter, prg GetProgram, env []string, startInput string) error { + var ( + prompter Prompter + ) + + prompter, err := newReadlinePrompter() + if err != nil { + return err + } + defer prompter.Close() + + for { + var ( + input string + ok bool + resp runner.ChatResponse + ) + + prg, err := prg() + if err != nil { + return err + } + + prompter.SetPrompt(getPrompt(prg, resp)) + + if startInput != "" { + input = startInput + startInput = "" + } else if !(prevState == nil && prg.ToolSet[prg.EntryToolID].Arguments == nil) { + // The above logic will skip prompting if this is the first loop and the chat expects no args + input, ok, err = prompter.Readline() + if !ok || err != nil { + return err + } + } + + resp, err = chatter.Chat(ctx, prevState, prg, env, input) + if err != nil || resp.Done { + return err + } + + if resp.Content != "" { + _, err := prompter.Printf(color.RedString("< %s\n", resp.Content)) + if err != nil { + return err + } + } + + prevState = resp.State + } +} diff --git a/pkg/chat/readline.go b/pkg/chat/readline.go new file mode 100644 index 00000000..029ddb90 --- /dev/null +++ b/pkg/chat/readline.go @@ -0,0 +1,66 @@ +package chat + +import ( + "errors" + "fmt" + "io" + "strings" + + "github.com/adrg/xdg" + "github.com/chzyer/readline" + "github.com/fatih/color" + "github.com/gptscript-ai/gptscript/pkg/mvl" +) + +var _ Prompter = (*readlinePrompter)(nil) + +type readlinePrompter struct { + readliner *readline.Instance +} + +func newReadlinePrompter() (*readlinePrompter, error) { + historyFile, err := xdg.CacheFile("gptscript/chat.history") + if err != nil { + historyFile = "" + } + + l, err := readline.NewEx(&readline.Config{ + Prompt: color.GreenString("> "), + HistoryFile: historyFile, + InterruptPrompt: "^C", + EOFPrompt: "exit", + HistorySearchFold: true, + }) + if err != nil { + return nil, err + } + + l.CaptureExitSignal() + mvl.SetOutput(l.Stderr()) + + return &readlinePrompter{ + readliner: l, + }, nil +} + +func (r *readlinePrompter) Printf(format string, args ...interface{}) (int, error) { + return fmt.Fprintf(r.readliner.Stdout(), format, args...) +} + +func (r *readlinePrompter) Readline() (string, bool, error) { + line, err := r.readliner.Readline() + if errors.Is(err, readline.ErrInterrupt) { + return "", false, nil + } else if errors.Is(err, io.EOF) { + return "", false, nil + } + return strings.TrimSpace(line), true, nil +} + +func (r *readlinePrompter) SetPrompt(prompt string) { + r.readliner.SetPrompt(prompt) +} + +func (r *readlinePrompter) Close() error { + return r.readliner.Close() +} diff --git a/pkg/cli/eval.go b/pkg/cli/eval.go index c9491177..be59ece5 100644 --- a/pkg/cli/eval.go +++ b/pkg/cli/eval.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/gptscript-ai/gptscript/pkg/chat" "github.com/gptscript-ai/gptscript/pkg/gptscript" "github.com/gptscript-ai/gptscript/pkg/input" "github.com/gptscript-ai/gptscript/pkg/loader" @@ -15,6 +16,7 @@ import ( type Eval struct { Tools []string `usage:"Tools available to call"` + Chat bool `usage:"Enable chat"` MaxTokens int `usage:"Maximum number of tokens to output"` Model string `usage:"The model to use"` JSON bool `usage:"Output JSON"` @@ -33,6 +35,7 @@ func (e *Eval) Run(cmd *cobra.Command, args []string) error { ModelName: e.Model, JSONResponse: e.JSON, InternalPrompt: e.InternalPrompt, + Chat: e.Chat, }, Instructions: strings.Join(args, " "), } @@ -66,6 +69,12 @@ func (e *Eval) Run(cmd *cobra.Command, args []string) error { return err } + if e.Chat { + return chat.Start(e.gptscript.NewRunContext(cmd), nil, runner, func() (types.Program, error) { + return prg, nil + }, os.Environ(), toolInput) + } + toolOutput, err := runner.Run(e.gptscript.NewRunContext(cmd), prg, os.Environ(), toolInput) if err != nil { return err diff --git a/pkg/cli/gptscript.go b/pkg/cli/gptscript.go index 2a210f26..095131d7 100644 --- a/pkg/cli/gptscript.go +++ b/pkg/cli/gptscript.go @@ -2,6 +2,7 @@ package cli import ( "context" + "encoding/json" "fmt" "io" "os" @@ -14,6 +15,7 @@ import ( "github.com/gptscript-ai/gptscript/pkg/assemble" "github.com/gptscript-ai/gptscript/pkg/builtin" "github.com/gptscript-ai/gptscript/pkg/cache" + "github.com/gptscript-ai/gptscript/pkg/chat" "github.com/gptscript-ai/gptscript/pkg/confirm" "github.com/gptscript-ai/gptscript/pkg/gptscript" "github.com/gptscript-ai/gptscript/pkg/input" @@ -57,6 +59,10 @@ type GPTScript struct { 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"` + ForceChat bool `usage:"Force an interactive chat session if even the top level tool is not a chat tool"` + + readData []byte } func New() *cobra.Command { @@ -207,11 +213,17 @@ func (r *GPTScript) PersistentPre(*cobra.Command, []string) error { r.Quiet = new(bool) } else { r.Quiet = &[]bool{true}[0] + if r.Color == nil { + r.Color = new(bool) + } } } if r.Debug { mvl.SetDebug() + if r.Color == nil { + r.Color = new(bool) + } } else { mvl.SetSimpleFormat() if *r.Quiet { @@ -245,9 +257,18 @@ func (r *GPTScript) readProgram(ctx context.Context, args []string) (prg types.P } if args[0] == "-" { - data, err := io.ReadAll(os.Stdin) - if err != nil { - return prg, err + var ( + data []byte + err error + ) + if len(r.readData) > 0 { + data = r.readData + } else { + data, err = io.ReadAll(os.Stdin) + if err != nil { + return prg, err + } + r.readData = data } return loader.ProgramFromSource(ctx, string(data), r.SubTool) } @@ -349,6 +370,24 @@ func (r *GPTScript) Run(cmd *cobra.Command, args []string) (retErr error) { return err } + if r.ChatState != "" { + resp, err := gptScript.Chat(r.NewRunContext(cmd), r.ChatState, prg, os.Environ(), toolInput) + if err != nil { + return err + } + data, err := json.Marshal(resp) + if err != nil { + return err + } + return r.PrintOutput(toolInput, string(data)) + } + + if prg.IsChat() || r.ForceChat { + return chat.Start(r.NewRunContext(cmd), nil, gptScript, func() (types.Program, error) { + return r.readProgram(ctx, args) + }, os.Environ(), toolInput) + } + s, err := gptScript.Run(r.NewRunContext(cmd), prg, os.Environ(), toolInput) if err != nil { return err diff --git a/pkg/engine/cmd.go b/pkg/engine/cmd.go index 8ceab4a3..65c249b4 100644 --- a/pkg/engine/cmd.go +++ b/pkg/engine/cmd.go @@ -21,7 +21,7 @@ import ( "github.com/gptscript-ai/gptscript/pkg/version" ) -func (e *Engine) runCommand(ctx context.Context, tool types.Tool, input string, isCredential bool) (cmdOut string, cmdErr error) { +func (e *Engine) runCommand(ctx context.Context, tool types.Tool, input string, toolCategory ToolCategory) (cmdOut string, cmdErr error) { id := fmt.Sprint(atomic.AddInt64(&completionID, 1)) defer func() { @@ -65,7 +65,7 @@ func (e *Engine) runCommand(ctx context.Context, tool types.Tool, input string, cmd.Stderr = io.MultiWriter(all, os.Stderr) cmd.Stdout = io.MultiWriter(all, output) - if isCredential { + if toolCategory == CredentialToolCategory { pause := context2.GetPauseFuncFromCtx(ctx) unpause := pause() defer unpause() diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index d8a30ec7..c123fae6 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -8,6 +8,7 @@ import ( "sync" "sync/atomic" + "github.com/gptscript-ai/gptscript/pkg/system" "github.com/gptscript-ai/gptscript/pkg/types" "github.com/gptscript-ai/gptscript/pkg/version" ) @@ -37,9 +38,9 @@ type State struct { } type Return struct { - State *State - Calls map[string]Call - Result *string + State *State `json:"state,omitempty"` + Calls map[string]Call `json:"calls,omitempty"` + Result *string `json:"result,omitempty"` } type Call struct { @@ -51,35 +52,43 @@ type CallResult struct { ToolID string `json:"toolID,omitempty"` CallID string `json:"callID,omitempty"` Result string `json:"result,omitempty"` + User string `json:"user,omitempty"` } type commonContext struct { ID string `json:"id"` Tool types.Tool `json:"tool"` InputContext []InputContext `json:"inputContext"` - // IsCredential indicates that the current call is for a credential tool - IsCredential bool `json:"isCredential"` + ToolCategory ToolCategory `json:"toolCategory,omitempty"` +} + +type CallContext struct { + commonContext `json:",inline"` + ToolName string `json:"toolName,omitempty"` + ParentID string `json:"parentID,omitempty"` } type Context struct { commonContext - Ctx context.Context - Parent *Context - Program *types.Program - CredentialContext string + Ctx context.Context + Parent *Context + Program *types.Program + ToolCategory ToolCategory } +type ToolCategory string + +const ( + CredentialToolCategory ToolCategory = "credential" + ContextToolCategory ToolCategory = "context" + NoCategory ToolCategory = "" +) + type InputContext struct { ToolID string `json:"toolID,omitempty"` Content string `json:"content,omitempty"` } -type BasicContext struct { - commonContext `json:",inline"` - ToolName string `json:"toolName,omitempty"` - ParentID string `json:"parentID,omitempty"` -} - func (c *Context) ParentID() string { if c.Parent == nil { return "" @@ -87,7 +96,7 @@ func (c *Context) ParentID() string { return c.Parent.ID } -func (c *Context) ToBasicContext() *BasicContext { +func (c *Context) GetCallContext() *CallContext { var toolName string if c.Parent != nil { for name, id := range c.Parent.Tool.ToolMapping { @@ -98,7 +107,7 @@ func (c *Context) ToBasicContext() *BasicContext { } } - return &BasicContext{ + return &CallContext{ commonContext: c.commonContext, ParentID: c.ParentID(), ToolName: toolName, @@ -110,7 +119,7 @@ func (c *Context) UnmarshalJSON([]byte) error { } func (c *Context) MarshalJSON() ([]byte, error) { - return json.Marshal(c.ToBasicContext()) + return json.Marshal(c.GetCallContext()) } var execID int32 @@ -127,7 +136,7 @@ func NewContext(ctx context.Context, prg *types.Program) Context { return callCtx } -func (c *Context) SubCall(ctx context.Context, toolID, callID string, isCredentialTool bool) (Context, error) { +func (c *Context) SubCall(ctx context.Context, toolID, callID string, toolCategory ToolCategory) (Context, error) { tool, ok := c.Program.ToolSet[toolID] if !ok { return Context{}, fmt.Errorf("failed to file tool for id [%s]", toolID) @@ -141,7 +150,7 @@ func (c *Context) SubCall(ctx context.Context, toolID, callID string, isCredenti commonContext: commonContext{ ID: callID, Tool: tool, - IsCredential: isCredentialTool, + ToolCategory: toolCategory, }, Ctx: ctx, Parent: c, @@ -170,8 +179,10 @@ func (e *Engine) Start(ctx Context, input string) (*Return, error) { return e.runDaemon(ctx.Ctx, ctx.Program, tool, input) } else if tool.IsOpenAPI() { return e.runOpenAPI(tool, input) + } else if tool.IsPrint() { + return e.runPrint(tool) } - s, err := e.runCommand(ctx.WrappedContext(), tool, input, ctx.IsCredential) + s, err := e.runCommand(ctx.WrappedContext(), tool, input, ctx.ToolCategory) if err != nil { return nil, err } @@ -180,7 +191,7 @@ func (e *Engine) Start(ctx Context, input string) (*Return, error) { }, nil } - if ctx.IsCredential { + if ctx.ToolCategory == CredentialToolCategory { return nil, fmt.Errorf("credential tools cannot make calls to the LLM") } @@ -193,12 +204,36 @@ func (e *Engine) Start(ctx Context, input string) (*Return, error) { InternalSystemPrompt: tool.Parameters.InternalPrompt, } + if tool.Chat && completion.InternalSystemPrompt == nil { + completion.InternalSystemPrompt = new(bool) + } + var err error completion.Tools, err = tool.GetCompletionTools(*ctx.Program) if err != nil { return nil, err } + completion.Messages = addUpdateSystem(ctx, tool, completion.Messages) + + if _, def := system.IsDefaultPrompt(input); tool.Chat && def { + // Ignore "default prompts" from chat + input = "" + } + + if input != "" { + completion.Messages = append(completion.Messages, types.CompletionMessage{ + Role: types.CompletionMessageRoleTypeUser, + Content: types.Text(input), + }) + } + + return e.complete(ctx.Ctx, &State{ + Completion: completion, + }) +} + +func addUpdateSystem(ctx Context, tool types.Tool, msgs []types.CompletionMessage) []types.CompletionMessage { var instructions []string for _, context := range ctx.InputContext { @@ -209,23 +244,20 @@ func (e *Engine) Start(ctx Context, input string) (*Return, error) { instructions = append(instructions, tool.Instructions) } - if len(instructions) > 0 { - completion.Messages = append(completion.Messages, types.CompletionMessage{ - Role: types.CompletionMessageRoleTypeSystem, - Content: types.Text(strings.Join(instructions, "\n")), - }) + if len(instructions) == 0 { + return msgs } - if input != "" { - completion.Messages = append(completion.Messages, types.CompletionMessage{ - Role: types.CompletionMessageRoleTypeUser, - Content: types.Text(input), - }) + msg := types.CompletionMessage{ + Role: types.CompletionMessageRoleTypeSystem, + Content: types.Text(strings.Join(instructions, "\n")), } - return e.complete(ctx.Ctx, &State{ - Completion: completion, - }) + if len(msgs) > 0 && msgs[0].Role == types.CompletionMessageRoleTypeSystem { + return append([]types.CompletionMessage{msg}, msgs[1:]...) + } + + return append([]types.CompletionMessage{msg}, msgs...) } func (e *Engine) complete(ctx context.Context, state *State) (*Return, error) { @@ -285,7 +317,9 @@ func (e *Engine) complete(ctx context.Context, state *State) (*Return, error) { return &ret, nil } -func (e *Engine) Continue(ctx context.Context, state *State, results ...CallResult) (*Return, error) { +func (e *Engine) Continue(ctx Context, state *State, results ...CallResult) (*Return, error) { + var added bool + state = &State{ Completion: state.Completion, Pending: state.Pending, @@ -293,7 +327,16 @@ func (e *Engine) Continue(ctx context.Context, state *State, results ...CallResu } for _, result := range results { - state.Results[result.CallID] = result + if result.CallID != "" { + state.Results[result.CallID] = result + } + if result.User != "" { + added = true + state.Completion.Messages = append(state.Completion.Messages, types.CompletionMessage{ + Role: types.CompletionMessageRoleTypeUser, + Content: types.Text(result.User), + }) + } } ret := Return{ @@ -301,10 +344,6 @@ func (e *Engine) Continue(ctx context.Context, state *State, results ...CallResu Calls: map[string]Call{}, } - var ( - added bool - ) - for id, pending := range state.Pending { if _, ok := state.Results[id]; !ok { ret.Calls[id] = Call{ @@ -315,6 +354,7 @@ func (e *Engine) Continue(ctx context.Context, state *State, results ...CallResu } if len(ret.Calls) > 0 { + // Outstanding tool calls still pending return &ret, nil } @@ -346,5 +386,6 @@ func (e *Engine) Continue(ctx context.Context, state *State, results ...CallResu return nil, fmt.Errorf("invalid continue call, no completion needed") } - return e.complete(ctx, state) + state.Completion.Messages = addUpdateSystem(ctx, ctx.Tool, state.Completion.Messages) + return e.complete(ctx.Ctx, state) } diff --git a/pkg/engine/print.go b/pkg/engine/print.go new file mode 100644 index 00000000..f9f2a0a0 --- /dev/null +++ b/pkg/engine/print.go @@ -0,0 +1,26 @@ +package engine + +import ( + "fmt" + "strings" + "sync/atomic" + + "github.com/gptscript-ai/gptscript/pkg/types" +) + +func (e *Engine) runPrint(tool types.Tool) (cmdOut *Return, cmdErr error) { + id := fmt.Sprint(atomic.AddInt64(&completionID, 1)) + out := strings.TrimPrefix(tool.Instructions, types.PrintPrefix+"\n") + + e.Progress <- types.CompletionStatus{ + CompletionID: id, + Response: map[string]any{ + "output": out, + "err": nil, + }, + } + + return &Return{ + Result: &out, + }, nil +} diff --git a/pkg/gptscript/gptscript.go b/pkg/gptscript/gptscript.go index 08c85b4d..ff9c70f2 100644 --- a/pkg/gptscript/gptscript.go +++ b/pkg/gptscript/gptscript.go @@ -94,6 +94,10 @@ func New(opts *Options) (*GPTScript, error) { }, nil } +func (g *GPTScript) Chat(ctx context.Context, prevState runner.ChatState, prg types.Program, env []string, input string) (runner.ChatResponse, error) { + return g.Runner.Chat(ctx, prevState, prg, env, input) +} + func (g *GPTScript) Run(ctx context.Context, prg types.Program, envs []string, input string) (string, error) { return g.Runner.Run(ctx, prg, envs, input) } diff --git a/pkg/loader/loader.go b/pkg/loader/loader.go index aaebca80..544cf676 100644 --- a/pkg/loader/loader.go +++ b/pkg/loader/loader.go @@ -9,14 +9,17 @@ import ( "io" "io/fs" "os" + "path" "path/filepath" "slices" "strings" + "unicode/utf8" "github.com/getkin/kin-openapi/openapi3" "github.com/gptscript-ai/gptscript/pkg/assemble" "github.com/gptscript-ai/gptscript/pkg/builtin" "github.com/gptscript-ai/gptscript/pkg/parser" + "github.com/gptscript-ai/gptscript/pkg/system" "github.com/gptscript-ai/gptscript/pkg/types" "gopkg.in/yaml.v3" ) @@ -131,6 +134,17 @@ func readTool(ctx context.Context, prg *types.Program, base *source, targetToolN } } + if ext := path.Ext(base.Name); len(tools) == 0 && ext != "" && ext != system.Suffix && utf8.Valid(data) { + tools = []types.Tool{ + { + Parameters: types.Parameters{ + Name: base.Name, + }, + Instructions: types.PrintPrefix + "\n" + string(data), + }, + } + } + // If we didn't get any tools from trying to parse it as OpenAPI, try to parse it as a GPTScript if len(tools) == 0 { tools, err = parser.Parse(bytes.NewReader(data), parser.Options{ diff --git a/pkg/monitor/display.go b/pkg/monitor/display.go index 621d18db..51ac6470 100644 --- a/pkg/monitor/display.go +++ b/pkg/monitor/display.go @@ -13,6 +13,7 @@ import ( "time" "github.com/fatih/color" + "github.com/gptscript-ai/gptscript/pkg/engine" "github.com/gptscript-ai/gptscript/pkg/runner" "github.com/gptscript-ai/gptscript/pkg/types" ) @@ -20,12 +21,14 @@ import ( type Options struct { DisplayProgress bool `usage:"-"` DumpState string `usage:"Dump the internal execution state to a file"` + DebugMessages bool `usage:"Enable logging of chat completion calls"` } func complete(opts ...Options) (result Options) { for _, opt := range opts { result.DumpState = types.FirstSet(opt.DumpState, result.DumpState) result.DisplayProgress = types.FirstSet(opt.DisplayProgress, result.DisplayProgress) + result.DebugMessages = types.FirstSet(opt.DebugMessages, result.DebugMessages) } return } @@ -33,6 +36,7 @@ func complete(opts ...Options) (result Options) { type Console struct { dumpState string displayProgress bool + printMessages bool } var ( @@ -42,7 +46,7 @@ var ( func (c *Console) Start(_ context.Context, prg *types.Program, _ []string, input string) (runner.Monitor, error) { id := atomic.AddInt64(&runID, 1) - mon := newDisplay(c.dumpState, c.displayProgress) + mon := newDisplay(c.dumpState, c.displayProgress, c.printMessages) mon.dump.ID = fmt.Sprint(id) mon.dump.Program = prg mon.dump.Input = input @@ -52,11 +56,12 @@ func (c *Console) Start(_ context.Context, prg *types.Program, _ []string, input } type display struct { - dump dump - livePrinter *livePrinter - dumpState string - callIDMap map[string]string - callLock sync.Mutex + dump dump + printMessages bool + livePrinter *livePrinter + dumpState string + callIDMap map[string]string + callLock sync.Mutex } type livePrinter struct { @@ -217,7 +222,7 @@ func (d *display) Event(event runner.Event) { call: ¤tCall, prg: d.dump.Program, calls: d.dump.Calls, - credential: event.CallContext.IsCredential, + toolCategory: event.CallContext.ToolCategory, userSpecifiedToolName: event.CallContext.ToolName, } @@ -251,7 +256,11 @@ func (d *display) Event(event runner.Event) { "request", toJSON(event.ChatRequest), ) } - log.Debugf("messages") + if d.printMessages { + log.Infof("messages") + } else { + log.Debugf("debug") + } currentCall.Messages = append(currentCall.Messages, message{ CompletionID: event.ChatCompletionID, Request: event.ChatRequest, @@ -290,13 +299,15 @@ func NewConsole(opts ...Options) *Console { return &Console{ dumpState: opt.DumpState, displayProgress: opt.DisplayProgress, + printMessages: opt.DebugMessages, } } -func newDisplay(dumpState string, progress bool) *display { +func newDisplay(dumpState string, progress, printMessages bool) *display { display := &display{ - dumpState: dumpState, - callIDMap: make(map[string]string), + dumpState: dumpState, + callIDMap: make(map[string]string), + printMessages: printMessages, } if progress { display.livePrinter = &livePrinter{ @@ -345,7 +356,7 @@ type callName struct { call *call prg *types.Program calls []call - credential bool + toolCategory engine.ToolCategory userSpecifiedToolName string } @@ -355,12 +366,12 @@ func (c callName) String() string { currentCall = c.call ) - if c.credential { + if c.toolCategory != engine.NoCategory { // We want to print the credential tool in the same format that the user referenced it, if possible. if c.userSpecifiedToolName != "" { - return "credential: " + color.YellowString(c.userSpecifiedToolName) + return fmt.Sprintf("%s: %s", c.toolCategory, color.YellowString(c.userSpecifiedToolName)) } - return "credential: " + color.YellowString(currentCall.ToolID) + return fmt.Sprintf("%s: %s", c.toolCategory, color.YellowString(currentCall.ToolID)) } for { diff --git a/pkg/mvl/log.go b/pkg/mvl/log.go index dd40b27e..422d0644 100644 --- a/pkg/mvl/log.go +++ b/pkg/mvl/log.go @@ -2,6 +2,7 @@ package mvl import ( "fmt" + "io" "os" "runtime" "strings" @@ -28,6 +29,15 @@ func (f formatter) Format(entry *logrus.Entry) ([]byte, error) { if i, ok := entry.Data["input"].(string); ok && i != "" { msg += fmt.Sprintf(" [input=%s]", i) } + if i, ok := entry.Data["output"].(string); ok && i != "" { + msg += fmt.Sprintf(" [output=%s]", i) + } + if i, ok := entry.Data["request"]; ok && i != "" { + msg += fmt.Sprintf(" [request=%s]", i) + } + if i, ok := entry.Data["response"]; ok && i != "" { + msg += fmt.Sprintf(" [response=%s]", i) + } return []byte(fmt.Sprintf("%s %s\n", entry.Time.Format(time.TimeOnly), msg)), nil @@ -67,6 +77,10 @@ func New(name string) Logger { } } +func SetOutput(out io.Writer) { + logrus.SetOutput(out) +} + type Logger struct { log *logrus.Logger fields logrus.Fields diff --git a/pkg/openai/client.go b/pkg/openai/client.go index 8d47a98b..6cf9f41b 100644 --- a/pkg/openai/client.go +++ b/pkg/openai/client.go @@ -234,17 +234,10 @@ func toMessages(request types.CompletionRequest) (result []openai.ChatCompletion systemPrompts = append(systemPrompts, system.InternalSystemPrompt) } - for i, message := range request.Messages { + for _, message := range request.Messages { if message.Role == types.CompletionMessageRoleTypeSystem { - // Append if the next message is system or user, otherwise set as user message - if i == len(request.Messages)-1 || - (request.Messages[i+1].Role != types.CompletionMessageRoleTypeSystem && - request.Messages[i+1].Role != types.CompletionMessageRoleTypeUser) { - message.Role = types.CompletionMessageRoleTypeUser - } else { - systemPrompts = append(systemPrompts, message.Content[0].Text) - continue - } + systemPrompts = append(systemPrompts, message.Content[0].Text) + continue } msgs = append(msgs, message) } diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index 4f3415cd..25ebab1b 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -94,6 +94,12 @@ func isParam(line string, tool *types.Tool) (_ bool, err error) { return false, err } tool.Parameters.InternalPrompt = &v + case "chat": + v, err := toBool(value) + if err != nil { + return false, err + } + tool.Parameters.Chat = v case "export": tool.Parameters.Export = append(tool.Parameters.Export, csv(strings.ToLower(value))...) case "tool", "tools": @@ -176,7 +182,8 @@ func (c *context) finish(tools *[]types.Tool) { if c.tool.Instructions != "" || c.tool.Parameters.Name != "" || len(c.tool.Export) > 0 || len(c.tool.Tools) > 0 || c.tool.GlobalModelName != "" || - len(c.tool.GlobalTools) > 0 { + len(c.tool.GlobalTools) > 0 || + c.tool.Chat { *tools = append(*tools, c.tool) } *c = context{} @@ -188,7 +195,7 @@ type Options struct { func complete(opts ...Options) (result Options) { for _, opt := range opts { - result.AssignGlobals = types.FirstSet(result.AssignGlobals, opt.AssignGlobals) + result.AssignGlobals = types.FirstSet(opt.AssignGlobals, result.AssignGlobals) } return } diff --git a/pkg/runner/parallel.go b/pkg/runner/parallel.go new file mode 100644 index 00000000..3908d8ce --- /dev/null +++ b/pkg/runner/parallel.go @@ -0,0 +1,57 @@ +package runner + +import ( + "context" + + "golang.org/x/sync/errgroup" +) + +type dispatcher interface { + Run(func(context.Context) error) + Wait() error +} + +type serialDispatcher struct { + ctx context.Context + err error +} + +func newSerialDispatcher(ctx context.Context) *serialDispatcher { + return &serialDispatcher{ + ctx: ctx, + } +} + +func (s *serialDispatcher) Run(f func(context.Context) error) { + if s.err != nil { + return + } + s.err = f(s.ctx) +} + +func (s *serialDispatcher) Wait() error { + return s.err +} + +type parallelDispatcher struct { + ctx context.Context + eg *errgroup.Group +} + +func newParallelDispatcher(ctx context.Context) *parallelDispatcher { + eg, ctx := errgroup.WithContext(ctx) + return ¶llelDispatcher{ + ctx: ctx, + eg: eg, + } +} + +func (p *parallelDispatcher) Run(f func(context.Context) error) { + p.eg.Go(func() error { + return f(p.ctx) + }) +} + +func (p *parallelDispatcher) Wait() error { + return p.eg.Wait() +} diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 11f8f632..12258a91 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -3,19 +3,20 @@ package runner import ( "context" "encoding/json" + "errors" "fmt" - "os" - "path/filepath" + "sort" "strings" "sync" "time" + "github.com/gptscript-ai/gptscript/pkg/builtin" "github.com/gptscript-ai/gptscript/pkg/config" context2 "github.com/gptscript-ai/gptscript/pkg/context" "github.com/gptscript-ai/gptscript/pkg/credentials" "github.com/gptscript-ai/gptscript/pkg/engine" "github.com/gptscript-ai/gptscript/pkg/types" - "golang.org/x/sync/errgroup" + "golang.org/x/exp/maps" ) type MonitorFactory interface { @@ -34,6 +35,7 @@ type Options struct { StartPort int64 `usage:"-"` EndPort int64 `usage:"-"` CredentialOverride string `usage:"-"` + Sequential bool `usage:"-"` } func complete(opts ...Options) (result Options) { @@ -43,6 +45,7 @@ func complete(opts ...Options) (result Options) { result.StartPort = types.FirstSet(opt.StartPort, result.StartPort) result.EndPort = types.FirstSet(opt.EndPort, result.EndPort) result.CredentialOverride = types.FirstSet(opt.CredentialOverride, result.CredentialOverride) + result.Sequential = types.FirstSet(opt.Sequential, result.Sequential) } if result.MonitorFactory == nil { result.MonitorFactory = noopFactory{} @@ -64,6 +67,7 @@ type Runner struct { credCtx string credMutex sync.Mutex credOverrides string + sequential bool } func New(client engine.Model, credCtx string, opts ...Options) (*Runner, error) { @@ -76,6 +80,7 @@ func New(client engine.Model, credCtx string, opts ...Options) (*Runner, error) credCtx: credCtx, credMutex: sync.Mutex{}, credOverrides: opt.CredentialOverride, + sequential: opt.Sequential, } if opt.StartPort != 0 { @@ -92,6 +97,96 @@ func (r *Runner) Close() { r.ports.CloseDaemons() } +type ErrContinuation struct { + State *State +} + +func (e *ErrContinuation) Prompt() string { + return *e.State.Continuation.Result +} + +func (e *ErrContinuation) Error() string { + return fmt.Sprintf("chat continuation required: %s", e.Prompt()) +} + +type ChatResponse struct { + Done bool `json:"done"` + Content string `json:"content"` + ToolID string `json:"toolID"` + State ChatState `json:"state"` +} + +type ChatState interface{} + +func (r *Runner) Chat(ctx context.Context, prevState ChatState, prg types.Program, env []string, input string) (resp ChatResponse, err error) { + var state *State + + if prevState != nil { + switch v := prevState.(type) { + case *State: + state = v + case string: + if v != "null" { + state = &State{} + if err := json.Unmarshal([]byte(v), state); err != nil { + return resp, fmt.Errorf("failed to unmarshal chat state: %w", err) + } + } + default: + return resp, fmt.Errorf("invalid type for state object: %T", prevState) + } + } + + monitor, err := r.factory.Start(ctx, &prg, env, input) + if err != nil { + return resp, err + } + defer func() { + monitor.Stop(resp.Content, err) + }() + + callCtx := engine.NewContext(ctx, &prg) + if state == nil { + startResult, err := r.start(callCtx, monitor, env, input) + if err != nil { + return resp, err + } + state = &State{ + Continuation: startResult, + } + } else { + state.ResumeInput = &input + } + + state, err = r.resume(callCtx, monitor, env, state) + if err != nil { + return resp, err + } + + if state.Result != nil { + return ChatResponse{ + Done: true, + Content: *state.Result, + }, nil + } + + content, err := state.ContinuationContent() + if err != nil { + return resp, err + } + + toolID, err := state.ContinuationContentToolID() + if err != nil { + return resp, err + } + + return ChatResponse{ + Content: content, + State: state, + ToolID: toolID, + }, nil +} + func (r *Runner) Run(ctx context.Context, prg types.Program, env []string, input string) (output string, err error) { monitor, err := r.factory.Start(ctx, &prg, env, input) if err != nil { @@ -102,12 +197,21 @@ func (r *Runner) Run(ctx context.Context, prg types.Program, env []string, input }() callCtx := engine.NewContext(ctx, &prg) - return r.call(callCtx, monitor, env, input) + state, err := r.call(callCtx, monitor, env, input) + if err != nil { + return "", nil + } + if state.Continuation != nil { + return "", &ErrContinuation{ + State: state, + } + } + return *state.Result, nil } type Event struct { Time time.Time `json:"time,omitempty"` - CallContext *engine.BasicContext `json:"callContext,omitempty"` + CallContext *engine.CallContext `json:"callContext,omitempty"` ToolSubCalls map[string]engine.Call `json:"toolSubCalls,omitempty"` ToolResults int `json:"toolResults,omitempty"` Type EventType `json:"type,omitempty"` @@ -136,19 +240,32 @@ func (r *Runner) getContext(callCtx engine.Context, monitor Monitor, env []strin } for _, toolID := range toolIDs { - content, err := r.subCall(callCtx.Ctx, callCtx, monitor, env, toolID, "", "", false) + content, err := r.subCall(callCtx.Ctx, callCtx, monitor, env, toolID, "", "", engine.ContextToolCategory) if err != nil { return nil, err } + if content.Result == nil { + return nil, fmt.Errorf("context tool can not result in a chat continuation") + } result = append(result, engine.InputContext{ ToolID: toolID, - Content: content, + Content: *content.Result, }) } return result, nil } -func (r *Runner) call(callCtx engine.Context, monitor Monitor, env []string, input string) (string, error) { +func (r *Runner) call(callCtx engine.Context, monitor Monitor, env []string, input string) (*State, error) { + result, err := r.start(callCtx, monitor, env, input) + if err != nil { + return nil, err + } + return r.resume(callCtx, monitor, env, &State{ + Continuation: result, + }) +} + +func (r *Runner) start(callCtx engine.Context, monitor Monitor, env []string, input string) (*engine.Return, error) { progress, progressClose := streamProgress(&callCtx, monitor) defer progressClose() @@ -156,14 +273,14 @@ func (r *Runner) call(callCtx engine.Context, monitor Monitor, env []string, inp var err error env, err = r.handleCredentials(callCtx, monitor, env) if err != nil { - return "", err + return nil, err } } var err error callCtx.InputContext, err = r.getContext(callCtx, monitor, env) if err != nil { - return "", err + return nil, err } e := engine.Engine{ @@ -176,56 +293,167 @@ func (r *Runner) call(callCtx engine.Context, monitor Monitor, env []string, inp monitor.Event(Event{ Time: time.Now(), - CallContext: callCtx.ToBasicContext(), + CallContext: callCtx.GetCallContext(), Type: EventTypeCallStart, Content: input, }) callCtx.Ctx = context2.AddPauseFuncToCtx(callCtx.Ctx, monitor.Pause) - result, err := e.Start(callCtx, input) + return e.Start(callCtx, input) +} + +type State struct { + Continuation *engine.Return `json:"continuation,omitempty"` + ContinuationToolID string `json:"continuationToolID,omitempty"` + Result *string `json:"result,omitempty"` + + ResumeInput *string `json:"resumeInput,omitempty"` + SubCalls []SubCallResult `json:"subCalls,omitempty"` + SubCallID string `json:"subCallID,omitempty"` +} + +func (s State) WithInput(input *string) *State { + s.ResumeInput = input + return &s +} + +func (s State) ContinuationContentToolID() (string, error) { + if s.Continuation.Result != nil { + return s.ContinuationToolID, nil + } + + for _, subCall := range s.SubCalls { + if s.SubCallID == subCall.CallID { + return subCall.State.ContinuationContentToolID() + } + } + return "", fmt.Errorf("illegal state: no result message found in chat response") +} + +func (s State) ContinuationContent() (string, error) { + if s.Continuation.Result != nil { + return *s.Continuation.Result, nil + } + + for _, subCall := range s.SubCalls { + if s.SubCallID == subCall.CallID { + return subCall.State.ContinuationContent() + } + } + return "", fmt.Errorf("illegal state: no result message found in chat response") +} + +type Needed struct { + Content string `json:"content,omitempty"` + Input string `json:"input,omitempty"` +} + +func (r *Runner) resume(callCtx engine.Context, monitor Monitor, env []string, state *State) (*State, error) { + progress, progressClose := streamProgress(&callCtx, monitor) + defer progressClose() + + if len(callCtx.Tool.Credentials) > 0 { + var err error + env, err = r.handleCredentials(callCtx, monitor, env) + if err != nil { + return nil, err + } + } + + var err error + callCtx.InputContext, err = r.getContext(callCtx, monitor, env) if err != nil { - return "", err + return nil, err + } + + e := engine.Engine{ + Model: r.c, + RuntimeManager: r.runtimeManager, + Progress: progress, + Env: env, + Ports: &r.ports, } for { - if result.Result != nil && len(result.Calls) == 0 { + if state.Continuation.Result != nil && len(state.Continuation.Calls) == 0 && state.SubCallID == "" && state.ResumeInput == nil { progressClose() monitor.Event(Event{ Time: time.Now(), - CallContext: callCtx.ToBasicContext(), + CallContext: callCtx.GetCallContext(), Type: EventTypeCallFinish, - Content: *result.Result, + Content: *state.Continuation.Result, }) - if err := recordStateMessage(result.State); err != nil { - // Log a message if failed to record state message so that it doesn't affect the main process if state can't be recorded - log.Infof("Failed to record state message: %v", err) + if callCtx.Tool.Chat { + return &State{ + Continuation: state.Continuation, + ContinuationToolID: callCtx.Tool.ID, + }, nil } - return *result.Result, nil + return &State{ + Result: state.Continuation.Result, + }, nil } monitor.Event(Event{ Time: time.Now(), - CallContext: callCtx.ToBasicContext(), + CallContext: callCtx.GetCallContext(), Type: EventTypeCallSubCalls, - ToolSubCalls: result.Calls, + ToolSubCalls: state.Continuation.Calls, }) - callResults, err := r.subCalls(callCtx, monitor, env, result) - if err != nil { - return "", err + var ( + callResults []SubCallResult + err error + ) + + state, callResults, err = r.subCalls(callCtx, monitor, env, state) + if errMessage := (*builtin.ErrChatFinish)(nil); errors.As(err, &errMessage) && callCtx.Tool.Chat { + return &State{ + Result: &errMessage.Message, + }, nil + } else if err != nil { + return nil, err + } + + var engineResults []engine.CallResult + for _, callResult := range callResults { + if callResult.State.Continuation == nil { + engineResults = append(engineResults, engine.CallResult{ + ToolID: callResult.ToolID, + CallID: callResult.CallID, + Result: *callResult.State.Result, + }) + } else { + return &State{ + Continuation: state.Continuation, + SubCalls: callResults, + SubCallID: callResult.CallID, + }, nil + } + } + + if state.ResumeInput != nil { + engineResults = append(engineResults, engine.CallResult{ + User: *state.ResumeInput, + }) } monitor.Event(Event{ Time: time.Now(), - CallContext: callCtx.ToBasicContext(), + CallContext: callCtx.GetCallContext(), Type: EventTypeCallContinue, ToolResults: len(callResults), }) - result, err = e.Continue(callCtx.Ctx, result.State, callResults...) + nextContinuation, err := e.Continue(callCtx, state.Continuation.State, engineResults...) if err != nil { - return "", err + return nil, err + } + + state = &State{ + Continuation: nextContinuation, + SubCalls: callResults, } } } @@ -241,7 +469,7 @@ func streamProgress(callCtx *engine.Context, monitor Monitor) (chan<- types.Comp if message := status.PartialResponse; message != nil { monitor.Event(Event{ Time: time.Now(), - CallContext: callCtx.ToBasicContext(), + CallContext: callCtx.GetCallContext(), Type: EventTypeCallProgress, ChatCompletionID: status.CompletionID, Content: message.String(), @@ -249,7 +477,7 @@ func streamProgress(callCtx *engine.Context, monitor Monitor) (chan<- types.Comp } else { monitor.Event(Event{ Time: time.Now(), - CallContext: callCtx.ToBasicContext(), + CallContext: callCtx.GetCallContext(), Type: EventTypeChat, ChatCompletionID: status.CompletionID, ChatRequest: status.Request, @@ -269,66 +497,104 @@ func streamProgress(callCtx *engine.Context, monitor Monitor) (chan<- types.Comp } } -func (r *Runner) subCall(ctx context.Context, parentContext engine.Context, monitor Monitor, env []string, toolID, input, callID string, isCredentialTool bool) (string, error) { - callCtx, err := parentContext.SubCall(ctx, toolID, callID, isCredentialTool) +func (r *Runner) subCall(ctx context.Context, parentContext engine.Context, monitor Monitor, env []string, toolID, input, callID string, toolCategory engine.ToolCategory) (*State, error) { + callCtx, err := parentContext.SubCall(ctx, toolID, callID, toolCategory) if err != nil { - return "", err + return nil, err } return r.call(callCtx, monitor, env, input) } -func (r *Runner) subCalls(callCtx engine.Context, monitor Monitor, env []string, lastReturn *engine.Return) (callResults []engine.CallResult, _ error) { +func (r *Runner) subCallResume(ctx context.Context, parentContext engine.Context, monitor Monitor, env []string, toolID, callID string, state *State) (*State, error) { + callCtx, err := parentContext.SubCall(ctx, toolID, callID, engine.NoCategory) + if err != nil { + return nil, err + } + + return r.resume(callCtx, monitor, env, state) +} + +type SubCallResult struct { + ToolID string `json:"toolId,omitempty"` + CallID string `json:"callId,omitempty"` + State *State `json:"state,omitempty"` +} + +func (r *Runner) newDispatcher(ctx context.Context) dispatcher { + if r.sequential { + return newSerialDispatcher(ctx) + } + return newParallelDispatcher(ctx) +} + +func (r *Runner) subCalls(callCtx engine.Context, monitor Monitor, env []string, state *State) (_ *State, callResults []SubCallResult, _ error) { var ( resultLock sync.Mutex ) - eg, subCtx := errgroup.WithContext(callCtx.Ctx) - for id, call := range lastReturn.Calls { - eg.Go(func() error { - result, err := r.subCall(subCtx, callCtx, monitor, env, call.ToolID, call.Input, id, false) + 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) + } + var found bool + for _, subCall := range state.SubCalls { + if subCall.CallID == state.SubCallID { + found = true + subState := *subCall.State + subState.ResumeInput = state.ResumeInput + result, err := r.subCallResume(callCtx.Ctx, callCtx, monitor, env, subCall.ToolID, subCall.CallID, subCall.State.WithInput(state.ResumeInput)) + if err != nil { + return nil, nil, err + } + callResults = append(callResults, SubCallResult{ + ToolID: subCall.ToolID, + CallID: subCall.CallID, + State: result, + }) + // Clear the input, we have already processed it + state = state.WithInput(nil) + } else { + callResults = append(callResults, subCall) + } + } + if !found { + return nil, nil, fmt.Errorf("invalid state, failed to find subCall for subCallID [%s]", state.SubCallID) + } + return state, callResults, nil + } + + d := r.newDispatcher(callCtx.Ctx) + + // Sort the id so if sequential the results are predictable + ids := maps.Keys(state.Continuation.Calls) + sort.Strings(ids) + + for _, id := range ids { + call := state.Continuation.Calls[id] + d.Run(func(ctx context.Context) error { + result, err := r.subCall(ctx, callCtx, monitor, env, call.ToolID, call.Input, id, "") if err != nil { return err } resultLock.Lock() defer resultLock.Unlock() - callResults = append(callResults, engine.CallResult{ + callResults = append(callResults, SubCallResult{ ToolID: call.ToolID, CallID: id, - Result: result, + State: result, }) return nil }) } - if err := eg.Wait(); err != nil { - return nil, err + if err := d.Wait(); err != nil { + return nil, nil, err } - return -} - -// recordStateMessage record the final state of the openai request and fetch messages and tools for analysis purpose -// The name follows `gptscript-state-${hostname}-${unixtimestamp}` -func recordStateMessage(state *engine.State) error { - if state == nil { - return nil - } - tmpdir := os.TempDir() - data, err := json.Marshal(state) - if err != nil { - return err - } - - hostname, err := os.Hostname() - if err != nil { - return err - } - - filename := filepath.Join(tmpdir, fmt.Sprintf("gptscript-state-%v-%v", hostname, time.Now().UnixMilli())) - return os.WriteFile(filename, data, 0444) + return state, callResults, nil } func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env []string) ([]string, error) { @@ -387,19 +653,24 @@ func (r *Runner) handleCredentials(callCtx engine.Context, monitor Monitor, env return nil, fmt.Errorf("failed to find ID for tool %s", credToolName) } - subCtx, err := callCtx.SubCall(callCtx.Ctx, credToolID, "", true) // leaving callID as "" will cause it to be set by the engine + subCtx, err := callCtx.SubCall(callCtx.Ctx, credToolID, "", engine.CredentialToolCategory) // leaving callID as "" will cause it to be set by the engine if err != nil { return nil, fmt.Errorf("failed to create subcall context for tool %s: %w", credToolName, err) } + res, err := r.call(subCtx, monitor, env, "") if err != nil { return nil, fmt.Errorf("failed to run credential tool %s: %w", credToolName, err) } + if res.Result == nil { + return nil, fmt.Errorf("invalid state: credential tool [%s] can not result in a continuation", credToolName) + } + var envMap struct { Env map[string]string `json:"env"` } - if err := json.Unmarshal([]byte(res), &envMap); err != nil { + if err := json.Unmarshal([]byte(*res.Result), &envMap); err != nil { return nil, fmt.Errorf("failed to unmarshal credential tool %s response: %w", credToolName, err) } diff --git a/pkg/tests/runner_test.go b/pkg/tests/runner_test.go index a9662e79..95f3db8d 100644 --- a/pkg/tests/runner_test.go +++ b/pkg/tests/runner_test.go @@ -1,14 +1,523 @@ package tests import ( + "context" + "encoding/json" + "os" "testing" + "github.com/gptscript-ai/gptscript/pkg/runner" "github.com/gptscript-ai/gptscript/pkg/tests/tester" "github.com/gptscript-ai/gptscript/pkg/types" + "github.com/hexops/autogold/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func toJSONString(t *testing.T, v interface{}) string { + t.Helper() + x, err := json.MarshalIndent(v, "", " ") + require.NoError(t, err) + return string(x) +} + +func TestDualSubChat(t *testing.T) { + r := tester.NewRunner(t) + r.RespondWith(tester.Result{ + Content: []types.ContentPart{ + { + ToolCall: &types.CompletionToolCall{ + ID: "call_1", + Function: types.CompletionFunctionCall{ + Name: "chatbot", + Arguments: "Input to chatbot1", + }, + }, + }, + { + ToolCall: &types.CompletionToolCall{ + ID: "call_2", + Function: types.CompletionFunctionCall{ + Name: "chatbot2", + Arguments: "Input to chatbot2", + }, + }, + }, + }, + }, tester.Result{ + Text: "Assistant Response 1 - from chatbot1", + }, tester.Result{ + Text: "Assistent Response 2 - from chatbot2", + }) + + 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{ + Func: types.CompletionFunctionCall{ + Name: types.ToolNormalizer("sys.chat.finish"), + Arguments: `{"message":"Chat done"}`, + }, + }) + + 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("Assistent Response 2 - from chatbot2").Equal(t, resp.Content) + autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step2")) + + r.RespondWith(tester.Result{ + Text: "Assistant 3", + }) + + 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 3").Equal(t, resp.Content) + autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step3")) + + r.RespondWith(tester.Result{ + Func: types.CompletionFunctionCall{ + Name: types.ToolNormalizer("sys.chat.finish"), + Arguments: `{"message":"Chat done2"}`, + }, + }, tester.Result{ + Text: "And we're done", + }) + + resp, err = r.Chat(context.Background(), resp.State, prg, os.Environ(), "User 4") + require.NoError(t, err) + r.AssertResponded(t) + assert.True(t, resp.Done) + autogold.Expect("And we're done").Equal(t, resp.Content) + autogold.ExpectFile(t, toJSONString(t, resp), autogold.Name(t.Name()+"/step4")) +} + +func TestSubChat(t *testing.T) { + r := tester.NewRunner(t) + r.RespondWith(tester.Result{ + Func: types.CompletionFunctionCall{ + Name: "chatbot", + }, + }, tester.Result{ + Text: "Assistant 1", + }, tester.Result{ + Text: "Assistant 2", + }) + + prg, err := r.Load("") + require.NoError(t, err) + + resp, err := r.Chat(context.Background(), nil, prg, os.Environ(), "Hello") + require.NoError(t, err) + + autogold.Expect(`{ + "done": false, + "content": "Assistant 1", + "toolID": "testdata/TestSubChat/test.gpt:6", + "state": { + "continuation": { + "state": { + "completion": { + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": null, + "Tools": [ + { + "function": { + "toolID": "testdata/TestSubChat/test.gpt:6", + "name": "chatbot", + "parameters": null + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "Call chatbot" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Hello" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "chatbot" + } + } + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null + }, + "pending": { + "call_1": { + "index": 0, + "id": "call_1", + "function": { + "name": "chatbot" + } + } + } + }, + "calls": { + "call_1": { + "toolID": "testdata/TestSubChat/test.gpt:6" + } + } + }, + "subCalls": [ + { + "toolId": "testdata/TestSubChat/test.gpt:6", + "callId": "call_1", + "state": { + "continuation": { + "state": { + "completion": { + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": false, + "Tools": null, + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistant 1" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null + } + }, + "result": "Assistant 1" + }, + "continuationToolID": "testdata/TestSubChat/test.gpt:6" + } + } + ], + "subCallID": "call_1" + } +}`).Equal(t, toJSONString(t, resp)) + + resp, err = r.Chat(context.Background(), resp.State, prg, os.Environ(), "User 1") + require.NoError(t, err) + + autogold.Expect(`{ + "done": false, + "content": "Assistant 2", + "toolID": "testdata/TestSubChat/test.gpt:6", + "state": { + "continuation": { + "state": { + "completion": { + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": null, + "Tools": [ + { + "function": { + "toolID": "testdata/TestSubChat/test.gpt:6", + "name": "chatbot", + "parameters": null + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "Call chatbot" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Hello" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "chatbot" + } + } + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null + }, + "pending": { + "call_1": { + "index": 0, + "id": "call_1", + "function": { + "name": "chatbot" + } + } + } + }, + "calls": { + "call_1": { + "toolID": "testdata/TestSubChat/test.gpt:6" + } + } + }, + "subCalls": [ + { + "toolId": "testdata/TestSubChat/test.gpt:6", + "callId": "call_1", + "state": { + "continuation": { + "state": { + "completion": { + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": false, + "Tools": null, + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistant 1" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 1" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistant 2" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null + } + }, + "result": "Assistant 2" + }, + "continuationToolID": "testdata/TestSubChat/test.gpt:6" + } + } + ], + "subCallID": "call_1" + } +}`).Equal(t, toJSONString(t, resp)) +} + +func TestChat(t *testing.T) { + r := tester.NewRunner(t) + r.RespondWith(tester.Result{ + Text: "Assistant 1", + }, tester.Result{ + Text: "Assistant 2", + }) + + prg, err := r.Load("") + require.NoError(t, err) + + resp, err := r.Chat(context.Background(), nil, prg, os.Environ(), "Hello") + require.NoError(t, err) + + autogold.Expect(`{ + "done": false, + "content": "Assistant 1", + "toolID": "testdata/TestChat/test.gpt:1", + "state": { + "continuation": { + "state": { + "completion": { + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": false, + "Tools": null, + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Hello" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistant 1" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null + } + }, + "result": "Assistant 1" + }, + "continuationToolID": "testdata/TestChat/test.gpt:1" + } +}`).Equal(t, toJSONString(t, resp)) + + resp, err = r.Chat(context.Background(), resp.State, prg, os.Environ(), "User 1") + require.NoError(t, err) + + autogold.Expect(`{ + "done": false, + "content": "Assistant 2", + "toolID": "testdata/TestChat/test.gpt:1", + "state": { + "continuation": { + "state": { + "completion": { + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": false, + "Tools": null, + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Hello" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistant 1" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 1" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistant 2" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null + } + }, + "result": "Assistant 2" + }, + "continuationToolID": "testdata/TestChat/test.gpt:1" + } +}`).Equal(t, toJSONString(t, resp)) +} + +func TestChatRunError(t *testing.T) { + r := tester.NewRunner(t) + _, err := r.Run("", "") + autogold.Expect(`"{\n \"continuation\": {\n \"state\": {\n \"completion\": {\n \"Model\": \"gpt-4-turbo-preview\",\n \"InternalSystemPrompt\": false,\n \"Tools\": null,\n \"Messages\": [\n {\n \"role\": \"system\",\n \"content\": [\n {\n \"text\": \"This is a chatbot\"\n }\n ]\n },\n {\n \"role\": \"assistant\",\n \"content\": [\n {\n \"text\": \"TEST RESULT CALL: 1\"\n }\n ]\n }\n ],\n \"MaxTokens\": 0,\n \"Temperature\": null,\n \"JSONResponse\": false,\n \"Grammar\": \"\",\n \"Cache\": null\n }\n },\n \"result\": \"TEST RESULT CALL: 1\"\n },\n \"continuationToolID\": \"testdata/TestChatRunError/test.gpt:1\"\n}"`).Equal(t, toJSONString(t, toJSONString(t, err.(*runner.ErrContinuation).State))) +} + func TestExportContext(t *testing.T) { runner := tester.NewRunner(t) x := runner.RunDefault() diff --git a/pkg/tests/testdata/TestChat/call1.golden b/pkg/tests/testdata/TestChat/call1.golden new file mode 100644 index 00000000..fd057486 --- /dev/null +++ b/pkg/tests/testdata/TestChat/call1.golden @@ -0,0 +1,28 @@ +`{ + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": false, + "Tools": null, + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Hello" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null +}` diff --git a/pkg/tests/testdata/TestChat/call2.golden b/pkg/tests/testdata/TestChat/call2.golden new file mode 100644 index 00000000..1a8f7180 --- /dev/null +++ b/pkg/tests/testdata/TestChat/call2.golden @@ -0,0 +1,44 @@ +`{ + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": false, + "Tools": null, + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Hello" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistant 1" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 1" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null +}` diff --git a/pkg/tests/testdata/TestChat/test.gpt b/pkg/tests/testdata/TestChat/test.gpt new file mode 100644 index 00000000..a856fce6 --- /dev/null +++ b/pkg/tests/testdata/TestChat/test.gpt @@ -0,0 +1,3 @@ +chat: true + +This is a chatbot \ No newline at end of file diff --git a/pkg/tests/testdata/TestChatRunError/call1.golden b/pkg/tests/testdata/TestChatRunError/call1.golden new file mode 100644 index 00000000..90b14ab6 --- /dev/null +++ b/pkg/tests/testdata/TestChatRunError/call1.golden @@ -0,0 +1,20 @@ +`{ + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": false, + "Tools": null, + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null +}` diff --git a/pkg/tests/testdata/TestChatRunError/test.gpt b/pkg/tests/testdata/TestChatRunError/test.gpt new file mode 100644 index 00000000..a856fce6 --- /dev/null +++ b/pkg/tests/testdata/TestChatRunError/test.gpt @@ -0,0 +1,3 @@ +chat: true + +This is a chatbot \ No newline at end of file diff --git a/pkg/tests/testdata/TestDualSubChat/call1.golden b/pkg/tests/testdata/TestDualSubChat/call1.golden new file mode 100644 index 00000000..83f98829 --- /dev/null +++ b/pkg/tests/testdata/TestDualSubChat/call1.golden @@ -0,0 +1,43 @@ +`{ + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": null, + "Tools": [ + { + "function": { + "toolID": "testdata/TestDualSubChat/test.gpt:6", + "name": "chatbot", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestDualSubChat/test.gpt:13", + "name": "chatbot2", + "parameters": null + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "Call chatbots" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 1" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null +}` diff --git a/pkg/tests/testdata/TestDualSubChat/call2.golden b/pkg/tests/testdata/TestDualSubChat/call2.golden new file mode 100644 index 00000000..fc0453bb --- /dev/null +++ b/pkg/tests/testdata/TestDualSubChat/call2.golden @@ -0,0 +1,45 @@ +`{ + "Model": "gpt-4-turbo-preview", + "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": { + "summary": { + "description": "A summary of the dialog", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Input to chatbot1" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null +}` diff --git a/pkg/tests/testdata/TestDualSubChat/call3.golden b/pkg/tests/testdata/TestDualSubChat/call3.golden new file mode 100644 index 00000000..ef80540d --- /dev/null +++ b/pkg/tests/testdata/TestDualSubChat/call3.golden @@ -0,0 +1,45 @@ +`{ + "Model": "gpt-4-turbo-preview", + "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": { + "summary": { + "description": "A summary of the dialog", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot2" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Input to chatbot2" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null +}` diff --git a/pkg/tests/testdata/TestDualSubChat/call4.golden b/pkg/tests/testdata/TestDualSubChat/call4.golden new file mode 100644 index 00000000..9d7965b1 --- /dev/null +++ b/pkg/tests/testdata/TestDualSubChat/call4.golden @@ -0,0 +1,61 @@ +`{ + "Model": "gpt-4-turbo-preview", + "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": { + "summary": { + "description": "A summary of the dialog", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Input to chatbot1" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistant Response 1 - from chatbot1" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 2" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null +}` diff --git a/pkg/tests/testdata/TestDualSubChat/call5.golden b/pkg/tests/testdata/TestDualSubChat/call5.golden new file mode 100644 index 00000000..26386251 --- /dev/null +++ b/pkg/tests/testdata/TestDualSubChat/call5.golden @@ -0,0 +1,61 @@ +`{ + "Model": "gpt-4-turbo-preview", + "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": { + "summary": { + "description": "A summary of the dialog", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot2" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Input to chatbot2" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistent Response 2 - from chatbot2" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 3" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null +}` diff --git a/pkg/tests/testdata/TestDualSubChat/call6.golden b/pkg/tests/testdata/TestDualSubChat/call6.golden new file mode 100644 index 00000000..494129dd --- /dev/null +++ b/pkg/tests/testdata/TestDualSubChat/call6.golden @@ -0,0 +1,77 @@ +`{ + "Model": "gpt-4-turbo-preview", + "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": { + "summary": { + "description": "A summary of the dialog", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot2" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Input to chatbot2" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistent Response 2 - from chatbot2" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 3" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistant 3" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 4" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null +}` diff --git a/pkg/tests/testdata/TestCwd.golden b/pkg/tests/testdata/TestDualSubChat/call7.golden similarity index 65% rename from pkg/tests/testdata/TestCwd.golden rename to pkg/tests/testdata/TestDualSubChat/call7.golden index f564ec3f..9e6b853a 100644 --- a/pkg/tests/testdata/TestCwd.golden +++ b/pkg/tests/testdata/TestDualSubChat/call7.golden @@ -4,13 +4,15 @@ "Tools": [ { "function": { - "name": "test", + "toolID": "testdata/TestDualSubChat/test.gpt:6", + "name": "chatbot", "parameters": null } }, { "function": { - "name": "local", + "toolID": "testdata/TestDualSubChat/test.gpt:13", + "name": "chatbot2", "parameters": null } } @@ -20,7 +22,15 @@ "role": "system", "content": [ { - "text": "noop" + "text": "Call chatbots" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 1" } ] }, @@ -32,7 +42,18 @@ "index": 0, "id": "call_1", "function": { - "name": "test" + "name": "chatbot", + "arguments": "Input to chatbot1" + } + } + }, + { + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "chatbot2", + "arguments": "Input to chatbot2" } } } @@ -42,43 +63,31 @@ "role": "tool", "content": [ { - "text": "sub\nsub" + "text": "Chat done" } ], "toolCall": { "index": 0, "id": "call_1", "function": { - "name": "test" + "name": "chatbot", + "arguments": "Input to chatbot1" } } }, - { - "role": "assistant", - "content": [ - { - "toolCall": { - "index": 1, - "id": "call_2", - "function": { - "name": "local" - } - } - } - ] - }, { "role": "tool", "content": [ { - "text": "/testdata/TestCwd\nthe data" + "text": "Chat done2" } ], "toolCall": { "index": 1, "id": "call_2", "function": { - "name": "local" + "name": "chatbot2", + "arguments": "Input to chatbot2" } } } diff --git a/pkg/tests/testdata/TestDualSubChat/step1.golden b/pkg/tests/testdata/TestDualSubChat/step1.golden new file mode 100644 index 00000000..503f946a --- /dev/null +++ b/pkg/tests/testdata/TestDualSubChat/step1.golden @@ -0,0 +1,240 @@ +`{ + "done": false, + "content": "Assistant Response 1 - from chatbot1", + "toolID": "testdata/TestDualSubChat/test.gpt:6", + "state": { + "continuation": { + "state": { + "completion": { + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": null, + "Tools": [ + { + "function": { + "toolID": "testdata/TestDualSubChat/test.gpt:6", + "name": "chatbot", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestDualSubChat/test.gpt:13", + "name": "chatbot2", + "parameters": null + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "Call chatbots" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 1" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "chatbot", + "arguments": "Input to chatbot1" + } + } + }, + { + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "chatbot2", + "arguments": "Input to chatbot2" + } + } + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null + }, + "pending": { + "call_1": { + "index": 0, + "id": "call_1", + "function": { + "name": "chatbot", + "arguments": "Input to chatbot1" + } + }, + "call_2": { + "index": 1, + "id": "call_2", + "function": { + "name": "chatbot2", + "arguments": "Input to chatbot2" + } + } + } + }, + "calls": { + "call_1": { + "toolID": "testdata/TestDualSubChat/test.gpt:6", + "input": "Input to chatbot1" + }, + "call_2": { + "toolID": "testdata/TestDualSubChat/test.gpt:13", + "input": "Input to chatbot2" + } + } + }, + "subCalls": [ + { + "toolId": "testdata/TestDualSubChat/test.gpt:6", + "callId": "call_1", + "state": { + "continuation": { + "state": { + "completion": { + "Model": "gpt-4-turbo-preview", + "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": { + "summary": { + "description": "A summary of the dialog", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Input to chatbot1" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistant Response 1 - from chatbot1" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null + } + }, + "result": "Assistant Response 1 - from chatbot1" + }, + "continuationToolID": "testdata/TestDualSubChat/test.gpt:6" + } + }, + { + "toolId": "testdata/TestDualSubChat/test.gpt:13", + "callId": "call_2", + "state": { + "continuation": { + "state": { + "completion": { + "Model": "gpt-4-turbo-preview", + "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": { + "summary": { + "description": "A summary of the dialog", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot2" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Input to chatbot2" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistent Response 2 - from chatbot2" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null + } + }, + "result": "Assistent Response 2 - from chatbot2" + }, + "continuationToolID": "testdata/TestDualSubChat/test.gpt:13" + } + } + ], + "subCallID": "call_1" + } +}` diff --git a/pkg/tests/testdata/TestDualSubChat/step2.golden b/pkg/tests/testdata/TestDualSubChat/step2.golden new file mode 100644 index 00000000..5fe0a132 --- /dev/null +++ b/pkg/tests/testdata/TestDualSubChat/step2.golden @@ -0,0 +1,182 @@ +`{ + "done": false, + "content": "Assistent Response 2 - from chatbot2", + "toolID": "testdata/TestDualSubChat/test.gpt:13", + "state": { + "continuation": { + "state": { + "completion": { + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": null, + "Tools": [ + { + "function": { + "toolID": "testdata/TestDualSubChat/test.gpt:6", + "name": "chatbot", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestDualSubChat/test.gpt:13", + "name": "chatbot2", + "parameters": null + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "Call chatbots" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 1" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "chatbot", + "arguments": "Input to chatbot1" + } + } + }, + { + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "chatbot2", + "arguments": "Input to chatbot2" + } + } + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null + }, + "pending": { + "call_1": { + "index": 0, + "id": "call_1", + "function": { + "name": "chatbot", + "arguments": "Input to chatbot1" + } + }, + "call_2": { + "index": 1, + "id": "call_2", + "function": { + "name": "chatbot2", + "arguments": "Input to chatbot2" + } + } + } + }, + "calls": { + "call_1": { + "toolID": "testdata/TestDualSubChat/test.gpt:6", + "input": "Input to chatbot1" + }, + "call_2": { + "toolID": "testdata/TestDualSubChat/test.gpt:13", + "input": "Input to chatbot2" + } + } + }, + "subCalls": [ + { + "toolId": "testdata/TestDualSubChat/test.gpt:6", + "callId": "call_1", + "state": { + "result": "Chat done" + } + }, + { + "toolId": "testdata/TestDualSubChat/test.gpt:13", + "callId": "call_2", + "state": { + "continuation": { + "state": { + "completion": { + "Model": "gpt-4-turbo-preview", + "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": { + "summary": { + "description": "A summary of the dialog", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot2" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Input to chatbot2" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistent Response 2 - from chatbot2" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null + } + }, + "result": "Assistent Response 2 - from chatbot2" + }, + "continuationToolID": "testdata/TestDualSubChat/test.gpt:13" + } + } + ], + "subCallID": "call_2" + } +}` diff --git a/pkg/tests/testdata/TestDualSubChat/step3.golden b/pkg/tests/testdata/TestDualSubChat/step3.golden new file mode 100644 index 00000000..d7732186 --- /dev/null +++ b/pkg/tests/testdata/TestDualSubChat/step3.golden @@ -0,0 +1,198 @@ +`{ + "done": false, + "content": "Assistant 3", + "toolID": "testdata/TestDualSubChat/test.gpt:13", + "state": { + "continuation": { + "state": { + "completion": { + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": null, + "Tools": [ + { + "function": { + "toolID": "testdata/TestDualSubChat/test.gpt:6", + "name": "chatbot", + "parameters": null + } + }, + { + "function": { + "toolID": "testdata/TestDualSubChat/test.gpt:13", + "name": "chatbot2", + "parameters": null + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "Call chatbots" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 1" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "toolCall": { + "index": 0, + "id": "call_1", + "function": { + "name": "chatbot", + "arguments": "Input to chatbot1" + } + } + }, + { + "toolCall": { + "index": 1, + "id": "call_2", + "function": { + "name": "chatbot2", + "arguments": "Input to chatbot2" + } + } + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null + }, + "pending": { + "call_1": { + "index": 0, + "id": "call_1", + "function": { + "name": "chatbot", + "arguments": "Input to chatbot1" + } + }, + "call_2": { + "index": 1, + "id": "call_2", + "function": { + "name": "chatbot2", + "arguments": "Input to chatbot2" + } + } + } + }, + "calls": { + "call_1": { + "toolID": "testdata/TestDualSubChat/test.gpt:6", + "input": "Input to chatbot1" + }, + "call_2": { + "toolID": "testdata/TestDualSubChat/test.gpt:13", + "input": "Input to chatbot2" + } + } + }, + "subCalls": [ + { + "toolId": "testdata/TestDualSubChat/test.gpt:6", + "callId": "call_1", + "state": { + "result": "Chat done" + } + }, + { + "toolId": "testdata/TestDualSubChat/test.gpt:13", + "callId": "call_2", + "state": { + "continuation": { + "state": { + "completion": { + "Model": "gpt-4-turbo-preview", + "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": { + "summary": { + "description": "A summary of the dialog", + "type": "string" + } + }, + "type": "object" + } + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot2" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Input to chatbot2" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistent Response 2 - from chatbot2" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 3" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistant 3" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null + } + }, + "result": "Assistant 3" + }, + "continuationToolID": "testdata/TestDualSubChat/test.gpt:13" + } + } + ], + "subCallID": "call_2" + } +}` diff --git a/pkg/tests/testdata/TestDualSubChat/step4.golden b/pkg/tests/testdata/TestDualSubChat/step4.golden new file mode 100644 index 00000000..592ce5da --- /dev/null +++ b/pkg/tests/testdata/TestDualSubChat/step4.golden @@ -0,0 +1,6 @@ +`{ + "done": true, + "content": "And we're done", + "toolID": "", + "state": null +}` diff --git a/pkg/tests/testdata/TestDualSubChat/test.gpt b/pkg/tests/testdata/TestDualSubChat/test.gpt new file mode 100644 index 00000000..6357b870 --- /dev/null +++ b/pkg/tests/testdata/TestDualSubChat/test.gpt @@ -0,0 +1,17 @@ +tools: chatbot, chatbot2 + +Call chatbots + +--- +tools: sys.chat.finish +name: chatbot +chat: true + +This is a chatbot + +--- +tools: sys.chat.finish +name: chatbot2 +chat: true + +This is a chatbot2 diff --git a/pkg/tests/testdata/TestExport/call1.golden b/pkg/tests/testdata/TestExport/call1.golden index 541fec05..ca22453a 100644 --- a/pkg/tests/testdata/TestExport/call1.golden +++ b/pkg/tests/testdata/TestExport/call1.golden @@ -23,7 +23,7 @@ { "function": { "toolID": "testdata/TestExport/parent.gpt:11", - "name": "parent_local", + "name": "parentLocal", "parameters": { "properties": { "defaultPromptParameter": { diff --git a/pkg/tests/testdata/TestExport/call3.golden b/pkg/tests/testdata/TestExport/call3.golden index 18acf446..c617acb2 100644 --- a/pkg/tests/testdata/TestExport/call3.golden +++ b/pkg/tests/testdata/TestExport/call3.golden @@ -23,7 +23,7 @@ { "function": { "toolID": "testdata/TestExport/parent.gpt:11", - "name": "parent_local", + "name": "parentLocal", "parameters": { "properties": { "defaultPromptParameter": { diff --git a/pkg/tests/testdata/TestSubChat/call1.golden b/pkg/tests/testdata/TestSubChat/call1.golden new file mode 100644 index 00000000..2cc97485 --- /dev/null +++ b/pkg/tests/testdata/TestSubChat/call1.golden @@ -0,0 +1,36 @@ +`{ + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": null, + "Tools": [ + { + "function": { + "toolID": "testdata/TestSubChat/test.gpt:6", + "name": "chatbot", + "parameters": null + } + } + ], + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "Call chatbot" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "Hello" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null +}` diff --git a/pkg/tests/testdata/TestSubChat/call2.golden b/pkg/tests/testdata/TestSubChat/call2.golden new file mode 100644 index 00000000..90b14ab6 --- /dev/null +++ b/pkg/tests/testdata/TestSubChat/call2.golden @@ -0,0 +1,20 @@ +`{ + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": false, + "Tools": null, + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null +}` diff --git a/pkg/tests/testdata/TestSubChat/call3.golden b/pkg/tests/testdata/TestSubChat/call3.golden new file mode 100644 index 00000000..25a20cc8 --- /dev/null +++ b/pkg/tests/testdata/TestSubChat/call3.golden @@ -0,0 +1,36 @@ +`{ + "Model": "gpt-4-turbo-preview", + "InternalSystemPrompt": false, + "Tools": null, + "Messages": [ + { + "role": "system", + "content": [ + { + "text": "This is a chatbot" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "text": "Assistant 1" + } + ] + }, + { + "role": "user", + "content": [ + { + "text": "User 1" + } + ] + } + ], + "MaxTokens": 0, + "Temperature": null, + "JSONResponse": false, + "Grammar": "", + "Cache": null +}` diff --git a/pkg/tests/testdata/TestSubChat/test.gpt b/pkg/tests/testdata/TestSubChat/test.gpt new file mode 100644 index 00000000..40ff49d8 --- /dev/null +++ b/pkg/tests/testdata/TestSubChat/test.gpt @@ -0,0 +1,9 @@ +tools: chatbot + +Call chatbot + +--- +name: chatbot +chat: true + +This is a chatbot \ No newline at end of file diff --git a/pkg/tests/tester/runner.go b/pkg/tests/tester/runner.go index 45905664..16c4d43e 100644 --- a/pkg/tests/tester/runner.go +++ b/pkg/tests/tester/runner.go @@ -22,9 +22,10 @@ type Client struct { } type Result struct { - Text string - Func types.CompletionFunctionCall - Err error + Text string + Func types.CompletionFunctionCall + Content []types.ContentPart + Err error } func (c *Client) Call(_ context.Context, messageRequest types.CompletionRequest, _ chan<- types.CompletionStatus) (*types.CompletionMessage, error) { @@ -53,6 +54,25 @@ func (c *Client) Call(_ context.Context, messageRequest types.CompletionRequest, return nil, result.Err } + if len(result.Content) > 0 { + msg := &types.CompletionMessage{ + Role: types.CompletionMessageRoleTypeAssistant, + } + for _, contentPart := range result.Content { + if contentPart.ToolCall == nil { + msg.Content = append(msg.Content, contentPart) + } else { + for i, tool := range messageRequest.Tools { + if contentPart.ToolCall.Function.Name == tool.Function.Name { + contentPart.ToolCall.Index = &i + msg.Content = append(msg.Content, contentPart) + } + } + } + } + return msg, nil + } + for i, tool := range messageRequest.Tools { if tool.Function.Name == result.Func.Name { return &types.CompletionMessage{ @@ -96,11 +116,15 @@ func (r *Runner) RunDefault() string { return result } -func (r *Runner) Run(script, input string) (string, error) { +func (r *Runner) Load(script string) (types.Program, error) { if script == "" { script = "test.gpt" } - prg, err := loader.Program(context.Background(), filepath.Join(".", "testdata", r.Client.t.Name(), script), "") + return loader.Program(context.Background(), filepath.Join(".", "testdata", r.Client.t.Name(), script), "") +} + +func (r *Runner) Run(script, input string) (string, error) { + prg, err := r.Load(script) if err != nil { return "", err } @@ -108,6 +132,11 @@ func (r *Runner) Run(script, input string) (string, error) { return r.Runner.Run(context.Background(), prg, os.Environ(), input) } +func (r *Runner) AssertResponded(t *testing.T) { + t.Helper() + require.Len(t, r.Client.result, 0) +} + func (r *Runner) RespondWith(result ...Result) { r.Client.result = append(r.Client.result, result...) } @@ -119,7 +148,9 @@ func NewRunner(t *testing.T) *Runner { t: t, } - run, err := runner.New(c, "default") + run, err := runner.New(c, "default", runner.Options{ + Sequential: true, + }) require.NoError(t, err) return &Runner{ diff --git a/pkg/types/tool.go b/pkg/types/tool.go index 21008aff..f619799f 100644 --- a/pkg/types/tool.go +++ b/pkg/types/tool.go @@ -14,6 +14,7 @@ import ( const ( DaemonPrefix = "#!sys.daemon" OpenAPIPrefix = "#!sys.openapi" + PrintPrefix = "#!sys.print" CommandPrefix = "#!" ) @@ -39,6 +40,20 @@ type Program struct { ToolSet ToolSet `json:"toolSet,omitempty"` } +func (p Program) IsChat() bool { + return p.ToolSet[p.EntryToolID].Chat +} + +func (p Program) ChatName() string { + if p.IsChat() { + name := p.ToolSet[p.EntryToolID].Name + if name != "" { + return name + } + } + return p.Name +} + func (p Program) GetContextToolIDs(toolID string) (result []string, _ error) { seen := map[string]struct{}{} tool := p.ToolSet[toolID] @@ -113,6 +128,7 @@ type Parameters struct { ModelName string `json:"modelName,omitempty"` ModelProvider bool `json:"modelProvider,omitempty"` JSONResponse bool `json:"jsonResponse,omitempty"` + Chat bool `json:"chat,omitempty"` Temperature *float32 `json:"temperature,omitempty"` Cache *bool `json:"cache,omitempty"` InternalPrompt *bool `json:"internalPrompt"` @@ -215,6 +231,9 @@ func (t Tool) String() string { if len(t.Parameters.Credentials) > 0 { _, _ = fmt.Fprintf(buf, "Credentials: %s\n", strings.Join(t.Parameters.Credentials, ", ")) } + if t.Chat { + _, _ = fmt.Fprintf(buf, "Chat: true") + } return buf.String() } @@ -278,7 +297,7 @@ func appendTool(completionTools []CompletionTool, prg Program, parentTool Tool, } args := subTool.Parameters.Arguments - if args == nil && !subTool.IsCommand() { + if args == nil && !subTool.IsCommand() && !subTool.Chat { args = &system.DefaultToolSchema } @@ -346,6 +365,10 @@ func (t Tool) IsOpenAPI() bool { return strings.HasPrefix(t.Instructions, OpenAPIPrefix) } +func (t Tool) IsPrint() bool { + return strings.HasPrefix(t.Instructions, PrintPrefix) +} + func (t Tool) IsHTTP() bool { return strings.HasPrefix(t.Instructions, "#!http://") || strings.HasPrefix(t.Instructions, "#!https://") diff --git a/pkg/types/toolname.go b/pkg/types/toolname.go index ce396cff..390d2913 100644 --- a/pkg/types/toolname.go +++ b/pkg/types/toolname.go @@ -9,7 +9,7 @@ import ( ) var ( - validToolName = regexp.MustCompile("^[a-zA-Z0-9_]{1,64}$") + validToolName = regexp.MustCompile("^[a-zA-Z0-9]{1,64}$") invalidChars = regexp.MustCompile("[^a-zA-Z0-9_]+") ) @@ -25,16 +25,22 @@ func ToolNormalizer(tool string) string { return tool } - name := invalidChars.ReplaceAllString(tool, "_") - for strings.HasSuffix(name, "_") { - name = strings.TrimSuffix(name, "_") + if len(tool) > 55 { + tool = tool[:55] } - if len(name) > 55 { - name = name[:55] + tool = invalidChars.ReplaceAllString(tool, "_") + + var result []string + for i, part := range strings.Split(tool, "_") { + lower := strings.ToLower(part) + if i != 0 && len(lower) > 0 { + lower = strings.ToTitle(lower[0:1]) + lower[1:] + } + result = append(result, lower) } - return name + return strings.Join(result, "") } func PickToolName(toolName string, existing map[string]struct{}) string { diff --git a/pkg/types/toolname_test.go b/pkg/types/toolname_test.go index 84621058..fe1d2524 100644 --- a/pkg/types/toolname_test.go +++ b/pkg/types/toolname_test.go @@ -7,5 +7,7 @@ import ( ) func TestToolNormalizer(t *testing.T) { - autogold.Expect("bob_tool").Equal(t, ToolNormalizer("bob-tool")) + autogold.Expect("bobTool").Equal(t, ToolNormalizer("bob-tool")) + autogold.Expect("bobTool").Equal(t, ToolNormalizer("bob_tool")) + autogold.Expect("bobTool").Equal(t, ToolNormalizer("BOB tOOL")) } diff --git a/scripts/ci.gpt b/scripts/ci.gpt deleted file mode 100644 index f37acd6c..00000000 --- a/scripts/ci.gpt +++ /dev/null @@ -1,11 +0,0 @@ -tools: sys.exec, sys.abort, sys.getenv - -Run each step sequentially, if either step fails abort - -1. If DANGEROUS environment variable does not equal "true" then abort -2. Run "make" to compile -3. Run the standard set of go validation tools: test, vet, and fmt recursively -4. Install golangci-lint and validate the code using it -5. If the git workspace is dirty, then abort - -Then print SUCCESS diff --git a/scripts/gen-docs.gpt b/scripts/gen-docs.gpt deleted file mode 100644 index f82d52cc..00000000 --- a/scripts/gen-docs.gpt +++ /dev/null @@ -1,14 +0,0 @@ -tools: system-tools-state, sys.read, sys.write - -Do the following in order: -1. Get the current state of system tools -2. Update the ./docs/docs/100-tools/03-offerings/02-system.md file to - reflect the latest changes. The file should not change structure and - should add any new content at the end of the file in the same format. - ---- -name: system-tools-state -tools: sys.exec -description: Get the current state of system tools - -#!/usr/bin/env go run main.go --list-tools diff --git a/scripts/upload-s3.gpt b/scripts/upload-s3.gpt deleted file mode 100644 index 77a32f2f..00000000 --- a/scripts/upload-s3.gpt +++ /dev/null @@ -1,12 +0,0 @@ -Tools: sys.exec, upload-s3 - -Find all files starts with `gptscript-state` in $TMPDIR, and upload the file to s3 bucket `gptscript-state`. Only find file from top-level folder. -Use filename to identify the file that is only uploaded within last week. - ---- -name: upload-s3 -description: upload all the file to s3 bucket -args: input: args to provide for aws s3 command - -aws s3 ${input} - diff --git a/scripts/upload-s3.sh b/scripts/upload-s3.sh deleted file mode 100755 index 346391cb..00000000 --- a/scripts/upload-s3.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -# Remove any files that could contain search results from a search API -grep -c "URL:" "${TMPDIR}"/gptscript-state* | grep -v ":0" | cut -d ':' -f1 | xargs rm - -find $TMPDIR -maxdepth 1 -type f -name 'gptscript-state*' -mtime -7 | xargs -I{} aws s3 cp {} s3://gptscript-state/