From 153efc1a4999ecb59bbac30103bf9ff1b52ad6c6 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 12 Dec 2024 16:37:59 +0800 Subject: [PATCH] Refactor command handling and conversation management by removing the command package, updating conversation methods to support chat IDs, and enhancing API functionality for chat history and titles. This includes adjustments to the DSL context and conversation settings for improved data handling. --- neo/command/command.go | 138 --------- neo/command/driver/memory.go | 224 -------------- neo/command/driver/memory_test.go | 89 ------ neo/command/driver/redis.go | 1 - neo/command/driver/types.go | 19 -- neo/command/driver/weaviate.go | 1 - neo/command/load.go | 106 ------- neo/command/load_test.go | 43 --- neo/command/prompt.go | 49 ---- neo/command/prompt_test.go | 39 --- neo/command/query/query.go | 55 ---- neo/command/request.go | 473 ------------------------------ neo/command/types.go | 99 ------- neo/{command => }/context.go | 12 +- neo/conversation/mongo.go | 14 +- neo/conversation/redis.go | 14 +- neo/conversation/types.go | 6 +- neo/conversation/weaviate.go | 14 +- neo/conversation/xun.go | 71 ++++- neo/conversation/xun_test.go | 68 ++++- neo/load.go | 22 -- neo/neo.go | 180 +++--------- neo/neo_test.go | 10 +- neo/types.go | 23 +- 24 files changed, 227 insertions(+), 1543 deletions(-) delete mode 100644 neo/command/command.go delete mode 100644 neo/command/driver/memory.go delete mode 100644 neo/command/driver/memory_test.go delete mode 100644 neo/command/driver/redis.go delete mode 100644 neo/command/driver/types.go delete mode 100644 neo/command/driver/weaviate.go delete mode 100644 neo/command/load.go delete mode 100644 neo/command/load_test.go delete mode 100644 neo/command/prompt.go delete mode 100644 neo/command/prompt_test.go delete mode 100644 neo/command/query/query.go delete mode 100644 neo/command/request.go delete mode 100644 neo/command/types.go rename neo/{command => }/context.go (75%) diff --git a/neo/command/command.go b/neo/command/command.go deleted file mode 100644 index 8059569f..00000000 --- a/neo/command/command.go +++ /dev/null @@ -1,138 +0,0 @@ -package command - -import ( - "fmt" - "regexp" - "strings" - - "github.com/yaoapp/gou/connector" - "github.com/yaoapp/yao/aigc" - "github.com/yaoapp/yao/neo/command/driver" - "github.com/yaoapp/yao/neo/command/query" - "github.com/yaoapp/yao/openai" -) - -// DefaultStore the default store driver -var DefaultStore Store -var recmd, _ = regexp.Compile(`^\/([a-zA-Z]+) +`) -var reCmdOnly, _ = regexp.Compile(`^\/([a-zA-Z]+)$`) - -// SetStore the driver interface -func SetStore(store Store) { - DefaultStore = store -} - -// Match the command from the content -func Match(sid string, query query.Param, input string) (string, error) { - - if DefaultStore == nil { - return "", fmt.Errorf("command store is not set") - } - - // Check the command from the store - if id, cid, has := DefaultStore.GetRequest(sid); has { - fmt.Println("Match Requst:", id) - return cid, nil - } - - // Match the command use the command ID - match := reCmdOnly.FindSubmatch([]byte(strings.TrimSpace(input))) - if match == nil { - match = recmd.FindSubmatch([]byte(strings.TrimSpace(input))) - } - if match != nil { - key := fmt.Sprintf("[Index]%s", match[1]) - fmt.Println("Match Index:", key) - - if cmd, ok := DefaultStore.Get(key); ok { - fmt.Println("Match Command:", cmd.ID) - return cmd.ID, nil - } - } - - return DefaultStore.Match(query, input) -} - -// Exit the command -func Exit(sid string) error { - if DefaultStore == nil { - return fmt.Errorf("command store is not set") - } - DefaultStore.DelRequest(sid) - return nil -} - -// GetCommands get all commands -func GetCommands() ([]driver.Command, error) { - if DefaultStore == nil { - return nil, fmt.Errorf("command store is not set") - } - return DefaultStore.GetCommands() -} - -// save the command to the store -func (cmd *Command) save() error { - if DefaultStore == nil { - return nil - } - - args := []map[string]interface{}{} - for _, arg := range cmd.Args { - args = append(args, map[string]interface{}{ - "name": arg.Name, - "description": arg.Description, - "type": arg.Type, - "required": arg.Required, - }) - } - - data := driver.Command{ - ID: cmd.ID, - Name: cmd.Name, - Use: cmd.Use, - Description: cmd.Description, - Args: args, - Stack: cmd.Stack, - Path: cmd.Path, - } - - if cmd.Use != "" { - key := fmt.Sprintf("[Index]%s", cmd.Use) - err := DefaultStore.Set(key, data) - if err != nil { - return err - } - } - - return DefaultStore.Set(cmd.ID, data) -} - -// NewAI create a new AI -func (cmd *Command) newAI() (aigc.AI, error) { - - if cmd.Connector == "" || strings.HasPrefix(cmd.Connector, "moapi") { - model := "gpt-3.5-turbo" - if strings.HasPrefix(cmd.Connector, "moapi:") { - model = strings.TrimPrefix(cmd.Connector, "moapi:") - } - - ai, err := openai.NewMoapi(model) - if err != nil { - return nil, err - } - - cmd.AI = ai - return cmd.AI, nil - } - - conn, err := connector.Select(cmd.Connector) - if err != nil { - return nil, err - } - - if conn.Is(connector.OPENAI) { - return openai.New(cmd.Connector) - } - - return nil, fmt.Errorf("%s connector %s not support, should be a openai", cmd.ID, cmd.Connector) -} diff --git a/neo/command/driver/memory.go b/neo/command/driver/memory.go deleted file mode 100644 index 4b006dce..00000000 --- a/neo/command/driver/memory.go +++ /dev/null @@ -1,224 +0,0 @@ -package driver - -import ( - "fmt" - "strings" - "sync" - - "github.com/yaoapp/gou/connector" - "github.com/yaoapp/yao/aigc" - "github.com/yaoapp/yao/neo/command/query" - "github.com/yaoapp/yao/openai" -) - -var commands = sync.Map{} -var requests = sync.Map{} - -// Memory the memory driver -type Memory struct { - model string - ai aigc.AI - prompts []aigc.Prompt -} - -// NewMemory create a new memory driver -func NewMemory(model string, prompts []aigc.Prompt) (*Memory, error) { - - if prompts == nil || len(prompts) == 0 { - prompts = []aigc.Prompt{ - { - Role: "system", - Content: ` - - Answer my question follow this rules: - - If it can match the "name" or "description" given to you, reply the "ID" of the matched command; - - reply the "ID" only, and do not explain your answer, and do not use punctuation. - - If no matching command is found, reply me . , don't answer redundantly. - `, - }, - } - } - - mem := &Memory{model: model, prompts: prompts} - ai, err := mem.newAI() - if err != nil { - return nil, err - } - mem.ai = ai - return mem, nil -} - -// Match match the command data -func (driver *Memory) Match(query query.Param, content string) (string, error) { - - return "", fmt.Errorf("no related command found") - - // prompts := append([]aigc.Prompt{}, driver.prompts...) - // has := false - // commands.Range(func(key, value interface{}) bool { - // cmd, ok := value.(Command) - // if !ok { - // return true - // } - // if query.MatchAny(cmd.Stack, cmd.Path) { - // has = true - // bytes, err := jsoniter.Marshal(map[string]interface{}{ - // "id": cmd.ID, - // "use": cmd.Use, - // "name": cmd.Name, - // "description": cmd.Description, - // "args": cmd.Args, - // }) - // if err != nil { - // return true - // } - // prompts = append(prompts, aigc.Prompt{ - // Role: "system", - // Content: string(bytes), - // }) - // } - // return true - // }) - - // if !has { - // return "", fmt.Errorf("no related command found") - // } - - // messages := []map[string]interface{}{} - // for _, prompt := range prompts { - // messages = append(messages, map[string]interface{}{ - // "role": prompt.Role, - // "content": prompt.Content, - // }) - // } - - // messages = append(messages, map[string]interface{}{ - // "role": "user", - // "content": content, - // }) - - // res, ex := driver.ai.ChatCompletions(messages, nil, nil) - // if ex != nil { - // return "", fmt.Errorf(ex.Message) - // } - - // bytes, err := jsoniter.Marshal(res) - // if err != nil { - // return "", err - // } - - // var data struct { - // Choices []struct{ Message struct{ Content string } } - // } - // err = jsoniter.Unmarshal(bytes, &data) - // if err != nil { - // return "", err - // } - - // if len(data.Choices) == 0 { - // return "", fmt.Errorf("no related command found") - // } - - // return data.Choices[0].Message.Content, nil -} - -// Set Set the command data -func (driver *Memory) Set(key string, cmd Command) error { - commands.Store(key, cmd) - return nil -} - -// Del delete the command data -func (driver *Memory) Del(key string) { - commands.Delete(key) -} - -// Get the command data -func (driver *Memory) Get(key string) (Command, bool) { - v, ok := commands.Load(key) - if !ok { - return Command{}, false - } - cmd, ok := v.(Command) - if !ok { - return Command{}, false - } - return cmd, true -} - -// SetRequest set the command request -func (driver *Memory) SetRequest(sid, id, cid string) error { - requests.Store(sid, Request{ - ID: id, - Cid: cid, - Sid: sid, - }) - return nil -} - -// GetRequest get the command request -func (driver *Memory) GetRequest(sid string) (string, string, bool) { - v, ok := requests.Load(sid) - if !ok { - return "", "", false - } - - r, ok := v.(Request) - if !ok { - return "", "", false - } - - return r.ID, r.Cid, true -} - -// DelRequest delete the command request -func (driver *Memory) DelRequest(sid string) { - requests.Delete(sid) -} - -// GetCommands get all commands -func (driver *Memory) GetCommands() ([]Command, error) { - resulets := []Command{} - commands.Range(func(key, value interface{}) bool { - - if strings.HasPrefix(key.(string), "[Index]") { - return true - } - - cmd, ok := value.(Command) - if !ok { - return true - } - resulets = append(resulets, cmd) - return true - }) - - return resulets, nil -} - -// NewAI create a new AI -func (driver *Memory) newAI() (aigc.AI, error) { - - if driver.model == "" || strings.HasPrefix(driver.model, "moapi") { - model := "gpt-3.5-turbo" - if strings.HasPrefix(driver.model, "moapi:") { - model = strings.TrimPrefix(driver.model, "moapi:") - } - - ai, err := openai.NewMoapi(model) - if err != nil { - return nil, err - } - return ai, nil - } - - conn, err := connector.Select(driver.model) - if err != nil { - return nil, err - } - - if conn.Is(connector.OPENAI) { - return openai.New(driver.model) - } - - return nil, fmt.Errorf("connector %s not support, should be a openai", driver.model) -} diff --git a/neo/command/driver/memory_test.go b/neo/command/driver/memory_test.go deleted file mode 100644 index ff2ceb2a..00000000 --- a/neo/command/driver/memory_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package driver - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/yaoapp/yao/config" - "github.com/yaoapp/yao/neo/command/query" - "github.com/yaoapp/yao/test" -) - -func TestMemorySetGetDel(t *testing.T) { - test.Prepare(t, config.Conf) - defer test.Clean() - - mem := prepare(t) - err := mem.Set("table.delete", Command{ - ID: "table.delete", - Name: "Generate test data for the table", - Description: "Generate test data for the table", - Stack: "Table.*", - Path: "*", - Args: []map[string]interface{}{ - { - "name": "data", - "type": "Array", - "description": "The data sets to generate", - "required": true, - "default": []interface{}{}, - }, - }, - }) - - if err != nil { - t.Fatal(err) - } - - cmd, has := mem.Get("table.delete") - if !has { - t.Fatal("table.delete not found") - } - - assert.Equal(t, "table.delete", cmd.ID) - mem.Del("table.delete") - - _, has = mem.Get("table.delete") - assert.False(t, has) -} - -func TestMemoryMatch(t *testing.T) { - test.Prepare(t, config.Conf) - defer test.Clean() - - mem := prepare(t) - // id, err := mem.Match(query.Param{Stack: "Table.Page.pet"}, "Generate table test data") - // if err != nil { - // t.Fatal(err) - // } - // assert.Equal(t, "table.data", id) - - _, err := mem.Match(query.Param{Stack: "Form.Page.pet", Path: "/Form/pet"}, "Generate table test data") - assert.ErrorContains(t, err, "no related command found") -} - -func prepare(t *testing.T) *Memory { - mem, err := NewMemory("gpt-3_5-turbo", nil) - if err != nil { - t.Fatal(err) - } - - mem.Set("table.data", Command{ - ID: "table.data", - Name: "Generate test data for the table", - Description: "Generate test data for the table", - Stack: "Table.*", - Path: "Table.*", - Args: []map[string]interface{}{ - { - "name": "data", - "type": "Array", - "description": "The data sets to generate", - "required": true, - "default": []interface{}{}, - }, - }, - }) - - return mem -} diff --git a/neo/command/driver/redis.go b/neo/command/driver/redis.go deleted file mode 100644 index bce7c468..00000000 --- a/neo/command/driver/redis.go +++ /dev/null @@ -1 +0,0 @@ -package driver diff --git a/neo/command/driver/types.go b/neo/command/driver/types.go deleted file mode 100644 index 282dd496..00000000 --- a/neo/command/driver/types.go +++ /dev/null @@ -1,19 +0,0 @@ -package driver - -// Request the command request -type Request struct { - ID string - Sid string - Cid string -} - -// Command the command struct -type Command struct { - ID string `json:"-" yaml:"-"` - Use string `json:"use,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Args []map[string]interface{} `json:"args,omitempty"` - Stack string `json:"stack,omitempty"` - Path string `json:"path,omitempty"` -} diff --git a/neo/command/driver/weaviate.go b/neo/command/driver/weaviate.go deleted file mode 100644 index bce7c468..00000000 --- a/neo/command/driver/weaviate.go +++ /dev/null @@ -1 +0,0 @@ -package driver diff --git a/neo/command/load.go b/neo/command/load.go deleted file mode 100644 index c8bf9ace..00000000 --- a/neo/command/load.go +++ /dev/null @@ -1,106 +0,0 @@ -package command - -import ( - "fmt" - "strings" - - "github.com/yaoapp/gou/application" - "github.com/yaoapp/yao/config" - "github.com/yaoapp/yao/share" -) - -// Commands the commands -var Commands = map[string]*Command{} - -// Autopilots the autopilots -var Autopilots = []string{} - -// Load load AIGC -func Load(cfg config.Config) error { - exts := []string{"*.cmd.yml", "*.cmd.yaml"} - messages := []string{} - - err := application.App.Walk("neo", func(root, file string, isdir bool) error { - if isdir { - return nil - } - - id := share.ID(root, file) - _, err := LoadFile(file, id) - if err != nil { - messages = append(messages, err.Error()) - } - return nil - }, exts...) - - if err != nil { - return err - } - - if len(messages) > 0 { - return fmt.Errorf("%s", strings.Join(messages, ";\n")) - } - - return nil - -} - -// LoadFile load AIGC by file -func LoadFile(file string, id string) (*Command, error) { - - data, err := application.App.Read(file) - if err != nil { - return nil, err - } - return LoadSource(data, file, id) -} - -// LoadSource load AIGC -func LoadSource(data []byte, file, id string) (*Command, error) { - - cmd := Command{ - ID: id, - Prepare: Prepare{ - Option: map[string]interface{}{}, - }, - Optional: Optional{ - Autopilot: false, - Confirm: false, - MaxAttempts: 10, - }, - } - - err := application.Parse(file, data, &cmd) - if err != nil { - return nil, err - } - - if cmd.Process == "" { - return nil, fmt.Errorf("%s process is required", id) - } - - if cmd.Prepare.Prompts == nil || len(cmd.Prepare.Prompts) == 0 { - return nil, fmt.Errorf("%s prompts is required", id) - } - - // create AI interface - cmd.AI, err = cmd.newAI() - if err != nil { - return nil, err - } - - // add to autopilots - if cmd.Optional.Autopilot { - Autopilots = append(Autopilots, id) - } - - // save - err = cmd.save() - if err != nil { - return nil, err - } - - // add to AIGCs - Commands[id] = &cmd - return Commands[id], nil -} diff --git a/neo/command/load_test.go b/neo/command/load_test.go deleted file mode 100644 index 4b012217..00000000 --- a/neo/command/load_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package command - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/yaoapp/yao/config" - "github.com/yaoapp/yao/neo/command/driver" - "github.com/yaoapp/yao/test" -) - -func TestLoad(t *testing.T) { - test.Prepare(t, config.Conf) - defer test.Clean() - - Commands = map[string]*Command{} - Load(config.Conf) - check(t) -} - -func TestLoadWithStore(t *testing.T) { - test.Prepare(t, config.Conf) - defer test.Clean() - - Commands = map[string]*Command{} - mem, err := driver.NewMemory("gpt-3_5-turbo", nil) - if err != nil { - t.Fatal(err) - } - - SetStore(mem) - Load(config.Conf) - check(t) -} - -func check(t *testing.T) { - ids := map[string]bool{} - for id := range Commands { - ids[id] = true - } - assert.True(t, ids["table.data"]) - assert.GreaterOrEqual(t, len(Autopilots), 1) -} diff --git a/neo/command/prompt.go b/neo/command/prompt.go deleted file mode 100644 index a4984d98..00000000 --- a/neo/command/prompt.go +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 4238c9ab..00000000 --- a/neo/command/prompt_test.go +++ /dev/null @@ -1,39 +0,0 @@ -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/query/query.go b/neo/command/query/query.go deleted file mode 100644 index 7af58b10..00000000 --- a/neo/command/query/query.go +++ /dev/null @@ -1,55 +0,0 @@ -package query - -import ( - "regexp" - "strings" -) - -// Param the command param -type Param struct { - Stack string `json:"stack,omitempty"` - Path string `json:"path,omitempty"` -} - -// MatchStack match the stack -func (query Param) MatchStack(stack string) bool { - - if stack == "" || stack == "*" || query.Stack == "" { - return true - } - - if stack == query.Stack { - return true - } - - matched, _ := regexp.MatchString(strings.ReplaceAll(stack, "*", ".*"), query.Stack) - return matched -} - -// MatchPath match the path -func (query Param) MatchPath(path string) bool { - if path == "" || path == "*" || query.Path == "" { - return true - } - - if path == query.Path { - return true - } - - matched, _ := regexp.MatchString(strings.ReplaceAll(path, "*", ".*"), query.Path) - return matched -} - -// MatchAny match the stack or path -func (query Param) MatchAny(stack, path string) bool { - - if path == "" || path == "-" { - return query.MatchStack(stack) - } - - if stack == "" || stack == "-" { - return query.MatchPath(path) - } - - return query.MatchStack(stack) || query.MatchPath(path) -} diff --git a/neo/command/request.go b/neo/command/request.go deleted file mode 100644 index 6e0da9ac..00000000 --- a/neo/command/request.go +++ /dev/null @@ -1,473 +0,0 @@ -package command - -import ( - "fmt" - "strings" - - "github.com/google/uuid" - jsoniter "github.com/json-iterator/go" - "github.com/yaoapp/gou/process" - v8 "github.com/yaoapp/gou/runtime/v8" - "github.com/yaoapp/kun/log" - "github.com/yaoapp/kun/maps" - "github.com/yaoapp/kun/utils" - "github.com/yaoapp/yao/config" - "github.com/yaoapp/yao/neo/conversation" - "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) error { - - // Enter the command mode - if input, ok := messages[len(messages)-1]["content"].(string); ok { - match := reCmdOnly.FindSubmatch([]byte(strings.TrimSpace(input))) - if match != nil { - fmt.Printf("Match Command: %s | %s\n", match[1], input) - cb(req.msg().Text("Enter the command Mode")) - cb(req.msg().Done()) - return nil - } - } - - if config.Conf.Mode == "development" { - utils.Dump("----Request Run ----") - fmt.Printf("Command Request: %s %s\n", req.Command.ID, req.sid) - fmt.Printf("Command Process: %s\n", req.Command.Process) - fmt.Printf("Command Prepare Before: %s\n", req.Command.Prepare.Before) - fmt.Printf("Command Prepare Before: %s\n", req.Command.Prepare.After) - } - - input, err := req.prepare(messages, cb) - if err != nil { - req.error(err, cb) - return err - } - - if config.Conf.Mode == "development" { - utils.Dump("----Input After Prepare ----", input) - } - - args, err := req.parseArgs(input, cb) - if err != nil { - cb(req.msg().Text("\n\n" + err.Error())) - cb(req.msg().Done()) - return nil - } - - if config.Conf.Mode == "development" { - utils.Dump("---- Command Args ----", args) - } - - // Send the command to the service - if req.Command.Optional.Confirm { - req.confirm(args, cb) - return nil - } - - // Execute the command by script - if strings.HasPrefix(req.Command.Process, "scripts.") || strings.HasPrefix(req.Command.Process, "studio.") { - - res, err := req.runScript(req.Command.Process, args, cb) - if err != nil { - cb(req.msg().Text("\n\n" + err.Error())) - return err - } - - msg := req.msg().Bind(res) - if req.Actions != nil && len(req.Actions) > 0 { - for _, action := range req.Actions { - msg.Action(action.Name, action.Type, action.Payload, action.Next) - } - } - - cb(msg.Done()) - return nil - } - - // Other process - p, err := process.Of(req.Command.Process, args...) - if err != nil { - return err - } - - res, err := p.Exec() - if err != nil { - cb(req.msg().Text("\n\n" + err.Error())) - return err - } - - msg := req.msg() - if data, ok := res.(map[string]interface{}); ok { - msg = msg.Bind(data) - } - - if req.Actions != nil && len(req.Actions) > 0 { - for _, action := range req.Actions { - msg.Action(action.Name, action.Type, action.Payload, action.Next) - } - } - - // DONE - cb(msg.Done()) - return nil -} - -// confirm the command -func (req *Request) confirm(args []interface{}, cb func(msg *message.JSON) int) { - - payload := map[string]interface{}{ - "method": "ExecCommand", - "args": []interface{}{ - req.id, - req.Command.Process, - args, - map[string]interface{}{"stack": req.ctx.Stack, "path": req.ctx.Path}, - }, - } - - msg := req.msg(). - Action("ExecCommand", "Service.__neo", payload, ""). - Confirm(). - Done() - - if req.Actions != nil && len(req.Actions) > 0 { - for _, action := range req.Actions { - msg.Action(action.Name, action.Type, action.Payload, action.Next) - } - } - - cb(msg) -} - -// validate the command -func (req *Request) parseArgs(input interface{}, cb func(msg *message.JSON) int) ([]interface{}, error) { - args := []interface{}{} - data := map[string]interface{}{} - - switch v := input.(type) { - case string: - err := jsoniter.Unmarshal([]byte(v), &data) - if err != nil { - return nil, err - } - break - - case []byte: - err := jsoniter.Unmarshal(v, &data) - if err != nil { - return nil, err - } - break - - case map[string]interface{}: - data = v - break - - default: - err := fmt.Errorf("\nInvalid input type: %T", v) - req.error(err, cb) - return nil, err - } - - // validate the args - if req.Command.Args != nil && len(req.Command.Args) > 0 { - for _, arg := range req.Command.Args { - v, ok := data[arg.Name] - if arg.Required && !ok { - err := fmt.Errorf("\nMissing required argument: %s", arg.Name) - return nil, err - } - - // @todo: validate the type - args = append(args, v) - } - } - - return args, nil -} - -// RunPrepare the command -func (req *Request) prepare(messages []map[string]interface{}, cb func(msg *message.JSON) int) (interface{}, error) { - - if config.Conf.Mode == "development" { - utils.Dump("----Messages Before Prepare ----", messages) - } - - // Before hook - 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) - } - } - - question, err := req.question(messages) - if err != nil { - req.error(err, cb) - return nil, err - } - - chatMessages, err := req.messages(prompts, question) - if err != nil { - req.error(err, cb) - return nil, err - } - - if config.Conf.Mode == "development" { - utils.Dump("----Command Prompts ----", chatMessages) - } - - // chat with AI - content := []byte{} - _, ex := req.AI.ChatCompletionsWith(req.ctx, chatMessages, req.Prepare.Option, func(data []byte) int { - msg := message.NewOpenAI(data) - if msg != nil { - if msg.IsDone() { - return 0 - } - content = msg.Append(content) - cb(req.msg().Text(msg.String())) - } - return 1 - }) - - if ex != nil { - req.error(fmt.Errorf(ex.Message), cb) - return nil, fmt.Errorf("Chat error: %s", ex.Message) - } - defer req.saveHistory(content, chatMessages) - - // After hook - args, err := req.prepareAfter(string(content), cb) - if err != nil { - log.Error("Prepare after error: %s", err.Error()) - fmt.Println(err) - return content, nil - } - - return args, 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) -} - -// prepareAfter hook -func (req *Request) prepareAfter(content string, cb func(msg *message.JSON) int) (interface{}, error) { - - if req.Prepare.After == "" { - return content, nil - } - - // prepare the args - args := []interface{}{ - content, - map[string]interface{}{"stack": req.ctx.Stack, "path": req.ctx.Path}, // context - } - - return req.runScript(req.Prepare.After, args, cb) -} - -// saveHistory save the history -func (req *Request) saveHistory(content []byte, messages []map[string]interface{}) { - - if len(content) > 0 && req.sid != "" && len(messages) > 0 { - err := req.conversation.SaveRequest( - req.sid, - req.id, - req.Command.ID, - []map[string]interface{}{ - {"role": "user", "content": messages[len(messages)-1]["content"], "name": req.sid}, - {"role": "assistant", "content": string(content), "name": req.sid}, - }, - ) - - if err != nil { - log.Error("Save request error: %s", err.Error()) - } - } -} - -func (req *Request) error(err error, cb func(msg *message.JSON) int) { - cb(req.msg().Text(err.Error())) - cb(req.msg().Done()) - // req.Done() -} - -func (req *Request) question(messages []map[string]interface{}) (string, error) { - if len(messages) < 1 { - return "", fmt.Errorf("No messages") - } - - question, ok := messages[len(messages)-1]["content"].(string) - if !ok { - return "", fmt.Errorf("messages content is not string") - } - - return question, nil -} - -func (req *Request) messages(prompts []Prompt, question string) ([]map[string]interface{}, error) { - messages := []map[string]interface{}{} - for _, prompt := range prompts { - message := map[string]interface{}{"role": prompt.Role, "content": prompt.Content} - if prompt.Name != "" { - message["name"] = prompt.Name - } - messages = append(messages, message) - } - - history, err := req.conversation.GetRequest(req.sid, req.id) - if err != nil { - return nil, err - } - messages = append(messages, history...) - messages = append(messages, map[string]interface{}{"role": "user", "content": question, "name": req.sid}) - return messages, nil -} - -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], ".") - - var err error - var script *v8.Script - if namer[0] == "scripts" { - script, err = v8.Select(scriptID) - } else if namer[0] == "studio" { - script, err = v8.SelectRoot(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() - - v8ctx.WithFunction("ssWrite", func(info *v8go.FunctionCallbackInfo) *v8go.Value { - args := info.Args() - if len(args) != 1 { - return v8go.Null(info.Context().Isolate()) - } - - text := args[0].String() - cb(req.msg().Text(text)) - return v8go.Null(info.Context().Isolate()) - }) - - v8ctx.WithFunction("done", func(info *v8go.FunctionCallbackInfo) *v8go.Value { - args := info.Args() - if len(args) == 1 { - text := args[0].String() - cb(req.msg().Text(text)) - } - - cb(req.msg().Done()) - // req.Done() - return v8go.Null(v8ctx.Context.Isolate()) - }) - - 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) -} - -// NewRequest create a new request -func (cmd *Command) NewRequest(ctx Context, conversation conversation.Conversation) (*Request, error) { - - if DefaultStore == nil { - return nil, fmt.Errorf("command store is not set") - } - - if ctx.Sid == "" { - return nil, fmt.Errorf("context sid is request") - } - - // continue the request - id, cid, has := DefaultStore.GetRequest(ctx.Sid) - if has { - if cid != cmd.ID { - return nil, fmt.Errorf("request id is not match") - } - return &Request{ - Command: cmd, - sid: ctx.Sid, - id: id, - ctx: ctx, - conversation: conversation, - }, nil - } - - // create a new request - id = uuid.New().String() - err := DefaultStore.SetRequest(ctx.Sid, id, cmd.ID) - if err != nil { - return nil, err - } - - return &Request{ - Command: cmd, - sid: ctx.Sid, - id: id, - ctx: ctx, - conversation: conversation, - }, nil -} - -// Done the request done -func (req *Request) Done() { - if DefaultStore != nil { - DefaultStore.DelRequest(req.sid) - } -} diff --git a/neo/command/types.go b/neo/command/types.go deleted file mode 100644 index f9026f28..00000000 --- a/neo/command/types.go +++ /dev/null @@ -1,99 +0,0 @@ -package command - -import ( - "context" - - "github.com/yaoapp/yao/aigc" - "github.com/yaoapp/yao/neo/command/driver" - "github.com/yaoapp/yao/neo/command/query" - "github.com/yaoapp/yao/neo/conversation" - "github.com/yaoapp/yao/neo/message" -) - -// Request the command request -type Request struct { - id string - sid string - ctx Context - conversation conversation.Conversation - *Command -} - -// Command the command struct -type Command struct { - ID string `json:"-" yaml:"-"` - Name string `json:"name,omitempty"` - Use string `json:"use,omitempty"` - Connector string `json:"connector"` - Process string `json:"process"` - Prepare Prepare `json:"prepare"` - Description string `json:"description,omitempty"` - Optional Optional `json:"optional,omitempty"` - Args []Arg `json:"args,omitempty"` - Actions []message.Action `json:"actions,omitempty"` - Stack string `json:"stack,omitempty"` // query stack - Path string `json:"path,omitempty"` // query path - AI aigc.AI `json:"-" yaml:"-"` -} - -// Arg the argument -type Arg struct { - Name string `json:"name"` - Type string `json:"type"` - Description string `json:"description,omitempty"` - Default interface{} `json:"default,omitempty"` - Required bool `json:"required,omitempty"` -} - -// Prepare the prepare struct -type Prepare struct { - Before string `json:"before,omitempty"` - After string `json:"after,omitempty"` - Prompts []Prompt `json:"prompts"` - Option map[string]interface{} `json:"option"` -} - -// Prompt a prompt -type Prompt struct { - Role string `json:"role"` - Content string `json:"content"` - Name string `json:"name,omitempty"` -} - -// Optional optional -type Optional struct { - Autopilot bool `json:"autopilot,omitempty"` - Confirm bool `json:"confirm,omitempty"` - MaxAttempts int `json:"maxAttempts,omitempty"` // default 10 -} - -// Context the context -type Context struct { - Sid string `json:"sid" yaml:"-"` - Stack string `json:"stack,omitempty"` - Path string `json:"pathname,omitempty"` - FormData map[string]interface{} `json:"formdata,omitempty"` - Field *ContextField `json:"field,omitempty"` - Namespace string `json:"namespace,omitempty"` - Config map[string]interface{} `json:"config,omitempty"` - Signal interface{} `json:"signal,omitempty"` - context.Context `json:"-" yaml:"-"` -} - -// ContextField the context field -type ContextField struct { - Name string `json:"name,omitempty"` - Bind string `json:"bind,omitempty"` -} - -// Store the command driver -type Store interface { - Match(query query.Param, content string) (string, error) - Set(key string, cmd driver.Command) error - Get(key string) (driver.Command, bool) - Del(key string) - SetRequest(sid, id, cid string) error - GetRequest(sid string) (string, string, bool) - DelRequest(sid string) - GetCommands() ([]driver.Command, error) -} diff --git a/neo/command/context.go b/neo/context.go similarity index 75% rename from neo/command/context.go rename to neo/context.go index 5496dd1a..100aef97 100644 --- a/neo/command/context.go +++ b/neo/context.go @@ -1,4 +1,4 @@ -package command +package neo import ( "context" @@ -9,7 +9,7 @@ import ( ) // NewContext create a new context -func NewContext(sid, payload string) Context { +func NewContext(sid, cid, payload string) Context { ctx := Context{Context: context.Background(), Sid: sid} if payload == "" { return ctx @@ -23,14 +23,14 @@ func NewContext(sid, payload string) Context { } // NewContextWithCancel create a new context with cancel -func NewContextWithCancel(sid, payload string) (Context, context.CancelFunc) { - ctx := NewContext(sid, payload) +func NewContextWithCancel(sid, cid, payload string) (Context, context.CancelFunc) { + ctx := NewContext(sid, cid, payload) return ContextWithCancel(ctx) } // NewContextWithTimeout create a new context with timeout -func NewContextWithTimeout(sid, payload string, timeout time.Duration) (Context, context.CancelFunc) { - ctx := NewContext(sid, payload) +func NewContextWithTimeout(sid, cid, payload string, timeout time.Duration) (Context, context.CancelFunc) { + ctx := NewContext(sid, cid, payload) return ContextWithTimeout(ctx, timeout) } diff --git a/neo/conversation/mongo.go b/neo/conversation/mongo.go index 72f263f7..6fe46e50 100644 --- a/neo/conversation/mongo.go +++ b/neo/conversation/mongo.go @@ -8,13 +8,23 @@ func NewMongo() *Mongo { return &Mongo{} } +// UpdateChatTitle update the chat title +func (conv *Mongo) UpdateChatTitle(sid string, cid string, title string) error { + return nil +} + +// GetChats get the chat list +func (conv *Mongo) GetChats(sid string) ([]map[string]interface{}, error) { + return []map[string]interface{}{}, nil +} + // GetHistory get the history -func (conv *Mongo) GetHistory(sid string) ([]map[string]interface{}, error) { +func (conv *Mongo) GetHistory(sid string, cid string) ([]map[string]interface{}, error) { return []map[string]interface{}{}, nil } // SaveHistory save the history -func (conv *Mongo) SaveHistory(sid string, messages []map[string]interface{}) error { +func (conv *Mongo) SaveHistory(sid string, messages []map[string]interface{}, cid string) error { return nil } diff --git a/neo/conversation/redis.go b/neo/conversation/redis.go index 9c478fc5..b19412cc 100644 --- a/neo/conversation/redis.go +++ b/neo/conversation/redis.go @@ -8,13 +8,23 @@ func NewRedis() *Redis { return &Redis{} } +// UpdateChatTitle update the chat title +func (conv *Redis) UpdateChatTitle(sid string, cid string, title string) error { + return nil +} + +// GetChats get the chat list +func (conv *Redis) GetChats(sid string) ([]map[string]interface{}, error) { + return []map[string]interface{}{}, nil +} + // GetHistory get the history -func (conv *Redis) GetHistory(sid string) ([]map[string]interface{}, error) { +func (conv *Redis) GetHistory(sid string, cid string) ([]map[string]interface{}, error) { return []map[string]interface{}{}, nil } // SaveHistory save the history -func (conv *Redis) SaveHistory(sid string, messages []map[string]interface{}) error { +func (conv *Redis) SaveHistory(sid string, messages []map[string]interface{}, cid string) error { return nil } diff --git a/neo/conversation/types.go b/neo/conversation/types.go index 5e3575ff..68ffba17 100644 --- a/neo/conversation/types.go +++ b/neo/conversation/types.go @@ -10,8 +10,10 @@ type Setting struct { // Conversation the store interface type Conversation interface { - GetHistory(sid string) ([]map[string]interface{}, error) - SaveHistory(sid string, messages []map[string]interface{}) error + UpdateChatTitle(sid string, cid string, title string) error + GetChats(sid string) ([]map[string]interface{}, error) + GetHistory(sid string, cid string) ([]map[string]interface{}, error) + SaveHistory(sid string, messages []map[string]interface{}, cid string) error GetRequest(sid string, rid string) ([]map[string]interface{}, error) SaveRequest(sid string, rid string, cid string, messages []map[string]interface{}) error } diff --git a/neo/conversation/weaviate.go b/neo/conversation/weaviate.go index 55d3163f..7495c44d 100644 --- a/neo/conversation/weaviate.go +++ b/neo/conversation/weaviate.go @@ -8,13 +8,23 @@ func NewWeaviate() *Weaviate { return &Weaviate{} } +// UpdateChatTitle update the chat title +func (conv *Weaviate) UpdateChatTitle(sid string, cid string, title string) error { + return nil +} + +// GetChats get the chat list +func (conv *Weaviate) GetChats(sid string) ([]map[string]interface{}, error) { + return []map[string]interface{}{}, nil +} + // GetHistory get the history -func (conv *Weaviate) GetHistory(sid string) ([]map[string]interface{}, error) { +func (conv *Weaviate) GetHistory(sid string, cid string) ([]map[string]interface{}, error) { return []map[string]interface{}{}, nil } // SaveHistory save the history -func (conv *Weaviate) SaveHistory(sid string, messages []map[string]interface{}) error { +func (conv *Weaviate) SaveHistory(sid string, messages []map[string]interface{}, cid string) error { return nil } diff --git a/neo/conversation/xun.go b/neo/conversation/xun.go index 6844cc69..c34cc58b 100644 --- a/neo/conversation/xun.go +++ b/neo/conversation/xun.go @@ -20,11 +20,12 @@ type Xun struct { type row struct { Role string `json:"role"` - Name string `json:"name"` + Title string `json:"title"` // Chat title + Name string `json:"name"` // User name Content string `json:"content"` Sid string `json:"sid"` Rid string `json:"rid"` - Cid string `json:"cid"` + Cid string `json:"cid"` // Chat ID from chat history ExpiredAt interface{} `json:"expired_at"` } @@ -62,13 +63,49 @@ func NewXun(setting Setting) (*Xun, error) { return conv, nil } +// UpdateChatTitle update the chat title +func (conv *Xun) UpdateChatTitle(sid string, cid string, title string) error { + _, err := conv.query.Table(conv.setting.Table). + Where("sid", sid).Where("cid", cid). + Update(map[string]interface{}{"title": title}) + return err +} + +// GetChats get the chat list +func (conv *Xun) GetChats(sid string) ([]map[string]interface{}, error) { + qb := conv.query.Table(conv.setting.Table). + Select("cid"). + Where("sid", sid). + GroupBy("cid") + + if conv.setting.TTL > 0 { + qb.Where("expired_at", ">", time.Now()) + } + + res := []map[string]interface{}{} + + rows, err := qb.Get() + if err != nil { + return nil, err + } + + for _, row := range rows { + res = append(res, map[string]interface{}{ + "chat_id": row.Get("cid"), + "title": row.Get("cid"), + }) + } + + return res, nil +} + // GetHistory get the history -func (conv *Xun) GetHistory(sid string) ([]map[string]interface{}, error) { +func (conv *Xun) GetHistory(sid string, cid string) ([]map[string]interface{}, error) { qb := conv.query.Table(conv.setting.Table). Select("role", "name", "content"). Where("sid", sid). - Where("cid", ""). + Where("cid", cid). OrderBy("id", "desc") if conv.setting.TTL > 0 { @@ -98,7 +135,7 @@ func (conv *Xun) GetHistory(sid string) ([]map[string]interface{}, error) { } // SaveHistory save the history -func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}) error { +func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}, cid string) error { defer conv.clean() var expiredAt interface{} = nil @@ -113,6 +150,7 @@ func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}) erro Name: "", Content: message["content"].(string), Sid: sid, + Cid: cid, ExpiredAt: expiredAt, } @@ -214,12 +252,13 @@ func (conv *Xun) Init() error { if !has { err = conv.schema.CreateTable(conv.setting.Table, func(table schema.Blueprint) { - table.ID("id") // The ID field - table.String("sid", 255).Index() - table.String("rid", 255).Null().Index() // The request ID - table.String("cid", 200).Null().Index() // The Command ID - table.String("role", 200).Null().Index() - table.String("name", 200).Null().Index() + table.ID("id") // The ID field + table.String("sid", 255).Index() // The Session ID + table.String("rid", 255).Null().Index() // The request ID + table.String("cid", 200).Null().Index() // The Chat ID + table.String("role", 200).Null().Index() // The Message role + table.String("name", 200).Null().Index() // The User name + table.String("title", 200).Null().Index() // The Chat title table.Text("content").Null() table.TimestampTz("created_at").SetDefaultRaw("NOW()").Index() @@ -246,5 +285,15 @@ func (conv *Xun) Init() error { } } + // Auto update the title + if !tab.HasColumn("title") { + err = conv.schema.AlterTable(conv.setting.Table, func(table schema.Blueprint) { + table.String("title", 200).Null().Index() + }) + if err != nil { + return err + } + } + return nil } diff --git a/neo/conversation/xun_test.go b/neo/conversation/xun_test.go index a944607b..e3fc0698 100644 --- a/neo/conversation/xun_test.go +++ b/neo/conversation/xun_test.go @@ -137,14 +137,15 @@ func TestXunSaveAndGetHistory(t *testing.T) { }) // save the history + cid := "123456" err = conv.SaveHistory("123456", []map[string]interface{}{ {"role": "user", "name": "user1", "content": "hello"}, {"role": "assistant", "name": "user1", "content": "Hello there, how"}, - }) + }, cid) assert.Nil(t, err) // get the history - data, err := conv.GetHistory("123456") + data, err := conv.GetHistory("123456", cid) if err != nil { t.Fatal(err) } @@ -182,3 +183,66 @@ func TestXunSaveAndGetRequest(t *testing.T) { } assert.Equal(t, 2, len(data)) } + +func TestXunSaveAndGetHistoryWithCID(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + defer capsule.Schema().DropTableIfExists("__unit_test_conversation") + + err := capsule.Schema().DropTableIfExists("__unit_test_conversation") + if err != nil { + t.Fatal(err) + } + + conv, err := NewXun(Setting{ + Connector: "default", + Table: "__unit_test_conversation", + TTL: 3600, + }) + + // save the history with specific cid + sid := "123456" + cid := "789012" + messages := []map[string]interface{}{ + {"role": "user", "name": "user1", "content": "hello"}, + {"role": "assistant", "name": "assistant1", "content": "Hi! How can I help you?"}, + } + err = conv.SaveHistory(sid, messages, cid) + assert.Nil(t, err) + + // get the history for specific cid + data, err := conv.GetHistory(sid, cid) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 2, len(data)) + + // save another message with different cid + anotherCID := "345678" + moreMessages := []map[string]interface{}{ + {"role": "user", "name": "user1", "content": "another message"}, + } + err = conv.SaveHistory(sid, moreMessages, anotherCID) + assert.Nil(t, err) + + // get history for the first cid - should still be 2 messages + data, err = conv.GetHistory(sid, cid) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 2, len(data)) + + // get history for the second cid - should be 1 message + data, err = conv.GetHistory(sid, anotherCID) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 1, len(data)) + + // get all history for the sid without specifying cid + allData, err := conv.GetHistory(sid, cid) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 2, len(allData)) +} diff --git a/neo/load.go b/neo/load.go index 90c383ca..6773a865 100644 --- a/neo/load.go +++ b/neo/load.go @@ -4,11 +4,8 @@ import ( "path/filepath" "github.com/yaoapp/gou/application" - "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/aigc" "github.com/yaoapp/yao/config" - "github.com/yaoapp/yao/neo/command" - "github.com/yaoapp/yao/neo/command/driver" "github.com/yaoapp/yao/neo/conversation" ) @@ -23,7 +20,6 @@ func Load(cfg config.Config) error { Prompts: []aigc.Prompt{}, Option: map[string]interface{}{}, Allows: []string{}, - Command: Command{Parser: ""}, ConversationSetting: conversation.Setting{ Table: "yao_neo_conversation", Connector: "default", @@ -58,23 +54,5 @@ func Load(cfg config.Config) error { return err } - // Command Setting - parser := setting.Command.Parser - if parser == "" || parser == "default" { - parser = setting.Connector - } - - store, err := driver.NewMemory(parser, nil) - if err != nil { - return err - } - command.SetStore(store) - - // Load the commands - err = command.Load(cfg) - if err != nil { - log.Error("Command Load Error: %s", err.Error()) - } - return nil } diff --git a/neo/neo.go b/neo/neo.go index 6de9b335..fecbff94 100644 --- a/neo/neo.go +++ b/neo/neo.go @@ -13,8 +13,6 @@ import ( "github.com/yaoapp/gou/process" "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/helper" - "github.com/yaoapp/yao/neo/command" - "github.com/yaoapp/yao/neo/command/query" "github.com/yaoapp/yao/neo/conversation" "github.com/yaoapp/yao/neo/message" "github.com/yaoapp/yao/openai" @@ -53,7 +51,7 @@ func (neo *DSL) API(router *gin.Engine, path string) error { } // set the context - ctx, cancel := command.NewContextWithCancel(sid, c.Query("context")) + ctx, cancel := NewContextWithCancel(sid, c.Query("chat_id"), c.Query("context")) defer cancel() err = neo.Answer(ctx, content, c) @@ -64,6 +62,28 @@ func (neo *DSL) API(router *gin.Engine, path string) error { }) router.GET(path, handlers...) + router.POST(path, handlers...) + + // api Get ChatList + handlers = append(middlewares, func(c *gin.Context) { + sid := c.GetString("__sid") + if sid == "" { + c.JSON(400, gin.H{"message": "sid is required", "code": 400}) + c.Done() + return + } + + list, err := neo.Conversation.GetChats(sid) + if err != nil { + c.JSON(500, gin.H{"message": err.Error(), "code": 500}) + c.Done() + return + } + + c.JSON(200, map[string]interface{}{"data": list}) + c.Done() + }) + router.GET(path+"/chats", handlers...) // api router chat history handlers = append(middlewares, func(c *gin.Context) { @@ -74,104 +94,24 @@ func (neo *DSL) API(router *gin.Engine, path string) error { return } - history, err := neo.Conversation.GetHistory(sid) + cid := c.Query("chat_id") + history, err := neo.Conversation.GetHistory(sid, cid) if err != nil { c.JSON(500, gin.H{"message": err.Error(), "code": 500}) c.Done() return } - c.JSON(200, map[string]interface{}{ - "data": history, - "command": nil, - }) + c.JSON(200, map[string]interface{}{"data": history}) c.Done() }) router.GET(path+"/history", handlers...) - // api router chat commands - handlers = append(middlewares, func(c *gin.Context) { - commands, err := command.GetCommands() - if err != nil { - c.JSON(500, gin.H{"message": err.Error(), "code": 500}) - c.Done() - return - } - - c.JSON(200, commands) - c.Done() - }) - router.GET(path+"/commands", handlers...) - - // api router exit command mode - handlers = append(middlewares, func(c *gin.Context) { - sid := c.GetString("__sid") - if sid == "" { - c.JSON(400, gin.H{"message": "sid is required", "code": 400}) - c.Done() - return - } - - var payload map[string]interface{} - err := c.ShouldBindJSON(&payload) - if err != nil { - c.JSON(400, gin.H{"message": err.Error(), "code": 400}) - c.Done() - return - } - - cmd, ok := payload["cmd"].(string) - if !ok { - c.JSON(400, gin.H{"message": "command is required", "code": 400}) - c.Done() - return - } - - switch cmd { - - case "ModelList": - c.JSON(200, gin.H{"data": neo.Models, "code": 200}) - c.Done() - - case "SelectModel": - model, ok := payload["model"].(string) - if !ok { - c.JSON(400, gin.H{"message": "model is required", "code": 400}) - c.Done() - return - } - - err := neo.Select(model) - if err != nil { - c.JSON(500, gin.H{"message": err.Error(), "code": 500}) - c.Done() - return - } - - c.JSON(200, gin.H{"message": "success", "code": 200}) - c.Done() - - case "ExitCommandMode": - err := command.Exit(sid) - if err != nil { - c.JSON(500, gin.H{"message": err.Error(), "code": 500}) - c.Done() - return - } - c.JSON(200, gin.H{"message": "success", "code": 200}) - c.Done() - - default: - c.JSON(400, gin.H{"message": "command is not supported", "code": 400}) - } - }) - router.POST(path, handlers...) - return nil } // Answer reply the message -func (neo *DSL) Answer(ctx command.Context, question string, c *gin.Context) error { +func (neo *DSL) Answer(ctx Context, question string, c *gin.Context) error { // get the chat messages messages, err := neo.chatMessages(ctx, question) if err != nil { @@ -190,39 +130,6 @@ func (neo *DSL) Answer(ctx command.Context, question string, c *gin.Context) err c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") - // check the command - cmd, isCommand := neo.matchCommand(ctx, messages) - if isCommand { - // execute the command - req, err := cmd.NewRequest(ctx, neo.Conversation) - if err != nil { - log.Error("Command with AI error: %s", err.Error()) - done <- true - return - } - - err = req.Run(messages, func(msg *message.JSON) int { - err := neo.send(ctx, msg, messages, content, c) - if err != nil { - c.Status(500) - return 0 // break - } - - // Complete the stream - if msg.IsDone() { - return 0 // break - } - return 1 - }) - - if err != nil { - c.Status(500) - log.Error("Command with AI error: %s", err.Error()) - } - - return - } - _, ex := neo.AI.ChatCompletionsWith(ctx, messages, neo.Option, func(data []byte) int { select { @@ -266,7 +173,7 @@ func (neo *DSL) Answer(ctx command.Context, question string, c *gin.Context) err } // save the history - neo.saveHistory(ctx.Sid, content, messages) + neo.saveHistory(ctx.Sid, ctx.ChatID, content, messages) c.Status(200) // Complete the stream @@ -285,7 +192,7 @@ func (neo *DSL) Answer(ctx command.Context, question string, c *gin.Context) err } // Send send the message to the stream -func (neo *DSL) send(ctx command.Context, msg *message.JSON, messages []map[string]interface{}, content []byte, c *gin.Context) error { +func (neo *DSL) send(ctx Context, msg *message.JSON, messages []map[string]interface{}, content []byte, c *gin.Context) error { w := c.Writer @@ -356,7 +263,7 @@ func (neo *DSL) prompts() []map[string]interface{} { } // prepare the messages -func (neo *DSL) prepare(ctx command.Context, messages []map[string]interface{}) []map[string]interface{} { +func (neo *DSL) prepare(ctx Context, messages []map[string]interface{}) []map[string]interface{} { if neo.Prepare == "" { return []map[string]interface{}{} } @@ -405,9 +312,9 @@ func (neo *DSL) prepare(ctx command.Context, messages []map[string]interface{}) } // chatMessages get the chat messages -func (neo *DSL) chatMessages(ctx command.Context, content string) ([]map[string]interface{}, error) { +func (neo *DSL) chatMessages(ctx Context, content string) ([]map[string]interface{}, error) { - history, err := neo.Conversation.GetHistory(ctx.Sid) + history, err := neo.Conversation.GetHistory(ctx.Sid, ctx.ChatID) if err != nil { return nil, err } @@ -424,28 +331,8 @@ func (neo *DSL) chatMessages(ctx command.Context, content string) ([]map[string] return messages, nil } -// matchCommand match the command -func (neo *DSL) matchCommand(ctx command.Context, messages []map[string]interface{}) (*command.Command, bool) { - if len(messages) < 1 { - return nil, false - } - - input, ok := messages[len(messages)-1]["content"].(string) - if !ok { - return nil, false - } - - id, err := command.Match(ctx.Sid, query.Param{Stack: ctx.Stack, Path: ctx.Path}, input) - if err == nil && id != "" { - cmd, isCommand := command.Commands[id] - return cmd, isCommand - } - - return nil, false -} - // saveHistory save the history -func (neo *DSL) saveHistory(sid string, content []byte, messages []map[string]interface{}) { +func (neo *DSL) saveHistory(sid string, chatID string, content []byte, messages []map[string]interface{}) { if len(content) > 0 && sid != "" && len(messages) > 0 { err := neo.Conversation.SaveHistory( @@ -454,6 +341,7 @@ func (neo *DSL) saveHistory(sid string, content []byte, messages []map[string]in {"role": "user", "content": messages[len(messages)-1]["content"], "name": sid}, {"role": "assistant", "content": string(content), "name": sid}, }, + chatID, ) if err != nil { diff --git a/neo/neo_test.go b/neo/neo_test.go index d992e539..2292fc23 100644 --- a/neo/neo_test.go +++ b/neo/neo_test.go @@ -15,7 +15,6 @@ import ( httpTest "github.com/yaoapp/gou/http" "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/helper" - "github.com/yaoapp/yao/neo/command" "github.com/yaoapp/yao/test" _ "github.com/yaoapp/yao/utils" ) @@ -50,7 +49,8 @@ func TestAPI(t *testing.T) { return 1 }) - assert.Contains(t, string(res), `{"done":true}`) + assert.Contains(t, string(res), `{`) + } func TestAPIAuth(t *testing.T) { @@ -111,12 +111,6 @@ func testRouter(t *testing.T) *gin.Engine { t.Fatal(err) } - // Load Commands - err = command.Load(config.Conf) - // if err != nil { - // t.Fatal(err) - // } - router := gin.New() gin.SetMode(gin.ReleaseMode) return router diff --git a/neo/types.go b/neo/types.go index f6846caa..32a9d011 100644 --- a/neo/types.go +++ b/neo/types.go @@ -1,6 +1,7 @@ package neo import ( + "context" "io" "github.com/gin-gonic/gin" @@ -21,7 +22,6 @@ type DSL struct { Write string `json:"write,omitempty"` Prompts []aigc.Prompt `json:"prompts,omitempty"` Allows []string `json:"allows,omitempty"` - Command Command `json:"command,omitempty"` Models []string `json:"models,omitempty"` AI aigc.AI `json:"-" yaml:"-"` Conversation conversation.Conversation `json:"-" yaml:"-"` @@ -35,7 +35,22 @@ type Answer interface { Header(key, value string) } -// Command setting -type Command struct { - Parser string `json:"parser,omitempty"` +// Context the context +type Context struct { + Sid string `json:"sid" yaml:"-"` + ChatID string `json:"chat_id,omitempty"` + Stack string `json:"stack,omitempty"` + Path string `json:"pathname,omitempty"` + FormData map[string]interface{} `json:"formdata,omitempty"` + Field *ContextField `json:"field,omitempty"` + Namespace string `json:"namespace,omitempty"` + Config map[string]interface{} `json:"config,omitempty"` + Signal interface{} `json:"signal,omitempty"` + context.Context `json:"-" yaml:"-"` +} + +// ContextField the context field +type ContextField struct { + Name string `json:"name,omitempty"` + Bind string `json:"bind,omitempty"` }