[add] neo commands ( 50% )
This commit is contained in:
parent
04ca2aebdc
commit
1f51bdaf8f
16 changed files with 863 additions and 16 deletions
20
aigc/load.go
20
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
61
neo/command/command.go
Normal file
61
neo/command/command.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
49
neo/command/context.go
Normal file
49
neo/command/context.go
Normal file
|
|
@ -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
|
||||
}
|
||||
191
neo/command/driver/memory.go
Normal file
191
neo/command/driver/memory.go
Normal file
|
|
@ -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 <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, 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)
|
||||
}
|
||||
88
neo/command/driver/memory_test.go
Normal file
88
neo/command/driver/memory_test.go
Normal file
|
|
@ -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
|
||||
}
|
||||
48
neo/command/driver/query.go
Normal file
48
neo/command/driver/query.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
1
neo/command/driver/redis.go
Normal file
1
neo/command/driver/redis.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package driver
|
||||
24
neo/command/driver/types.go
Normal file
24
neo/command/driver/types.go
Normal file
|
|
@ -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"`
|
||||
}
|
||||
1
neo/command/driver/weaviate.go
Normal file
1
neo/command/driver/weaviate.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package driver
|
||||
106
neo/command/load.go
Normal file
106
neo/command/load.go
Normal file
|
|
@ -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
|
||||
}
|
||||
43
neo/command/load_test.go
Normal file
43
neo/command/load_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
84
neo/command/request.go
Normal file
84
neo/command/request.go
Normal file
|
|
@ -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
|
||||
}
|
||||
82
neo/command/types.go
Normal file
82
neo/command/types.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
48
neo/neo.go
48
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
|
||||
}
|
||||
|
||||
|
|
|
|||
13
neo/types.go
13
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue