From f6a65051ff8594c9553b32a8dc4d659d1592d8a8 Mon Sep 17 00:00:00 2001 From: Hanxing Yang Date: Thu, 26 Dec 2024 11:58:06 +0800 Subject: [PATCH] Copilot API (#1173) --- spx-backend/.env.dev | 4 + spx-backend/cmd/spx-backend/gop_autogen.go | 49 ++++- .../cmd/spx-backend/post_copilot_message.yap | 29 +++ spx-backend/go.mod | 29 +-- spx-backend/go.sum | 177 ++++++++++-------- spx-backend/internal/controller/controller.go | 24 ++- spx-backend/internal/controller/copilot.go | 144 ++++++++++++++ .../internal/controller/copilot_test.go | 87 +++++++++ .../internal/copilot/custom_elements.md | 47 +++++ spx-backend/internal/copilot/gop_defs.json | 1 + spx-backend/internal/copilot/prompt.go | 47 +++++ spx-backend/internal/copilot/spx_defs.json | 1 + spx-backend/internal/copilot/system_prompt.md | 156 +++++++++++++++ spx-gui/src/apis/aigc.ts | 2 +- spx-gui/src/apis/copilot.ts | 19 ++ .../editor/code-editor/code-editor.ts | 6 +- .../components/editor/code-editor/context.ts | 2 +- .../components/editor/code-editor/copilot.ts | 166 ++++++++++++++-- .../code-editor/document-base/helpers.ts | 59 ++++++ .../editor/code-editor/document-base/index.ts | 1 + .../editor/code-editor/ui/copilot/index.ts | 9 +- .../code-editor/ui/markdown/MarkdownView.vue | 2 +- spx-gui/src/models/widget/monitor.ts | 2 +- spx-gui/src/models/widget/widget.ts | 13 +- 24 files changed, 944 insertions(+), 132 deletions(-) create mode 100644 spx-backend/cmd/spx-backend/post_copilot_message.yap create mode 100644 spx-backend/internal/controller/copilot.go create mode 100644 spx-backend/internal/controller/copilot_test.go create mode 100644 spx-backend/internal/copilot/custom_elements.md create mode 100644 spx-backend/internal/copilot/gop_defs.json create mode 100644 spx-backend/internal/copilot/prompt.go create mode 100644 spx-backend/internal/copilot/spx_defs.json create mode 100644 spx-backend/internal/copilot/system_prompt.md create mode 100644 spx-gui/src/apis/copilot.ts create mode 100644 spx-gui/src/components/editor/code-editor/document-base/helpers.ts diff --git a/spx-backend/.env.dev b/spx-backend/.env.dev index 388b2b16a..9a5b25bee 100644 --- a/spx-backend/.env.dev +++ b/spx-backend/.env.dev @@ -46,3 +46,7 @@ Iw== -----END CERTIFICATE-----" GOP_CASDOOR_ORGANIZATIONNAME="GoPlus" GOP_CASDOOR_APPLICATIONNAME="application_x8aevk" + +# Anthropic Service +ANTHROPIC_ENDPOINT= +ANTHROPIC_API_KEY= diff --git a/spx-backend/cmd/spx-backend/gop_autogen.go b/spx-backend/cmd/spx-backend/gop_autogen.go index da827c12f..26e1e4831 100644 --- a/spx-backend/cmd/spx-backend/gop_autogen.go +++ b/spx-backend/cmd/spx-backend/gop_autogen.go @@ -95,6 +95,10 @@ type post_asset struct { yap.Handler *AppV2 } +type post_copilot_message struct { + yap.Handler + *AppV2 +} type post_project_release struct { yap.Handler *AppV2 @@ -192,7 +196,7 @@ func (this *AppV2) MainEntry() { } } func (this *AppV2) Main() { - yap.Gopt_AppV2_Main(this, new(delete_asset_id), new(delete_project_owner_name), new(delete_project_owner_name_liking), new(delete_user_username_following), new(get_asset_id), new(get_assets_list), new(get_project_release_owner_project_release), new(get_project_releases_list), new(get_project_owner_name), new(get_project_owner_name_liking), new(get_projects_list), new(get_user_username), new(get_user_username_following), new(get_users_list), new(get_util_upinfo), new(post_aigc_matting), new(post_asset), new(post_project_release), new(post_project), new(post_project_owner_name_liking), new(post_project_owner_name_view), new(post_user_username_following), new(post_util_fileurls), new(post_util_fmtcode), new(put_asset_id), new(put_project_owner_name), new(put_user)) + yap.Gopt_AppV2_Main(this, new(delete_asset_id), new(delete_project_owner_name), new(delete_project_owner_name_liking), new(delete_user_username_following), new(get_asset_id), new(get_assets_list), new(get_project_release_owner_project_release), new(get_project_releases_list), new(get_project_owner_name), new(get_project_owner_name_liking), new(get_projects_list), new(get_user_username), new(get_user_username_following), new(get_users_list), new(get_util_upinfo), new(post_aigc_matting), new(post_asset), new(post_copilot_message), new(post_project_release), new(post_project), new(post_project_owner_name_liking), new(post_project_owner_name_view), new(post_user_username_following), new(post_util_fileurls), new(post_util_fmtcode), new(put_asset_id), new(put_project_owner_name), new(put_user)) } //line cmd/spx-backend/delete_asset_#id.yap:6 func (this *delete_asset_id) Main(_gop_arg0 *yap.Context) { @@ -953,6 +957,49 @@ func (this *post_asset) Main(_gop_arg0 *yap.Context) { func (this *post_asset) Classfname() string { return "post_asset" } +//line cmd/spx-backend/post_copilot_message.yap:10 +func (this *post_copilot_message) Main(_gop_arg0 *yap.Context) { + this.Handler.Main(_gop_arg0) +//line cmd/spx-backend/post_copilot_message.yap:10:1 + ctx := &this.Context +//line cmd/spx-backend/post_copilot_message.yap:11:1 + if +//line cmd/spx-backend/post_copilot_message.yap:11:1 + _, isAuthed := ensureAuthedUser(ctx); !isAuthed { +//line cmd/spx-backend/post_copilot_message.yap:12:1 + return + } +//line cmd/spx-backend/post_copilot_message.yap:15:1 + params := &controller.GenerateMessageParams{} +//line cmd/spx-backend/post_copilot_message.yap:16:1 + if !parseJSON(ctx, params) { +//line cmd/spx-backend/post_copilot_message.yap:17:1 + return + } +//line cmd/spx-backend/post_copilot_message.yap:19:1 + if +//line cmd/spx-backend/post_copilot_message.yap:19:1 + ok, msg := params.Validate(); !ok { +//line cmd/spx-backend/post_copilot_message.yap:20:1 + replyWithCodeMsg(ctx, errorInvalidArgs, msg) +//line cmd/spx-backend/post_copilot_message.yap:21:1 + return + } +//line cmd/spx-backend/post_copilot_message.yap:24:1 + result, err := this.ctrl.GenerateMessage(ctx.Context(), params) +//line cmd/spx-backend/post_copilot_message.yap:25:1 + if err != nil { +//line cmd/spx-backend/post_copilot_message.yap:26:1 + replyWithInnerError(ctx, err) +//line cmd/spx-backend/post_copilot_message.yap:27:1 + return + } +//line cmd/spx-backend/post_copilot_message.yap:29:1 + this.Json__1(result) +} +func (this *post_copilot_message) Classfname() string { + return "post_copilot_message" +} //line cmd/spx-backend/post_project-release.yap:10 func (this *post_project_release) Main(_gop_arg0 *yap.Context) { this.Handler.Main(_gop_arg0) diff --git a/spx-backend/cmd/spx-backend/post_copilot_message.yap b/spx-backend/cmd/spx-backend/post_copilot_message.yap new file mode 100644 index 000000000..91c28f251 --- /dev/null +++ b/spx-backend/cmd/spx-backend/post_copilot_message.yap @@ -0,0 +1,29 @@ +// Generate a message by sending a list of input messages. +// +// Request: +// POST /copilot/message + +import ( + "github.com/goplus/builder/spx-backend/internal/controller" +) + +ctx := &Context +if _, isAuthed := ensureAuthedUser(ctx); !isAuthed { + return +} + +params := &controller.GenerateMessageParams{} +if !parseJSON(ctx, params) { + return +} +if ok, msg := params.Validate(); !ok { + replyWithCodeMsg(ctx, errorInvalidArgs, msg) + return +} + +result, err := ctrl.GenerateMessage(ctx.Context(), params) +if err != nil { + replyWithInnerError(ctx, err) + return +} +json result diff --git a/spx-backend/go.mod b/spx-backend/go.mod index 3764e2ded..140abd668 100644 --- a/spx-backend/go.mod +++ b/spx-backend/go.mod @@ -10,11 +10,12 @@ require ( github.com/qiniu/x v1.13.10 gocloud.dev v0.36.0 // indirect golang.org/x/mod v0.17.0 - golang.org/x/tools v0.19.0 + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d ) require ( github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.8 github.com/casdoor/casdoor-go-sdk v0.36.0 github.com/goplus/gop v1.2.6 github.com/qiniu/go-sdk/v7 v7.18.0 @@ -28,23 +29,25 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/googleapis/gax-go/v2 v2.12.5 // indirect github.com/goplus/gogen v1.15.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/net v0.22.0 // indirect - golang.org/x/oauth2 v0.14.0 // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect - google.golang.org/api v0.151.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect - google.golang.org/grpc v1.59.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/api v0.189.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect + google.golang.org/grpc v1.64.1 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/spx-backend/go.sum b/spx-backend/go.sum index ad1ab0fb8..4607ce589 100644 --- a/spx-backend/go.sum +++ b/spx-backend/go.sum @@ -1,10 +1,13 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= -cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= +cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= +cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE= +cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs= +cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI= +cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= +cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w= @@ -14,46 +17,48 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.8 h1:ss/c/eeyILgoK2sMsTJdcdLdhY3wZSt//+nanM41B9w= +github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.8/go.mod h1:GJxtdOs9K4neo8Gg65CjJ7jNautmldGli5/OFNabOoo= github.com/aws/aws-sdk-go v1.49.0 h1:g9BkW1fo9GqKfwg2+zCD+TW/D36Ux+vtfJ8guF4AYmY= github.com/aws/aws-sdk-go v1.49.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= -github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= -github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= -github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o= -github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg= -github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU= -github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= +github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= +github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM= +github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= +github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= +github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7 h1:FnLf60PtjXp8ZOzQfhJVsqF0OtYKQZWQfqOLshh8YXg= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7/go.mod h1:tDVvl8hyU6E9B8TrnNrZQEVkQlB8hjJwcgpPhgtlnNg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM= github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 h1:Keso8lIOS+IzI2MkPZyK6G0LYcK3My2LQ+T5bxghEAY= github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= -github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= -github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= +github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= +github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/casdoor/casdoor-go-sdk v0.36.0 h1:0kK98ptEhqSb2/QR3EO5DvOHTa/Rr9y1Lc7D/jOSFmE= github.com/casdoor/casdoor-go-sdk v0.36.0/go.mod h1:hVSgmSdwTCsBEJNt9r2K5aLVsoeMc37/N4Zzescy5SA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -67,8 +72,12 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= @@ -96,17 +105,14 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= @@ -114,14 +120,14 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= -github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= +github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= github.com/goplus/gogen v1.15.2 h1:Q6XaSx/Zi5tWnjfAziYsQI6Jv6MgODRpFtOYqNkiiqM= github.com/goplus/gogen v1.15.2/go.mod h1:92qEzVgv7y8JEFICWG9GvYI5IzfEkxYdsA1DbmnTkqk= github.com/goplus/gop v1.2.6 h1:kog3c5Js+8EopqmI4+CwueXsqibnBwYVt5q5N7juRVY= @@ -173,9 +179,29 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= gocloud.dev v0.36.0 h1:q5zoXux4xkOZP473e1EZbG8Gq9f0vlg1VNH5Du/ybus= gocloud.dev v0.36.0/go.mod h1:bLxah6JQVKBaIxzsr5BQLYB4IYdWHkMZdzCXlo6F0gg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -183,8 +209,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -202,17 +228,17 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY 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.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= -golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/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.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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -223,8 +249,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -233,12 +259,11 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -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.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= -golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -246,34 +271,32 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= -golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -google.golang.org/api v0.151.0 h1:FhfXLO/NFdJIzQtCqjpysWwqKk8AzGWBUhMIx67cVDU= -google.golang.org/api v0.151.0/go.mod h1:ccy+MJ6nrYFgE3WgRx/AMXOxOmU8Q4hSa+jjibzhxcg= +google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI= +google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f h1:Vn+VyHU5guc9KjB5KrjI2q0wCOWEOIh0OEsleqakHJg= -google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY= -google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY= -google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= +google.golang.org/genproto v0.0.0-20240722135656-d784300faade h1:lKFsS7wpngDgSCeFn7MoLy+wBDQZ1UQIJD4UNM1Qvkg= +google.golang.org/genproto v0.0.0-20240722135656-d784300faade/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY= +google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3 h1:QW9+G6Fir4VcRXVH8x3LilNAb6cxBGLa6+GM4hRwexE= +google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3/go.mod h1:kdrSS/OiLkPrNUpzD4aHgCq2rVuC/YRxok32HXZ4vRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -283,10 +306,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/spx-backend/internal/controller/controller.go b/spx-backend/internal/controller/controller.go index b892302df..ef2bd724b 100644 --- a/spx-backend/internal/controller/controller.go +++ b/spx-backend/internal/controller/controller.go @@ -9,6 +9,8 @@ import ( "strconv" "time" + "github.com/anthropics/anthropic-sdk-go" + anthropicOption "github.com/anthropics/anthropic-sdk-go/option" "github.com/casdoor/casdoor-go-sdk/casdoorsdk" _ "github.com/go-sql-driver/mysql" "github.com/goplus/builder/spx-backend/internal/aigc" @@ -36,10 +38,11 @@ type contextKey struct { // Controller is the controller for the service. type Controller struct { - db *gorm.DB - kodo *kodoConfig - aigcClient *aigc.AigcClient - casdoorClient casdoorClient + db *gorm.DB + kodo *kodoConfig + aigcClient *aigc.AigcClient + casdoorClient casdoorClient + anthropicClient *anthropic.Client } // New creates a new controller. @@ -62,12 +65,17 @@ func New(ctx context.Context) (*Controller, error) { kodoConfig := newKodoConfig(logger) aigcClient := aigc.NewAigcClient(mustEnv(logger, "AIGC_ENDPOINT")) casdoorClient := newCasdoorClient(logger) + anthropicClient := anthropic.NewClient( + anthropicOption.WithAPIKey(mustEnv(logger, "ANTHROPIC_API_KEY")), + anthropicOption.WithBaseURL(mustEnv(logger, "ANTHROPIC_ENDPOINT")), + ) return &Controller{ - db: db, - kodo: kodoConfig, - aigcClient: aigcClient, - casdoorClient: casdoorClient, + db: db, + kodo: kodoConfig, + aigcClient: aigcClient, + casdoorClient: casdoorClient, + anthropicClient: anthropicClient, }, nil } diff --git a/spx-backend/internal/controller/copilot.go b/spx-backend/internal/controller/copilot.go new file mode 100644 index 000000000..319f88196 --- /dev/null +++ b/spx-backend/internal/controller/copilot.go @@ -0,0 +1,144 @@ +package controller + +import ( + "context" + "fmt" + + "github.com/anthropics/anthropic-sdk-go" + "github.com/goplus/builder/spx-backend/internal/copilot" + "github.com/goplus/builder/spx-backend/internal/log" +) + +const ( + MAX_CONTENT_TEXT_LENGTH = 5000 + MAX_MESSAGE_COUNT = 50 + MAX_TOKENS = 1024 +) + +type Role string + +const ( + RoleUser Role = "user" + RoleCopilot Role = "copilot" +) + +func (r Role) Validate() (ok bool, msg string) { + switch r { + case RoleUser, RoleCopilot: + return true, "" + default: + return false, "invalid role" + } +} + +type ContentType string + +const ( + ContentTypeText ContentType = "text" +) + +func (c ContentType) Validate() (ok bool, msg string) { + switch c { + case ContentTypeText: + return true, "" + default: + return false, "invalid content type" + } +} + +type Content struct { + Type ContentType `json:"type"` + Text string `json:"text"` +} + +func (c *Content) Validate() (ok bool, msg string) { + if ok, msg := c.Type.Validate(); !ok { + return false, msg + } + if c.Text == "" { + return false, "missing text" + } + if len(c.Text) > MAX_CONTENT_TEXT_LENGTH { + return false, "text too long" + } + return true, "" +} + +type Message struct { + Role Role `json:"role"` + Content Content `json:"content"` +} + +func (m *Message) Validate() (ok bool, msg string) { + if ok, msg := m.Role.Validate(); !ok { + return false, fmt.Sprintf("invalid role: %s", msg) + } + if ok, msg := m.Content.Validate(); !ok { + return false, fmt.Sprintf("invalid content: %s", msg) + } + return true, "" +} + +type GenerateMessageParams struct { + Messages []Message `json:"messages"` +} + +func (p *GenerateMessageParams) Validate() (ok bool, msg string) { + if len(p.Messages) > MAX_MESSAGE_COUNT { + return false, "too many messages" + } + for i, m := range p.Messages { + if ok, msg := m.Validate(); !ok { + return false, fmt.Sprintf("invalid message at index %d: %s", i, msg) + } + } + return true, "" +} + +type GenerateMessageResult Message + +// GenerateMessage generates response message based on input messages. +func (ctrl *Controller) GenerateMessage(ctx context.Context, params *GenerateMessageParams) (*GenerateMessageResult, error) { + logger := log.GetReqLogger(ctx) + messages := []anthropic.MessageParam{} + for _, m := range params.Messages { + var message anthropic.MessageParam + if m.Role == RoleUser { + message = anthropic.NewUserMessage(anthropic.NewTextBlock(m.Content.Text)) + } else if m.Role == RoleCopilot { + message = anthropic.NewAssistantMessage(anthropic.NewTextBlock(m.Content.Text)) + } + messages = append(messages, message) + } + generatedMsg, err := ctrl.anthropicClient.Messages.New(ctx, anthropic.MessageNewParams{ + Model: anthropic.F(anthropic.ModelClaude3_5Haiku20241022), + MaxTokens: anthropic.F(int64(MAX_TOKENS)), + // Now we are using APIs from api.gptsapi.net for testing. `[]anthropic.TextBlockParam` is not supported. + // So we use `anthropic.Raw` to pass the string-type system prompt. TODO: + // * Switch to official API + // * Use `[]anthropic.TextBlockParam` instead of `string` + // * Enable prompt caching if it helps + System: anthropic.Raw[[]anthropic.TextBlockParam](copilot.SystemPrompt), + Messages: anthropic.F(messages), + }) + if err != nil { + logger.Printf("failed to generate message: %v", err) + return nil, err + } + generatedContent := generatedMsg.Content + if len(generatedContent) == 0 { + logger.Printf("empty content from anthropic") + return nil, fmt.Errorf("empty content") + } + if len(generatedContent) > 1 { + logger.Printf("too much content from anthropic: %d", len(generatedContent)) + return nil, fmt.Errorf("too much content") + } + return &GenerateMessageResult{ + Role: RoleCopilot, + Content: Content{ + Type: ContentTypeText, + Text: generatedContent[0].Text, + }, + }, nil +} diff --git a/spx-backend/internal/controller/copilot_test.go b/spx-backend/internal/controller/copilot_test.go new file mode 100644 index 000000000..9d4c95108 --- /dev/null +++ b/spx-backend/internal/controller/copilot_test.go @@ -0,0 +1,87 @@ +package controller + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGenerateMessageParamsValidate(t *testing.T) { + t.Run("Normal", func(t *testing.T) { + params := &GenerateMessageParams{} + ok, msg := params.Validate() + assert.True(t, ok) + assert.Empty(t, msg) + }) + + t.Run("Too many messages", func(t *testing.T) { + params := &GenerateMessageParams{ + Messages: make([]Message, MAX_MESSAGE_COUNT+1), + } + ok, msg := params.Validate() + assert.False(t, ok) + assert.Equal(t, "too many messages", msg) + }) + + t.Run("Invalid message role", func(t *testing.T) { + params := &GenerateMessageParams{ + Messages: []Message{ + { + Role: Role("invalid"), + }, + }, + } + ok, msg := params.Validate() + assert.False(t, ok) + assert.Equal(t, "invalid message at index 0: invalid role: invalid role", msg) + }) + + t.Run("Invalid message content type", func(t *testing.T) { + params := &GenerateMessageParams{ + Messages: []Message{ + { + Role: RoleUser, + Content: Content{ + Type: ContentType("invalid"), + }, + }, + }, + } + ok, msg := params.Validate() + assert.False(t, ok) + assert.Equal(t, "invalid message at index 0: invalid content: invalid content type", msg) + }) + + t.Run("Invalid message empty content text", func(t *testing.T) { + params := &GenerateMessageParams{ + Messages: []Message{ + { + Role: RoleUser, + Content: Content{ + Type: ContentTypeText, + }, + }, + }, + } + ok, msg := params.Validate() + assert.False(t, ok) + assert.Equal(t, "invalid message at index 0: invalid content: missing text", msg) + }) + + t.Run("Invalid message too long content text", func(t *testing.T) { + params := &GenerateMessageParams{ + Messages: []Message{ + { + Role: RoleUser, + Content: Content{ + Type: ContentTypeText, + Text: "a" + string(make([]byte, MAX_CONTENT_TEXT_LENGTH)), + }, + }, + }, + } + ok, msg := params.Validate() + assert.False(t, ok) + assert.Equal(t, "invalid message at index 0: invalid content: text too long", msg) + }) +} diff --git a/spx-backend/internal/copilot/custom_elements.md b/spx-backend/internal/copilot/custom_elements.md new file mode 100644 index 000000000..36c8172ea --- /dev/null +++ b/spx-backend/internal/copilot/custom_elements.md @@ -0,0 +1,47 @@ +# `code-link` + +Display a link to a code location in the project. By clicking on the link, the user will be navigated to the code location. A location can be a position or a range. + +## Attributes + +### `file` + +Text document URI, e.g., `file:///NiuXiaoQi.spx` + +### `position` + +`${line},${column}`, e.g., `10,20`. + +`line` & `column` are numbers start from 1. `1,1` means the first column of the first line. + +### `range` + +`${startLine},${startColumn}-${endLine}${endColumn}`, e.g., `10,20-12,10` + +`startLine`, `startColumn`, `endLine`, `endColumn` are numbers start from 1. `10,20-12,10` means the range from line 10, column 20 to line 12, column 10. The end position is exclusive. + +## Examples + +### Basic example + +```xml + +``` + +This is a link to line 10, column 20 in the code of sprite `NiuXiaoQi`. + +### Example with text + +```xml +Details +``` + +This is a link to the beginning of the code of stage with the text "Details". + +### Example with range + +```xml +onStart +``` + +This is a link to a range (line 2, column 1 to line 2, column 10) in the code of stage with the text "onStart". diff --git a/spx-backend/internal/copilot/gop_defs.json b/spx-backend/internal/copilot/gop_defs.json new file mode 100644 index 000000000..fa0a383ce --- /dev/null +++ b/spx-backend/internal/copilot/gop_defs.json @@ -0,0 +1 @@ +[{"name":"for_iterate","sample":"for i, v <- set { ... }","desc":"Iterate within given set"},{"name":"for_loop_with_condition","sample":"for condition { ... }","desc":"Loop with condition"},{"name":"for_loop_with_range","sample":"for i <- start:end { ... }","desc":"Loop with range"},{"name":"func_declaration","sample":"func name(params) { ... }","desc":"Function declaration, e.g., `func add(a int, b int) int {}`"},{"name":"if_else_statement","sample":"if condition { ... } else { ... }","desc":"If else statement"},{"name":"if_statement","sample":"if condition { ... }","desc":"If statement"},{"name":"println","sample":"println msg, ...","desc":"Print line, e.g., `println \"Hello, world!\"`"},{"name":"var_declaration","sample":"var name type","desc":"Variable declaration, e.g., `var count int`"}] diff --git a/spx-backend/internal/copilot/prompt.go b/spx-backend/internal/copilot/prompt.go new file mode 100644 index 000000000..a1461053a --- /dev/null +++ b/spx-backend/internal/copilot/prompt.go @@ -0,0 +1,47 @@ +package copilot + +import ( + _ "embed" + "strings" + "text/template" +) + +// For details about maintaining `*_defs.json` files, see: +// spx-gui/src/components/editor/code-editor/document-base/helpers.ts + +//go:embed gop_defs.json +var gopDefs string + +//go:embed spx_defs.json +var spxDefs string + +//go:embed custom_elements.md +var customElements string + +//go:embed system_prompt.md +var systemPromptTpl string + +type systemPromptTplData struct { + GopDefs string + SpxDefs string + CustomElements string +} + +var SystemPrompt string + +func init() { + tplData := systemPromptTplData{ + GopDefs: gopDefs, + SpxDefs: spxDefs, + CustomElements: customElements, + } + tpl, err := template.New("system-prompt").Parse(systemPromptTpl) + if err != nil { + panic(err) + } + var sb strings.Builder + if err := tpl.Execute(&sb, tplData); err != nil { + panic(err) + } + SystemPrompt = sb.String() +} diff --git a/spx-backend/internal/copilot/spx_defs.json b/spx-backend/internal/copilot/spx_defs.json new file mode 100644 index 000000000..fd5e31104 --- /dev/null +++ b/spx-backend/internal/copilot/spx_defs.json @@ -0,0 +1 @@ +{"game":[{"pkg":"spx","name":"backdropIndex","sample":"backdropIndex","desc":"Get the index of the current backdrop"},{"pkg":"spx","name":"backdropName","sample":"backdropName","desc":"Get the name of the current backdrop"},{"pkg":"spx","name":"broadcast","sample":"broadcast message","desc":"Broadcast a message, e.g., `broadcast \"message\"`"},{"pkg":"spx","name":"broadcast","sample":"broadcast message, wait","desc":"Broadcast a message with waiting, e.g., `broadcast \"message\", true`"},{"pkg":"spx","name":"broadcast","sample":"broadcast message, data, wait","desc":"Broadcast a message with data and waiting, e.g., `broadcast \"message\", data, true`"},{"pkg":"spx","name":"changeVolume","sample":"changeVolume dVolume","desc":"Change the volume for sounds with given volume change, e.g., `changeVolume 10`"},{"pkg":"spx","name":"exit","sample":"exit","desc":"Exit the game"},{"pkg":"spx","name":"onClick","sample":"onClick => { ... }","desc":"Listen to stage clicked"},{"pkg":"spx","name":"onKey","sample":"onKey key, => { ... }","desc":"Listen to given key pressed"},{"pkg":"spx","name":"onKey","sample":"onKey keys, key => { ... }","desc":"Listen to given keys pressed (and get the key)"},{"pkg":"spx","name":"onKey","sample":"onKey keys, => { ... }","desc":"Listen to given keys pressed"},{"pkg":"spx","name":"onMsg","sample":"onMsg (message, data) => { ... }","desc":"Listen to any message broadcasted, get the broadcasted message and data"},{"pkg":"spx","name":"onMsg","sample":"onMsg message, => { ... }","desc":"Listen to specific message broadcasted"},{"pkg":"spx","name":"onStart","sample":"onStart => { ... }","desc":"Listen to game start"},{"pkg":"spx","name":"getWidget","sample":"getWidget(type, name)","desc":"Get the widget by given name"},{"pkg":"spx","name":"keyPressed","sample":"keyPressed(key)","desc":"Check if given key is currently pressed, e.g., `keyPressed(KeyA)`"},{"pkg":"spx","name":"mouseHitItem","sample":"mouseHitItem","desc":"Get the topmost sprite which is hit by mouse, e.g., `hitSprite, ok := mouseHitItem`"},{"pkg":"spx","name":"mousePressed","sample":"mousePressed","desc":"Check if the mouse is currently pressed"},{"pkg":"spx","name":"mouseX","sample":"mouseX","desc":"Get X position of the mouse"},{"pkg":"spx","name":"mouseY","sample":"mouseY","desc":"Get Y position of the mouse"},{"pkg":"spx","name":"nextBackdrop","sample":"nextBackdrop","desc":"Switch to the next backdrop"},{"pkg":"spx","name":"onAnyKey","sample":"onAnyKey key => { ... }","desc":"Listen to any key pressed"},{"pkg":"spx","name":"onBackdrop","sample":"onBackdrop backdrop => { ... }","desc":"Listen to backdrop switching"},{"pkg":"spx","name":"onBackdrop","sample":"onBackdrop backdrop, => { ... }","desc":"Listen to switching to specific backdrop"},{"pkg":"spx","name":"play","sample":"play sound","desc":"Play given sound, e.g., `play explosion`"},{"pkg":"spx","name":"play","sample":"play sound, wait","desc":"Play given sound with waiting, e.g., `play explosion, true`"},{"pkg":"spx","name":"play","sample":"play sound, options","desc":"Play given sound with options, e.g., `play explosion, { Loop: true }`"},{"pkg":"spx","name":"play","sample":"play soundName","desc":"Play sound with given name, e.g., `play \"explosion\"`"},{"pkg":"spx","name":"play","sample":"play soundName, wait","desc":"Play sound with given name and waiting, e.g., `play \"explosion\", true`"},{"pkg":"spx","name":"play","sample":"play soundName, options","desc":"Play sound with given name and options, e.g., `play \"explosion\", { Loop: true }`"},{"pkg":"spx","name":"prevBackdrop","sample":"prevBackdrop","desc":"Switch to the previous backdrop"},{"pkg":"spx","name":"rand","sample":"rand(from, to)","desc":"Generate a random number, e.g., `rand(1, 10)`"},{"pkg":"spx","name":"setVolume","sample":"setVolume volume","desc":"Set the volume for sounds, e.g., `setVolume 100`"},{"pkg":"spx","name":"startBackdrop","sample":"startBackdrop backdropName","desc":"Set the current backdrop by specifying name, e.g., `startBackdrop \"backdrop1\"`"},{"pkg":"spx","name":"startBackdrop","sample":"startBackdrop backdropName, wait","desc":"Set the current backdrop by specifying name, with waiting, e.g., `startBackdrop \"backdrop1\", true`"},{"pkg":"spx","name":"stopAllSounds","sample":"stopAllSounds","desc":"Stop all playing sounds"},{"pkg":"spx","name":"volume","sample":"volume","desc":"Get the volume for sounds"},{"pkg":"spx","name":"wait","sample":"wait seconds","desc":"Block current execution (coroutine) for given seconds, e.g., `wait 0.5`"}],"sprite":[{"pkg":"spx","name":"animate","sample":"animate name","desc":"Play animation with given name, e.g., `animate \"jump\"`"},{"pkg":"spx","name":"bounceOffEdge","sample":"bounceOffEdge","desc":"Check & bounce off current sprite if touching the edge"},{"pkg":"spx","name":"changeHeading","sample":"changeHeading dDirection","desc":"Change heading with given direction change, e.g., `changeHeading 90`"},{"pkg":"spx","name":"changeSize","sample":"changeSize dSize","desc":"Change the size of current sprite, e.g., `changeSize 1`"},{"pkg":"spx","name":"changeXYpos","sample":"changeXYpos dX, dY","desc":"Move with given position (X, Y) change, e.g., `changeXYpos 10, 10`"},{"pkg":"spx","name":"changeXpos","sample":"changeXpos dX","desc":"Move with given X position change, e.g., `changeXpos 10`"},{"pkg":"spx","name":"changeYpos","sample":"changeYpos dY","desc":"Move with given Y position change, e.g., `changeYpos 10`"},{"pkg":"spx","name":"clone","sample":"clone","desc":"Make a clone of current sprite, with optional data (for `onCloned` callback)"},{"pkg":"spx","name":"costumeName","sample":"costumeName","desc":"The name of the current costume"},{"pkg":"spx","name":"die","sample":"die","desc":"Let current sprite die. Animation bound to state \"die\" will be played."},{"pkg":"spx","name":"distanceTo","sample":"distanceTo(sprite)","desc":"Get the distance from current sprite to given sprite"},{"pkg":"spx","name":"distanceTo","sample":"distanceTo(spriteName)","desc":"Get the distance from current sprite to the sprite with given name, e.g., `distanceTo(\"Enemy\")`"},{"pkg":"spx","name":"distanceTo","sample":"distanceTo(obj)","desc":"Get the distance from current sprite to given object, e.g., `distanceTo(Mouse)`"},{"pkg":"spx","name":"glide","sample":"glide x, y, seconds","desc":"Move to given position (X, Y), using a glide animation"},{"pkg":"spx","name":"glide","sample":"glide sprite, seconds","desc":"Move to given sprite, using a glide animation"},{"pkg":"spx","name":"glide","sample":"glide spriteName, seconds","desc":"Move to the sprite with given name, using a glide animation"},{"pkg":"spx","name":"glide","sample":"glide obj, seconds","desc":"Move to given obj, using a glide animation, e.g., `glide Mouse, 2`"},{"pkg":"spx","name":"goto","sample":"goto sprite","desc":"Move to given sprite"},{"pkg":"spx","name":"goto","sample":"goto spriteName","desc":"Move to the sprite with given name, e.g., `goto \"Enemy\"`"},{"pkg":"spx","name":"goto","sample":"goto obj","desc":"Move to given obj, e.g., `goto Mouse`"},{"pkg":"spx","name":"heading","sample":"heading","desc":"Get current heading direction"},{"pkg":"spx","name":"hide","sample":"hide","desc":"Make current sprite invisible"},{"pkg":"spx","name":"move","sample":"move distance","desc":"Move given distance, toward current heading"},{"pkg":"spx","name":"onCloned","sample":"onCloned => { ... }","desc":"Listen to current sprite cloned"},{"pkg":"spx","name":"onMoving","sample":"onMoving => { ... }","desc":"Listen to current sprite moving (position change)"},{"pkg":"spx","name":"onTouchStart","sample":"onTouchStart => { ... }","desc":"Listen to current sprite starting to be touched by any other sprites"},{"pkg":"spx","name":"onTouchStart","sample":"onTouchStart sprite => { ... }","desc":"Listen to current sprite starting to be touched by any other sprites (and get the sprite)"},{"pkg":"spx","name":"onTouchStart","sample":"onTouchStart spriteName, => { ... }","desc":"Listen to current sprite starting to be touched by sprite of given name"},{"pkg":"spx","name":"onTouchStart","sample":"onTouchStart spriteName, sprite => { ... }","desc":"Listen to current sprite starting to be touched by sprite of given name (and get the sprite)"},{"pkg":"spx","name":"onTurning","sample":"onTurning => { ... }","desc":"Listen to current sprite turning (heading change)"},{"pkg":"spx","name":"say","sample":"say word","desc":"Make the sprite say some word, e.g., `say \"Hello!\"`"},{"pkg":"spx","name":"say","sample":"say word, seconds","desc":"Make the sprite say some word with duration, e.g., `say \"Hello!\", 2`"},{"pkg":"spx","name":"setCostume","sample":"setCostume name","desc":"Set the current costume by specifying name, e.g., `setCostume \"happy\"`"},{"pkg":"spx","name":"setHeading","sample":"setHeading direction","desc":"Set heading to given value, e.g., `setHeading Up`"},{"pkg":"spx","name":"setRotationStyle","sample":"setRotationStyle style","desc":"Set the rotation style of the sprite, e.g., `setRotationStyle LeftRight`"},{"pkg":"spx","name":"setSize","sample":"setSize size","desc":"Set the size of current sprite, e.g., `setSize 2`"},{"pkg":"spx","name":"setXYpos","sample":"setXYpos x, y","desc":"Move to given position, e.g., `setXYpos 100, 100`"},{"pkg":"spx","name":"setXpos","sample":"setXpos x","desc":"Move to given X position, e.g., `setXpos 100`"},{"pkg":"spx","name":"setYpos","sample":"setYpos y","desc":"Move to given Y position, e.g., `setYpos 100`"},{"pkg":"spx","name":"show","sample":"show","desc":"Make current sprite visible"},{"pkg":"spx","name":"size","sample":"size","desc":"Get the size of current sprite"},{"pkg":"spx","name":"onClick","sample":"onClick => { ... }","desc":"Listen to current sprite clicked"},{"pkg":"spx","name":"onKey","sample":"onKey key, => { ... }","desc":"Listen to given key pressed"},{"pkg":"spx","name":"onKey","sample":"onKey keys, key => { ... }","desc":"Listen to given keys pressed (and get the key)"},{"pkg":"spx","name":"onKey","sample":"onKey keys, => { ... }","desc":"Listen to given keys pressed"},{"pkg":"spx","name":"onMsg","sample":"onMsg (message, data) => { ... }","desc":"Listen to any message broadcasted, get the broadcasted message and data"},{"pkg":"spx","name":"onMsg","sample":"onMsg message, => { ... }","desc":"Listen to specific message broadcasted"},{"pkg":"spx","name":"onStart","sample":"onStart => { ... }","desc":"Listen to game start"},{"pkg":"spx","name":"step","sample":"step distance","desc":"Step given distance, toward current heading. Animation bound to state \"step\" will be played"},{"pkg":"spx","name":"think","sample":"think word","desc":"Make the sprite think of some word, e.g., `think \"Wow!\"`"},{"pkg":"spx","name":"think","sample":"think word, seconds","desc":"Make the sprite think of some word with duration, e.g., `think \"Wow!\", 2`"},{"pkg":"spx","name":"touching","sample":"touching(spriteName)","desc":"Check if current sprite touching sprite with given name"},{"pkg":"spx","name":"touching","sample":"touching(sprite)","desc":"Check if current sprite touching given sprite"},{"pkg":"spx","name":"touching","sample":"touching(obj)","desc":"Check if current sprite touching given object, e.g., `touching(Mouse)`"},{"pkg":"spx","name":"turn","sample":"turn degree","desc":"Turn with given degree, e.g., `turn 90`"},{"pkg":"spx","name":"turn","sample":"turn direction","desc":"Turn with given direction, e.g., `turn Left`"},{"pkg":"spx","name":"turnTo","sample":"turnTo sprite","desc":"Turn heading to given sprite"},{"pkg":"spx","name":"turnTo","sample":"turnTo spriteName","desc":"Turn heading to given sprite by name, e.g., `turnTo \"Enemy\"`"},{"pkg":"spx","name":"turnTo","sample":"turnTo degree","desc":"Turn heading to given degree, e.g., `turnTo 90`"},{"pkg":"spx","name":"turnTo","sample":"turnTo direction","desc":"Turn heading to given direction, e.g., `turnTo Left`"},{"pkg":"spx","name":"turnTo","sample":"turnTo obj","desc":"Turn heading to given object, e.g., `turnTo Mouse`"},{"pkg":"spx","name":"visible","sample":"visible","desc":"If current sprite visible"},{"pkg":"spx","name":"xpos","sample":"xpos","desc":"Get current X position"},{"pkg":"spx","name":"ypos","sample":"ypos","desc":"Get current Y position"}],"others":[{"pkg":"spx","name":"Down","sample":"Down","desc":"Down direction"},{"pkg":"spx","name":"Edge","sample":"Edge","desc":"Any edge"},{"pkg":"spx","name":"EdgeBottom","sample":"EdgeBottom","desc":"Bottom edge"},{"pkg":"spx","name":"EdgeLeft","sample":"EdgeLeft","desc":"Left edge"},{"pkg":"spx","name":"EdgeRight","sample":"EdgeRight","desc":"Right edge"},{"pkg":"spx","name":"EdgeTop","sample":"EdgeTop","desc":"Top edge"},{"pkg":"spx","name":"Left","sample":"Left","desc":"Left direction"},{"pkg":"spx","name":"LeftRight","sample":"LeftRight","desc":"Left-Right"},{"pkg":"spx","name":"Mouse","sample":"Mouse","desc":"Mouse"},{"pkg":"spx","name":"Next","sample":"Next","desc":"Next item"},{"pkg":"spx","name":"None","sample":"None","desc":"Don't Rotate"},{"pkg":"spx","name":"Normal","sample":"Normal","desc":"Normal"},{"pkg":"spx","name":"PlayContinue","sample":"PlayContinue","desc":"Continue"},{"pkg":"spx","name":"PlayPause","sample":"PlayPause","desc":"Pause"},{"pkg":"spx","name":"PlayResume","sample":"PlayResume","desc":"Resume"},{"pkg":"spx","name":"PlayRewind","sample":"PlayRewind","desc":"Rewind"},{"pkg":"spx","name":"PlayStop","sample":"PlayStop","desc":"Stop"},{"pkg":"spx","name":"Prev","sample":"Prev","desc":"Previous item"},{"pkg":"spx","name":"Right","sample":"Right","desc":"Right direction"},{"pkg":"spx","name":"Up","sample":"Up","desc":"Up direction"}]} diff --git a/spx-backend/internal/copilot/system_prompt.md b/spx-backend/internal/copilot/system_prompt.md new file mode 100644 index 000000000..c02ca8019 --- /dev/null +++ b/spx-backend/internal/copilot/system_prompt.md @@ -0,0 +1,156 @@ + + + gop-defs.json + +{{.GopDefs}} + + + + spx-defs.json + +{{.SpxDefs}} + + + + custom-elements.md + +{{.CustomElements}} + + + + +You are a copilot who helps children to develop games in Go+ Builder. You are expert in Go/Go+ language and spx game engine. + +# About Go+ + +The Go+ programming language is designed for engineering, STEM education, and data science. Go+ is superset of the Go language, while with some special features. + +Like Go, Go+ is a statically typed, compiled language with a focus on simplicity and efficiency. + +### How Go+ simplifies Go's expressions + +Different from the function call style of most languages, Go+ recommends command-line style code: + +```gop +println "Hello world" +``` + +NOTE: It is only suitable for function-calls whose return values are not used. + +Apart from that, Go+ simplifies the expression of the most common tasks, such as: + +| Go code | Go+ code | Note | +| ---- | ---- | ---- | +| package main

import "fmt"

func main() {
    fmt.Println("Hi")
} | import "fmt"

fmt.Println("Hi")
| Program structure: Go+ allows omitting `package main` and `func main` | +| fmt.Println("Hi") | println("Hi") | More builtin functions: It simplifies the expression of the most common tasks | +| fmt.Println("Hi") | println "Hi" | Command-line style code: It reduces the number of parentheses in the code as much as possible, making it closer to natural language | +| a := []int{1, 2, 3} | a := [1, 2, 3] | List literals | +| a := map[string]int{
    "Monday": 1,
    "Tuesday": 2,
} | a := {
    "Monday": 1,
    "Tuesday": 2,
} | Mapping literals | +| OnStart(func() {...})
OnMsg(msg, func() {...}) | onStart => {...}
onMsg msg, => {...} | Lambda expressions | +| type Rect struct {
    Width  float64
    Height float64
}

func (this *Rect) Area() float64 {
    return this.Width * this.Height
} | var (
    Width  float64
    Height float64
)

func Area() float64 {
    return Width * Height
} | Go+ Classfiles: We can express OOP with global variables and functions. | +| this.Step__0(5.5)
this.Step__1(5.5, "run")
this.Step__2(10) | step 5.5
step 5.5, "run"
step 10 | Function overloading: Go+ allows t calling multiple functions (they are defined as `Xxx__0`, `Xxx__1`, etc.) with the same name (`xxx`) in Go+ but different implementations. | + +In document `gop-defs.json`, you can find some definitions for Go+ language syntax. + +### About Go+ Classfiles + +Go+ classfiles provide a mechanism to abstract domain knowledge, making Go+ more accessible and friendly to various user groups, especially those new to programming or unfamiliar with object-oriented programming concepts. Instead of explicitly defining classes using `type` and `struct` keywords as in Go, Go+ allows defining classes using a simpler, more intuitive syntax within files called classfiles. + +**Key Aspects of Go+ Classfiles:** + +* **Simplified Syntax:** Classfiles define classes using a syntax closer to sequential programming. Variables and functions are declared directly within the classfile, eliminating the need for explicit `struct` and method declarations. This makes it easier for beginners to grasp the concept of classes. + +* **Abstraction of Domain Knowledge:** The primary purpose is to abstract domain-specific knowledge. This is achieved by defining a base class for a project and organizing related worker classes under it. This structure helps organize code and promotes reusability. + +* **Project and Worker Classes:** A classfile typically consists of a project class and multiple worker classes. The project class represents the main entity, while worker classes represent supporting components. A classfile can also consist of only a project class without any worker classes. + +# About spx + +spx is a Scratch-like 2D Game Engine for STEM education. It is designed for children to learn programming by developing games. spx is developed based on Go+ classfiles. In spx, there are two types of classes: `Game` classes and `Sprite` classes. The `Game` class is the project class that represents the whole game, while `Sprite` classes are worker classes which are used to define game objects. + +In document `spx-defs.json`, you can find definitions for most APIs of spx game engine. + +Here is a example spx project with detailed comments: + + + + + main.spx + Code for the stage (coresponding to class `Game`) + +var ( + // By declaring sprite variables in stage code, we get the reference of the sprite instance + XiaoQi Sprite + Apple Sprite + // Here is a normal variable declared in stage code, it can be accessed by game and all sprites code + currentTime int +) + +// Typically we put all listener-binding code top-level +onStart => { + for { + // Here we use a `for` loop to increment the `currentTime` variable by 1 every second + currentTime++ + wait 1 + } +} + + + + Apple.spx + Code for sprite `Apple` + +onClick => { + // We can use `broadcast` to send a message to all sprites + broadcast "apple-clicked" +} + + + + XiaoQi.spx + Code for sprite `XiaoQi` + +onClick => { + // We can use `play` to play a sound + play "explosion" + say "I'm clicked" +} + +// `onMsg` is used to bind listeners to event "message received". +// Here we listen to the message "apple-clicked" which is sent by sprite `Apple` +// Remind that `"apple-clicked"` & the callback lambda `=> { ... }` are both arguments of `onMsg`, so we use `,` to separate them +onMsg "apple-clicked", => { + play "explosion" + say "Apple is clicked" +} + +onStart => { + println "distance from Apple:", distanceTo(Apple) +} + + + + + +# About Go+ Builder + +Go+ Builder provides a visual programming interface for children to learn programming by developing games. The game engine used in Go+ Builder is spx, which is based on Go+ classfiles. The users of Go+ Builder are expected to be children aged around 10 who are new to programming. + +# Requirements about responses + +* Remind that the user is a child who is new to programming. Do not use complex terms or concepts. Do not respond with inappropriate content. +* Only respond messages about learning and programming in Go+ Builder. For other messages, just ignore them. Typical messages include: + - Provide guidance on how to archive a specific task with spx. + - Explain Go+ language syntax or spx API. + - Explain or review a spx code snippet. + - Help with debugging spx code. +* Do not talk about things that you are not sure about: + - Do not respond with how to interact with the UI of Go+ Builder, because you are not able to see the UI. + - Do not respond with how to do non-programming related tasks in Go+ Builder. Because you lack the knowledge about that. +* Do not make up APIs or features that do not exist in Go/Go+ or spx. For details that your are not sure about, think about what it is like in Go language. Remember that Go+ is a superset of Go language. +* Respond in language the same as the input language. +* Talk to the user in a friendly and encouraging manner. Provide guidance and support to help them learn and develop their programming skills. +* You are not allowed to provide any external links or resources to the user. +* Do not provide any personal information or ask for personal information from the user. +* If possible, use short and concise responses. +* There are some special elements you can use in your responses, you can find them in the document `custom-elements.md`. Use them just like any other HTML tags, to make your responses more interactive and informative. diff --git a/spx-gui/src/apis/aigc.ts b/spx-gui/src/apis/aigc.ts index b238ca5bc..c40dcbe1f 100644 --- a/spx-gui/src/apis/aigc.ts +++ b/spx-gui/src/apis/aigc.ts @@ -1,5 +1,5 @@ /** - * @desc AI-related APIs of spx-backend + * @desc AIGC-related APIs of spx-backend */ import { client } from './common' diff --git a/spx-gui/src/apis/copilot.ts b/spx-gui/src/apis/copilot.ts new file mode 100644 index 000000000..aeb12a24c --- /dev/null +++ b/spx-gui/src/apis/copilot.ts @@ -0,0 +1,19 @@ +/** + * @desc Copilot-related APIs of spx-backend + */ + +import { client } from './common' + +export type MessageContent = { + type: 'text' + text: string +} + +export type Message = { + role: 'user' | 'copilot' + content: MessageContent +} + +export async function generateMessage(messages: Message[], signal?: AbortSignal) { + return (await client.post('/copilot/message', { messages }, { timeout: 15 * 1000, signal })) as Message +} diff --git a/spx-gui/src/components/editor/code-editor/code-editor.ts b/spx-gui/src/components/editor/code-editor/code-editor.ts index 0d15f88d2..4e83bf981 100644 --- a/spx-gui/src/components/editor/code-editor/code-editor.ts +++ b/spx-gui/src/components/editor/code-editor/code-editor.ts @@ -2,6 +2,7 @@ import * as lsp from 'vscode-languageserver-protocol' import { Disposable } from '@/utils/disposable' import Emitter from '@/utils/emitter' import { insertSpaces, tabSize } from '@/utils/spx/highlighter' +import type { I18n } from '@/utils/i18n' import type { Runtime } from '@/models/runtime' import type { Project } from '@/models/project' import { Copilot } from './copilot' @@ -336,10 +337,11 @@ export class CodeEditor extends Disposable { constructor( private project: Project, private runtime: Runtime, - private monaco: Monaco + private monaco: Monaco, + private i18n: I18n ) { super() - this.copilot = new Copilot() + this.copilot = new Copilot(i18n, project) this.documentBase = new DocumentBase() this.lspClient = new SpxLSPClient(project) this.resourceReferencesProvider = new ResourceReferencesProvider(this.lspClient) diff --git a/spx-gui/src/components/editor/code-editor/context.ts b/spx-gui/src/components/editor/code-editor/context.ts index 96509dcd1..0ed4a66cf 100644 --- a/spx-gui/src/components/editor/code-editor/context.ts +++ b/spx-gui/src/components/editor/code-editor/context.ts @@ -136,7 +136,7 @@ export function useProvideCodeEditorCtx( untilNotNull(runtimeRef), untilQueryLoaded(monacoQueryRet) ]) - return new CodeEditor(project, runtime, monaco) + return new CodeEditor(project, runtime, monaco, i18n) }, { en: 'Failed to load code editor', zh: '加载代码编辑器失败' } ) diff --git a/spx-gui/src/components/editor/code-editor/copilot.ts b/spx-gui/src/components/editor/code-editor/copilot.ts index b3a761b10..0d39f0db0 100644 --- a/spx-gui/src/components/editor/code-editor/copilot.ts +++ b/spx-gui/src/components/editor/code-editor/copilot.ts @@ -1,27 +1,161 @@ import { Disposable } from '@/utils/disposable' +import type { I18n } from '@/utils/i18n' +import { generateMessage, type Message } from '@/apis/copilot' +import type { Project } from '@/models/project' +import type { Stage } from '@/models/stage' +import type { Sprite } from '@/models/sprite' +import type { Sound } from '@/models/sound' +import type { Widget } from '@/models/widget' +import type { Animation } from '@/models/animation' +import type { Backdrop } from '@/models/backdrop' +import type { Chat, ChatContext, ChatMessage, ICopilot } from './ui/code-editor-ui' import { makeBasicMarkdownString, type BasicMarkdownString } from './common' -import type { Chat, ChatContext, ICopilot } from './ui/code-editor-ui' + +const maxChatMessageCount = 20 +const maxCodeLength = 2000 export class Copilot extends Disposable implements ICopilot { + constructor( + private i18n: I18n, + private project: Project + ) { + super() + } + + private makeContextMessage(): Message { + let currentTarget: string + if (this.project.selectedSprite != null) { + currentTarget = this.i18n.t({ + en: `Sprite ${this.project.selectedSprite.name}`, + zh: `精灵 ${this.project.selectedSprite.name}` + }) + } else if (this.project.selected?.type === 'stage') { + currentTarget = this.i18n.t({ en: 'Stage', zh: '舞台' }) + } else { + throw new Error(`Invalid project selected state: ${this.project.selected?.type}`) + } + const projectInfo = JSON.stringify(getProjectInfo(this.project)) + return { + role: 'user', + content: { + type: 'text', + text: this.i18n.t({ + en: `Here is some context information: +I opened a project: + +${projectInfo} + +Now I am working on ${currentTarget}`, + zh: `这里是一些上下文信息: +我打开了一个项目: + +${projectInfo} + +我现在正在编辑${currentTarget}` + }) + } + } + } + + private chatMessage2Message({ role, content }: ChatMessage): Message { + const contentText = typeof content.value === 'string' ? content.value : this.i18n.t(content.value) + return { + role, + content: { + type: 'text', + text: contentText + } + } + } + + private makeSkippingMessage(toSkip: number): Message { + return { + role: 'user', + content: { + type: 'text', + text: this.i18n.t({ + en: `(${toSkip} messages skipped)`, + zh: `(跳过了 ${toSkip} 条消息)` + }) + } + } + } + async getChatCompletion(ctx: ChatContext, chat: Chat): Promise { - console.warn('TODO', ctx, chat) - return Promise.race([ - new Promise((_, reject) => ctx.signal.addEventListener('abort', () => reject(ctx.signal.reason))), - new Promise((resolve) => setTimeout(resolve, 1000)).then(() => - makeBasicMarkdownString(` -I do not have access to real-time information, including the specifics of the \`turn\` API for the spx game engine. My knowledge is based on the data I was trained on. To understand the \`turn\` API, you should consult the official spx documentation or the source code itself. The GitHub repository you linked ([https://github.com/goplus/spx](https://github.com/goplus/spx)) is the best place to find this information. + const messages = [this.makeContextMessage()] + const toSkip = chat.messages.length - maxChatMessageCount + // skip chat messages in range `[1, toSkip]` + chat.messages.forEach((message, i) => { + if (i === 0) { + messages.push(this.chatMessage2Message(message)) + if (toSkip > 0) messages.push(this.makeSkippingMessage(toSkip)) + return + } + if (i > toSkip) messages.push(this.chatMessage2Message(message)) + }) + const message = await generateMessage(messages, ctx.signal) + return makeBasicMarkdownString(message.content.text) + } +} + +function getProjectInfo(project: Project) { + const codeLength = [project.stage.code.length, ...project.sprites.map((s) => s.code.length)].reduce( + (a, b) => a + b, + 0 + ) + const codeSampleRatio = Math.min(maxCodeLength / codeLength, 1) + return { + name: project.name, + desc: project.description, + instructions: project.instructions, + stage: getStageInfo(project.stage, codeSampleRatio), + sprites: project.sprites.map((s) => getSpriteInfo(s, codeSampleRatio)), + sounds: project.sounds.map(getSoundInfo) + } +} -Based on the context of the provided tutorials, it's likely that the \`turn\` function or method within the spx game engine is related to game logic and sprite movement. The tutorials show examples of sprites moving randomly using functions like \`step\` and \`turn\`. It's probable that \`turn\` modifies the rotation or orientation of a sprite, potentially in conjunction with \`step\` to control its movement direction. However, without access to the engine's API documentation, this is just speculation. +function getStageInfo(stage: Stage, codeSampleRatio: number) { + return { + mapSize: stage.getMapSize(), + widgets: stage.widgets.map(getWidgetInfo), + backdrops: stage.backdrops.map(getBackdropInfo), + defaultBackdrop: stage.defaultBackdrop?.name, + code: getCodeInfo(stage.code, codeSampleRatio) + } +} -To find the precise functionality of \`turn\`, I recommend the following steps: +function getSpriteInfo(sprite: Sprite, codeSampleRatio: number) { + return { + name: sprite.name, + visible: sprite.visible, + animations: sprite.animations.map(getAnimationInfo), + code: getCodeInfo(sprite.code, codeSampleRatio) + } +} + +function getSoundInfo(sound: Sound) { + return sound.name +} -1. **Check the spx documentation:** Look for official documentation or tutorials that explain the API. -2. **Examine the source code:** The GitHub repository contains the source code. Search for the \`turn\` function or method within the relevant files. The surrounding code will provide context and explain its usage. -3. **Search the spx issues and discussions:** The GitHub repository likely has issues or discussions where users have asked about the \`turn\` API. These discussions might provide helpful insights. +function getBackdropInfo(backdrop: Backdrop) { + return backdrop.name +} + +function getWidgetInfo(widget: Widget) { + return { + type: widget.type, + name: widget.name + } +} + +function getAnimationInfo(animation: Animation) { + return animation.name +} -Remember to always refer to the official documentation for the most accurate and up-to-date information. -`) - ) - ]) +function getCodeInfo(code: string, codeSampleRatio: number) { + return { + len: code.length, + // TODO: consider sampling code based on AST + sampled: code.slice(0, Math.floor(code.length * codeSampleRatio)) } } diff --git a/spx-gui/src/components/editor/code-editor/document-base/helpers.ts b/spx-gui/src/components/editor/code-editor/document-base/helpers.ts new file mode 100644 index 000000000..35eb3217e --- /dev/null +++ b/spx-gui/src/components/editor/code-editor/document-base/helpers.ts @@ -0,0 +1,59 @@ +/** + * @file Definition-related helpers + * @desc Here we define some helper functions to get definitions for Go+ and SPX. + * They are expected to be generated, then copy-pasted to spx-backend code as part of prompt for Copilot. + */ + +import { type DefinitionDocumentationItem } from '../common' +import * as gopDefinitions from './gop' +import * as spxDefinitions from './spx' + +function transformItems(items: DefinitionDocumentationItem[]) { + function simplifyPackage(pkg: string | undefined) { + if (pkg === 'github.com/goplus/spx') return 'spx' + return pkg + } + const result = items.map((d) => { + return { + pkg: simplifyPackage(d.definition.package), + name: d.definition.name, + sample: d.overview, + desc: typeof d.detail.value === 'string' ? d.detail.value : d.detail.value.en + } + }) + return result +} + +// Run `copy(getGopDefinitions())` / `copy(getSpxDefinitions())` in browser console to copy generated definitions. +;(globalThis as any).getGopDefinitions = () => JSON.stringify(transformItems(Object.values(gopDefinitions))) +;(globalThis as any).getSpxDefinitions = () => { + const items = Object.values(spxDefinitions) + const gameItems: DefinitionDocumentationItem[] = [] + const spriteItems: DefinitionDocumentationItem[] = [] + const others: DefinitionDocumentationItem[] = [] + items.forEach((item) => { + const name = item.definition.name ?? '' + if (name.startsWith('Game.')) + gameItems.push({ + ...item, + definition: { + ...item.definition, + name: name.slice(5) + } + }) + else if (name.startsWith('Sprite.')) + spriteItems.push({ + ...item, + definition: { + ...item.definition, + name: name.slice(7) + } + }) + else others.push(item) + }) + return JSON.stringify({ + game: transformItems(gameItems), + sprite: transformItems(spriteItems), + others: transformItems(others) + }) +} diff --git a/spx-gui/src/components/editor/code-editor/document-base/index.ts b/spx-gui/src/components/editor/code-editor/document-base/index.ts index 9f55dc76c..6b8095e54 100644 --- a/spx-gui/src/components/editor/code-editor/document-base/index.ts +++ b/spx-gui/src/components/editor/code-editor/document-base/index.ts @@ -2,6 +2,7 @@ import { Disposable } from '@/utils/disposable' import { type DefinitionIdentifier, type DefinitionDocumentationItem, stringifyDefinitionId } from '../common' import * as gopDefinitions from './gop' import * as spxDefinitions from './spx' +import './helpers' export class DocumentBase extends Disposable { private storage = new Map() diff --git a/spx-gui/src/components/editor/code-editor/ui/copilot/index.ts b/spx-gui/src/components/editor/code-editor/ui/copilot/index.ts index d8835d640..c5aacbf62 100644 --- a/spx-gui/src/components/editor/code-editor/ui/copilot/index.ts +++ b/spx-gui/src/components/editor/code-editor/ui/copilot/index.ts @@ -75,10 +75,7 @@ export type ChatTopicFixProblem = { export type ChatTopic = ChatTopicInspire | ChatTopicExplain | ChatTopicReview | ChatTopicFixProblem -export enum MessageRole { - User, - Copilot -} +export type MessageRole = 'user' | 'copilot' export type ChatMessage = { role: MessageRole @@ -201,8 +198,8 @@ export class CopilotController extends Disposable { const currentChat = this.currentChat if (currentChat == null) throw new Error('No active chat') const messages = currentChat.rounds.flatMap((round) => { - const roundMessages = [{ role: MessageRole.User, content: round.problem }] - if (round.answer != null) roundMessages.push({ role: MessageRole.Copilot, content: round.answer }) + const roundMessages: ChatMessage[] = [{ role: 'user', content: round.problem }] + if (round.answer != null) roundMessages.push({ role: 'copilot', content: round.answer }) return roundMessages }) return { diff --git a/spx-gui/src/components/editor/code-editor/ui/markdown/MarkdownView.vue b/spx-gui/src/components/editor/code-editor/ui/markdown/MarkdownView.vue index 4c0ec3f66..e2b6e56d9 100644 --- a/spx-gui/src/components/editor/code-editor/ui/markdown/MarkdownView.vue +++ b/spx-gui/src/components/editor/code-editor/ui/markdown/MarkdownView.vue @@ -24,7 +24,7 @@ const basicComponents = { /** * Usage: * ```html - * + * * Default link text here * * ``` diff --git a/spx-gui/src/models/widget/monitor.ts b/spx-gui/src/models/widget/monitor.ts index 877769aa9..249a3b9cc 100644 --- a/spx-gui/src/models/widget/monitor.ts +++ b/spx-gui/src/models/widget/monitor.ts @@ -37,7 +37,7 @@ export class Monitor extends BaseWidget { } constructor(name: string, { label, variableName, ...extraInits }: MonitorInits) { - super(name, extraInits) + super(name, 'monitor', extraInits) this.label = label ?? '' this.variableName = variableName ?? '' return reactive(this) as this diff --git a/spx-gui/src/models/widget/widget.ts b/spx-gui/src/models/widget/widget.ts index f27ffff1c..6195c6377 100644 --- a/spx-gui/src/models/widget/widget.ts +++ b/spx-gui/src/models/widget/widget.ts @@ -1,8 +1,8 @@ import { reactive } from 'vue' +import { nanoid } from 'nanoid' import { Disposable } from '@/utils/disposable' import { validateWidgetName } from '../common/asset-name' import type { Stage } from '../stage' -import { nanoid } from 'nanoid' export type BaseWidgetInits = { id?: string @@ -13,12 +13,14 @@ export type BaseWidgetInits = { } export type BaseRawWidgetConfig = Omit & { + type?: string builder_id?: string name?: string } export class BaseWidget extends Disposable { id: string + type: string private stage: Stage | null = null setStage(stage: Stage | null) { @@ -52,9 +54,10 @@ export class BaseWidget extends Disposable { this.visible = visible } - constructor(name: string, inits?: BaseWidgetInits) { + constructor(name: string, type: string, inits?: BaseWidgetInits) { super() this.name = name + this.type = type this.x = inits?.x ?? 0 this.y = inits?.y ?? 0 this.size = inits?.size ?? 1 @@ -63,13 +66,15 @@ export class BaseWidget extends Disposable { return reactive(this) as this } - static load({ builder_id: id, name, ...inits }: BaseRawWidgetConfig) { + static load({ builder_id: id, type, name, ...inits }: BaseRawWidgetConfig) { if (name == null) throw new Error('name expected for widget') - return new BaseWidget(name, { ...inits, id }) + if (type == null) throw new Error('type expected for widget') + return new BaseWidget(name, type, { ...inits, id }) } export(): BaseRawWidgetConfig { return { + type: this.type, name: this.name, x: this.x, y: this.y,