From f620ddfc600293615bdfde9c2cb676ea3382989d Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 5 May 2023 11:04:54 +0800 Subject: [PATCH] [add] Neo command prepare hook --- go.mod | 4 +- neo/command/prompt.go | 49 +++++++++++++++ neo/command/prompt_test.go | 39 ++++++++++++ neo/command/request.go | 122 ++++++++++++++++++++++++++++++++++++- 4 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 neo/command/prompt.go create mode 100644 neo/command/prompt_test.go diff --git a/go.mod b/go.mod index 75ae4c78..c81537f9 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,8 @@ require ( github.com/yaoapp/kun v0.9.0 github.com/yaoapp/xun v0.9.0 golang.org/x/crypto v0.7.0 + gopkg.in/yaml.v3 v3.0.1 + rogchap.com/v8go v0.8.0 ) require ( @@ -106,8 +108,6 @@ require ( google.golang.org/protobuf v1.28.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 - rogchap.com/v8go v0.8.0 // indirect ) // go env -w GOPRIVATE=github.com/yaoapp/* diff --git a/neo/command/prompt.go b/neo/command/prompt.go new file mode 100644 index 00000000..a4984d98 --- /dev/null +++ b/neo/command/prompt.go @@ -0,0 +1,49 @@ +package command + +import ( + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou/helper" + "github.com/yaoapp/kun/maps" +) + +// Replace the prompt with the context +func (prompt Prompt) Replace(data maps.MapStrAny) Prompt { + + v := map[string]interface{}{"role": prompt.Role, "content": prompt.Content} + if prompt.Name != "" { + v["name"] = prompt.Name + } + + replaced := helper.Bind(v, data) + res, ok := replaced.(map[string]interface{}) + if !ok { + return prompt + } + + if res["role"] == nil { + prompt.Role = "" + } else if role, ok := res["role"].(string); ok { + prompt.Role = role + } + + if res["name"] == nil { + prompt.Name = "" + } else if name, ok := res["name"].(string); ok { + prompt.Name = name + } + + switch content := res["content"].(type) { + case string: + prompt.Content = content + + default: + if content == nil { + prompt.Content = "" + + } else if bytes, err := jsoniter.Marshal(content); err == nil { + prompt.Content = string(bytes) + } + } + + return prompt +} diff --git a/neo/command/prompt_test.go b/neo/command/prompt_test.go new file mode 100644 index 00000000..4238c9ab --- /dev/null +++ b/neo/command/prompt_test.go @@ -0,0 +1,39 @@ +package command + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/kun/maps" +) + +func TestPromptReplace(t *testing.T) { + + prompt := Prompt{ + Role: "{{ role }}", + Name: "{{ name }}", + Content: "{{ content }}", + } + + data := maps.Of(map[string]interface{}{ + "role": "User", + "name": "Name", + "content": "- Content\n", + }).Dot() + + prompt = prompt.Replace(data) + assert.Equal(t, "User", prompt.Role) + assert.Equal(t, "Name", prompt.Name) + assert.Equal(t, "- Content\n", prompt.Content) + + prompt = Prompt{ + Role: "Role", + Name: "{{ notfound }}", + Content: "{{ content }}", + } + + prompt = prompt.Replace(data) + assert.Equal(t, "Role", prompt.Role) + assert.Equal(t, "", prompt.Name) + assert.Equal(t, "- Content\n", prompt.Content) +} diff --git a/neo/command/request.go b/neo/command/request.go index 2357e7d2..7f4bf2cc 100644 --- a/neo/command/request.go +++ b/neo/command/request.go @@ -2,15 +2,30 @@ package command import ( "fmt" + "strings" "time" "github.com/google/uuid" + v8 "github.com/yaoapp/gou/runtime/v8" + "github.com/yaoapp/kun/maps" + "github.com/yaoapp/kun/utils" "github.com/yaoapp/yao/neo/message" + "rogchap.com/v8go" ) // Run the command func (req *Request) Run(messages []map[string]interface{}, cb func(msg *message.JSON) int) (interface{}, error) { + prompts, err := req.prepare(messages, cb) + if err != nil { + cb(req.msg().Text(fmt.Sprintf("Prepare Before Error: %s\n", err.Error()))) + cb(message.New().Done()) + req.Done() + return nil, err + } + + utils.Dump(prompts) + cb(req.msg().Text(fmt.Sprintf("- Command: %s\n", req.Command.Name))) time.Sleep(200 * time.Millisecond) @@ -24,6 +39,111 @@ func (req *Request) Run(messages []map[string]interface{}, cb func(msg *message. return nil, nil } +// RunPrepare the command +func (req *Request) prepare(messages []map[string]interface{}, cb func(msg *message.JSON) int) ([]Prompt, error) { + + data, err := req.prepareBefore(messages, cb) + if err != nil { + return nil, err + } + + // replace the pro + prompts := []Prompt{} + if req.Command.Prepare.Prompts != nil && len(req.Command.Prepare.Prompts) > 0 { + prompts = append(prompts, req.Command.Prepare.Prompts...) + } + + if data != nil { + data = maps.Of(data).Dot() + for i, prompt := range prompts { + prompts[i] = prompt.Replace(data) + } + } + + return prompts, nil +} + +// prepareBefore hook +func (req *Request) prepareBefore(messages []map[string]interface{}, cb func(msg *message.JSON) int) (map[string]interface{}, error) { + + if req.Prepare.Before == "" { + return nil, nil + } + + // prepare the args + args := []interface{}{ + map[string]interface{}{"stack": req.ctx.Stack, "path": req.ctx.Path}, + messages, + } + + return req.runScript(req.Prepare.Before, args, cb) +} + +func (req *Request) runScript(id string, args []interface{}, cb func(msg *message.JSON) int) (map[string]interface{}, error) { + + namer := strings.Split(id, ".") + method := namer[len(namer)-1] + scriptID := strings.Join(namer[1:len(namer)-1], ".") + script, err := v8.Select(scriptID) + if err != nil { + return nil, err + } + + // make a new script context + v8ctx, err := script.NewContext(req.sid, map[string]interface{}{}) + if err != nil { + return nil, err + } + defer v8ctx.Close() + + // make a new bridge function ssWrite + ssWriteT := v8go.NewFunctionTemplate(v8ctx.Isolate(), func(info *v8go.FunctionCallbackInfo) *v8go.Value { + args := info.Args() + if len(args) != 1 { + return v8go.Null(v8ctx.Isolate()) + } + + text := args[0].String() + cb(req.msg().Text(text)) + return v8go.Null(v8ctx.Isolate()) + }) + + // make a new bridge function done + doneT := v8go.NewFunctionTemplate(v8ctx.Isolate(), func(info *v8go.FunctionCallbackInfo) *v8go.Value { + args := info.Args() + if len(args) == 1 { + text := args[0].String() + cb(req.msg().Text(text)) + } + + cb(message.New().Done()) + req.Done() + return v8go.Null(v8ctx.Isolate()) + }) + + v8ctx.Global().Set("ssWrite", ssWriteT.GetFunction(v8ctx.Context)) + v8ctx.Global().Set("done", doneT.GetFunction(v8ctx.Context)) + + res, err := v8ctx.CallWith(req.ctx, method, args...) + if err != nil { + return nil, err + } + + // return data + switch v := res.(type) { + + case bool: + return nil, nil + + case map[string]interface{}: + return v, nil + + default: + return nil, fmt.Errorf("script return type is not supported") + } + +} + func (req *Request) msg() *message.JSON { return message.New().Command(req.Command.Name, req.Command.ID, req.id) } @@ -70,7 +190,7 @@ func (cmd *Command) NewRequest(ctx Context) (*Request, error) { // Done the request done func (req *Request) Done() { - if DefaultStore == nil { + if DefaultStore != nil { DefaultStore.DelRequest(req.sid) } }