diff --git a/aigc/load.go b/aigc/load.go index 0d0385aa..c650a164 100644 --- a/aigc/load.go +++ b/aigc/load.go @@ -2,6 +2,7 @@ package aigc import ( "fmt" + "strings" "github.com/yaoapp/gou/application" "github.com/yaoapp/yao/config" @@ -11,15 +12,30 @@ import ( // Load load AIGC func Load(cfg config.Config) error { exts := []string{"*.ai.yml", "*.ai.yaml"} - return application.App.Walk("aigcs", func(root, file string, isdir bool) error { + messages := []string{} + err := application.App.Walk("aigcs", func(root, file string, isdir bool) error { if isdir { return nil } id := share.ID(root, file) _, err := LoadFile(file, id) - return err + 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 diff --git a/connector/connector.go b/connector/connector.go index d9ad09c1..28af940a 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -1,6 +1,9 @@ package connector import ( + "fmt" + "strings" + "github.com/yaoapp/gou/application" "github.com/yaoapp/gou/connector" "github.com/yaoapp/yao/config" @@ -10,11 +13,24 @@ import ( // Load load store func Load(cfg config.Config) error { exts := []string{"*.yao", "*.json", "*.jsonc"} - return application.App.Walk("connectors", func(root, file string, isdir bool) error { + messages := []string{} + err := application.App.Walk("connectors", func(root, file string, isdir bool) error { if isdir { return nil } _, err := connector.Load(file, share.ID(root, file)) - return err + 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 } diff --git a/neo/command/command.go b/neo/command/command.go new file mode 100644 index 00000000..b9149174 --- /dev/null +++ b/neo/command/command.go @@ -0,0 +1,61 @@ +package command + +import ( + "fmt" + + "github.com/yaoapp/gou/connector" + "github.com/yaoapp/yao/aigc" + "github.com/yaoapp/yao/neo/command/driver" + "github.com/yaoapp/yao/openai" +) + +// DefaultStore the default store driver +var DefaultStore Store + +// SetStore the driver interface +func SetStore(store Store) { + DefaultStore = 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, + }) + } + + return DefaultStore.Set(cmd.ID, driver.Command{ + ID: cmd.ID, + Description: cmd.Description, + Args: args, + Stack: cmd.Stack, + Path: cmd.Path, + }) +} + +// NewAI create a new AI +func (cmd *Command) newAI() (aigc.AI, error) { + + if cmd.Connector == "" { + return nil, fmt.Errorf("%s connector is required", cmd.ID) + } + + 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/context.go b/neo/command/context.go new file mode 100644 index 00000000..5496dd1a --- /dev/null +++ b/neo/command/context.go @@ -0,0 +1,49 @@ +package command + +import ( + "context" + "time" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/kun/log" +) + +// NewContext create a new context +func NewContext(sid, payload string) Context { + ctx := Context{Context: context.Background(), Sid: sid} + if payload == "" { + return ctx + } + + err := jsoniter.Unmarshal([]byte(payload), &ctx) + if err != nil { + log.Error("%s", err.Error()) + } + return ctx +} + +// NewContextWithCancel create a new context with cancel +func NewContextWithCancel(sid, payload string) (Context, context.CancelFunc) { + ctx := NewContext(sid, 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) + return ContextWithTimeout(ctx, timeout) +} + +// ContextWithCancel create a new context +func ContextWithCancel(parent Context) (Context, context.CancelFunc) { + new, cancel := context.WithCancel(parent.Context) + parent.Context = new + return parent, cancel +} + +// ContextWithTimeout create a new context +func ContextWithTimeout(parent Context, timeout time.Duration) (Context, context.CancelFunc) { + new, cancel := context.WithTimeout(parent.Context, timeout) + parent.Context = new + return parent, cancel +} diff --git a/neo/command/driver/memory.go b/neo/command/driver/memory.go new file mode 100644 index 00000000..cb0741e2 --- /dev/null +++ b/neo/command/driver/memory.go @@ -0,0 +1,191 @@ +package driver + +import ( + "fmt" + "sync" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou/connector" + "github.com/yaoapp/yao/aigc" + "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, content string) (string, error) { + 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, + "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, + }) + + prompts = append([]aigc.Prompt{}, driver.prompts...) + 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(id string, cmd Command) error { + commands.Store(id, cmd) + return nil +} + +// Del delete the command data +func (driver *Memory) Del(id string) { + commands.Delete(id) +} + +// Get the command data +func (driver *Memory) Get(id string) (Command, bool) { + v, ok := commands.Load(id) + 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) +} + +// NewAI create a new AI +func (driver *Memory) newAI() (aigc.AI, error) { + + if driver.model == "" { + return nil, fmt.Errorf("%s connector is required", driver.model) + } + + 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 new file mode 100644 index 00000000..b1bd3f2a --- /dev/null +++ b/neo/command/driver/memory_test.go @@ -0,0 +1,88 @@ +package driver + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/yao/config" + "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{}, "Generate table test data") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "table.data", id) + + id, err = mem.Match(Query{Stack: "Form"}, "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: "*", + 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/query.go b/neo/command/driver/query.go new file mode 100644 index 00000000..19817a6f --- /dev/null +++ b/neo/command/driver/query.go @@ -0,0 +1,48 @@ +package driver + +import ( + "regexp" + "strings" +) + +// MatchStack match the stack +func (query Query) MatchStack(stack string) bool { + + if query.Stack == "" || query.Stack == "*" || stack == "" { + return true + } + + if query.Stack == stack { + return true + } + + matched, _ := regexp.MatchString(strings.ReplaceAll(query.Stack, "*", ".*"), stack) + return matched +} + +// MatchPath match the path +func (query Query) MatchPath(path string) bool { + if query.Path == "" || query.Path == "*" || path == "" { + return true + } + + if query.Path == path { + return true + } + + matched, _ := regexp.MatchString(strings.ReplaceAll(query.Path, "*", ".*"), path) + return matched +} + +// MatchAny match the stack or path +func (query Query) MatchAny(stack, path string) bool { + if query.Path == "" || query.Path == "-" { + return query.MatchStack(stack) + } + + if query.Stack == "" || query.Stack == "-" { + return query.MatchPath(path) + } + + return query.MatchStack(stack) || query.MatchPath(path) +} diff --git a/neo/command/driver/redis.go b/neo/command/driver/redis.go new file mode 100644 index 00000000..bce7c468 --- /dev/null +++ b/neo/command/driver/redis.go @@ -0,0 +1 @@ +package driver diff --git a/neo/command/driver/types.go b/neo/command/driver/types.go new file mode 100644 index 00000000..af343430 --- /dev/null +++ b/neo/command/driver/types.go @@ -0,0 +1,24 @@ +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:"-"` + 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"` +} + +// Query the query struct +type Query struct { + Stack string `json:"stack,omitempty"` + Path string `json:"path,omitempty"` +} diff --git a/neo/command/driver/weaviate.go b/neo/command/driver/weaviate.go new file mode 100644 index 00000000..bce7c468 --- /dev/null +++ b/neo/command/driver/weaviate.go @@ -0,0 +1 @@ +package driver diff --git a/neo/command/load.go b/neo/command/load.go new file mode 100644 index 00000000..c8bf9ace --- /dev/null +++ b/neo/command/load.go @@ -0,0 +1,106 @@ +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 new file mode 100644 index 00000000..4b012217 --- /dev/null +++ b/neo/command/load_test.go @@ -0,0 +1,43 @@ +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/request.go b/neo/command/request.go new file mode 100644 index 00000000..9647f1af --- /dev/null +++ b/neo/command/request.go @@ -0,0 +1,84 @@ +package command + +import ( + "context" + "fmt" + "sync" + + "github.com/google/uuid" + "github.com/yaoapp/kun/exception" +) + +var requests = sync.Map{} + +// Run the command +func (req *Request) Run(cb func(data []byte) int) (interface{}, error) { + return nil, nil +} + +// NewRequest create a new request +func (cmd *Command) NewRequest(ctx Context, messages []map[string]interface{}) (*Request, error) { + + v, ok := requests.Load(ctx.Sid) + if !ok { + v = map[string]string{ + "id": uuid.New().String(), + "cmd": cmd.ID, + } + } + + req, ok := v.(map[string]string) + if !ok { + return nil, fmt.Errorf("request id is not string") + } + + if req["id"] == "" { + return nil, fmt.Errorf("request id is request") + } + + if req["cmd"] != cmd.ID { + defer requests.Delete(ctx.Sid) + return nil, fmt.Errorf("request id is not match") + } + + return &Request{ + Command: cmd, + messages: messages, + sid: ctx.Sid, + id: req["id"], + ctx: ctx, + }, nil +} + +// Done the request done +func (req *Request) Done() { + requests.Delete(req.sid) +} + +// prepare the command +func (req *Request) prepare(ctx context.Context, data []map[string]interface{}, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) (int, *exception.Exception) { + return 1, nil +} + +// before the process +func (req *Request) before(ctx context.Context, data []map[string]interface{}, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) (interface{}, *exception.Exception) { + return nil, nil +} + +// after the process +func (req *Request) after(ctx context.Context, data []map[string]interface{}, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) (interface{}, *exception.Exception) { + return nil, nil +} + +// run the process +func (req *Request) process(ctx context.Context, data []map[string]interface{}, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) (interface{}, *exception.Exception) { + return nil, nil +} + +func (req *Request) saveConversation(ctx context.Context, data []map[string]interface{}, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) (interface{}, *exception.Exception) { + return nil, nil +} + +func (req *Request) saveData(ctx context.Context, data []map[string]interface{}, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) (interface{}, *exception.Exception) { + return nil, nil +} diff --git a/neo/command/types.go b/neo/command/types.go new file mode 100644 index 00000000..1a9cfcbe --- /dev/null +++ b/neo/command/types.go @@ -0,0 +1,82 @@ +package command + +import ( + "context" + + "github.com/yaoapp/yao/aigc" + "github.com/yaoapp/yao/neo/command/driver" +) + +// Request the command request +type Request struct { + id string + sid string + ctx Context + messages []map[string]interface{} + *Command +} + +// Command the command struct +type Command struct { + ID string `json:"-" yaml:"-"` + Name string `json:"name,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"` + 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:"-" yaml:"-"` + Stack string `json:"stack,omitempty"` + Path string `json:"path,omitempty"` + context.Context `json:"-" yaml:"-"` +} + +// Store the command driver +type Store interface { + Match(query driver.Query, content string) (string, error) + Set(id string, cmd driver.Command) error + Get(id string) (driver.Command, bool) + Del(id string) + SetRequest(sid, id, cid string) error + GetRequest(sid string) (string, string, bool) + DelRequest(sid string) +} diff --git a/neo/neo.go b/neo/neo.go index 70a390d6..8b34caca 100644 --- a/neo/neo.go +++ b/neo/neo.go @@ -1,10 +1,8 @@ package neo import ( - "context" "fmt" "io" - "net/http" "net/url" "strings" @@ -16,6 +14,7 @@ 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/conversation" "github.com/yaoapp/yao/openai" ) @@ -66,8 +65,8 @@ func (neo *DSL) API(router *gin.Engine, path string) error { messages = append(messages, map[string]interface{}{"role": "user", "content": content, "name": sid}) // utils.Dump(messages) - // reply the content - ctx, cancel := context.WithCancel(context.Background()) + // set the context + ctx, cancel := command.NewContextWithCancel(sid, c.GetString("context")) defer cancel() err = neo.Answer(ctx, c, messages) @@ -82,17 +81,43 @@ func (neo *DSL) API(router *gin.Engine, path string) error { } // Answer the message -func (neo *DSL) Answer(ctx context.Context, c *gin.Context, messages []map[string]interface{}) error { +func (neo *DSL) Answer(ctx command.Context, answer Answer, messages []map[string]interface{}) error { chanStream := make(chan []byte, 1) chanError := make(chan error, 1) + // check the command + // cmd, isCommand := neo.Command.Match(ctx, messages) + isCommand := false + cmd := command.Command{} + go func() { defer func() { close(chanStream) close(chanError) }() + // execute the command + if isCommand { + + req, err := cmd.NewRequest(ctx, messages) + if err != nil { + chanError <- err + return + } + + _, err = req.Run(func(data []byte) int { + chanStream <- data + return 1 + }) + + if err != nil { + chanError <- err + } + return + } + + // chat with AI _, ex := neo.AI.ChatCompletionsWith(ctx, messages, neo.Option, func(data []byte) int { chanStream <- data return 1 @@ -106,7 +131,7 @@ func (neo *DSL) Answer(ctx context.Context, c *gin.Context, messages []map[strin // save the history content := []byte{} defer func() { - sid := c.GetString("__sid") + sid := answer.GetString("__sid") if len(content) > 0 && sid != "" && len(messages) > 0 { err := neo.Conversation.SaveHistory( sid, @@ -122,13 +147,14 @@ func (neo *DSL) Answer(ctx context.Context, c *gin.Context, messages []map[strin } }() - c.Header("Content-Type", "text/event-stream;charset=utf-8") - ok := c.Stream(func(w io.Writer) bool { + answer.Header("Content-Type", "text/event-stream;charset=utf-8") + ok := answer.Stream(func(w io.Writer) bool { select { case err := <-chanError: if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error(), "code": 500}) + w.Write([]byte(fmt.Sprintf(`data: {"text":"%s"}%s`, err.Error(), "\n\n"))) } + w.Write([]byte(fmt.Sprintf("data: %s\n\n", `{"done":true}`))) return false case msg := <-chanStream: @@ -163,11 +189,11 @@ func (neo *DSL) Answer(ctx context.Context, c *gin.Context, messages []map[strin }) if !ok { - c.Status(500) + answer.Status(500) return nil } - c.Status(200) + answer.Status(200) return nil } diff --git a/neo/types.go b/neo/types.go index f67e6a6a..c5ddaf20 100644 --- a/neo/types.go +++ b/neo/types.go @@ -1,7 +1,10 @@ package neo import ( + "io" + "github.com/yaoapp/yao/aigc" + "github.com/yaoapp/yao/neo/command" "github.com/yaoapp/yao/neo/conversation" ) @@ -28,5 +31,13 @@ type Conversation interface { // Command the command interface type Command interface { - Match(messages []map[string]interface{}) (bool, error) + Match(ctx command.Context, messages []map[string]interface{}) (*command.Command, bool) +} + +// Answer the answer interface +type Answer interface { + GetString(key string) (s string) + Stream(func(w io.Writer) bool) bool + Status(code int) + Header(key, value string) }