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.

This commit is contained in:
Max 2024-12-12 16:37:59 +08:00
parent 319faff110
commit 153efc1a49
24 changed files with 227 additions and 1543 deletions

View file

@ -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)
}

View file

@ -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 <no related command found>. <No relevant command found>, 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)
}

View file

@ -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
}

View file

@ -1 +0,0 @@
package driver

View file

@ -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"`
}

View file

@ -1 +0,0 @@
package driver

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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))
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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

View file

@ -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"`
}