Skip to content

Commit 338f570

Browse files
committed
arch: init llm plugin
1. init project 2. enable plugin arch
1 parent 38bf16b commit 338f570

File tree

14 files changed

+552
-2
lines changed

14 files changed

+552
-2
lines changed

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
1-
# llmplugin
2-
plugin for LLM
1+
# LLM Plugin
2+
3+
LLM Plugin system.
4+
5+
6+
## TESTING
7+
8+
```bash
9+
go test -v ./...
10+
```

go.mod

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module github.com/agi-cn/llmplugin
2+
3+
go 1.20
4+
5+
require (
6+
github.com/pkg/errors v0.9.1
7+
github.com/sashabaranov/go-openai v1.9.0
8+
github.com/sirupsen/logrus v1.9.0
9+
github.com/stretchr/testify v1.8.2
10+
)
11+
12+
require (
13+
github.com/davecgh/go-spew v1.1.1 // indirect
14+
github.com/pmezard/go-difflib v1.0.0 // indirect
15+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
16+
gopkg.in/yaml.v3 v3.0.1 // indirect
17+
)

go.sum

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
5+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
6+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
7+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8+
github.com/sashabaranov/go-openai v1.9.0 h1:NoiO++IISxxJ1pRc0n7uZvMGMake0G+FJ1XPwXtprsA=
9+
github.com/sashabaranov/go-openai v1.9.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
10+
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
11+
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
12+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
13+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
14+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
15+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
16+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
17+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
18+
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
19+
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
20+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
21+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
22+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
23+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
24+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
25+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
26+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

init.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package llmplugin
2+
3+
import "github.com/sirupsen/logrus"
4+
5+
func init() {
6+
7+
logrus.SetLevel(logrus.DebugLevel)
8+
}

llm/llm.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package llm
2+
3+
import (
4+
"context"
5+
6+
"github.com/sashabaranov/go-openai"
7+
)
8+
9+
type Role string
10+
11+
const (
12+
RoleUser Role = openai.ChatMessageRoleUser
13+
RoleAssistant Role = openai.ChatMessageRoleAssistant
14+
RoleSystem Role = openai.ChatMessageRoleSystem
15+
)
16+
17+
func (r Role) String() string {
18+
return string(r)
19+
}
20+
21+
type LlmMessage struct {
22+
Role Role
23+
Content string
24+
}
25+
26+
type LlmAnswer struct {
27+
Role string
28+
Content string
29+
}
30+
31+
type LLMer interface {
32+
Chat(ctx context.Context, messages []LlmMessage) (*LlmAnswer, error)
33+
}

llm/openai/chatgpt.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package openai
2+
3+
import (
4+
"context"
5+
6+
"github.com/agi-cn/llmplugin/llm"
7+
8+
"github.com/pkg/errors"
9+
"github.com/sashabaranov/go-openai"
10+
)
11+
12+
type ChatGPT struct {
13+
model string
14+
client *openai.Client
15+
}
16+
17+
type Option func(c *ChatGPT)
18+
19+
func WithModel(model string) Option {
20+
return func(c *ChatGPT) {
21+
c.model = model
22+
}
23+
}
24+
25+
func NewChatGPT(token string, opts ...Option) *ChatGPT {
26+
27+
client := openai.NewClient(token)
28+
29+
chatgpt := &ChatGPT{
30+
model: openai.GPT3Dot5Turbo,
31+
client: client,
32+
}
33+
34+
for _, opt := range opts {
35+
opt(chatgpt)
36+
}
37+
38+
return chatgpt
39+
}
40+
41+
func (c ChatGPT) Chat(ctx context.Context, messages []llm.LlmMessage) (*llm.LlmAnswer, error) {
42+
43+
chatGPTMessages := c.makeChatGPTMessage(messages)
44+
45+
return c.send(ctx, chatGPTMessages)
46+
47+
}
48+
49+
func (c ChatGPT) makeChatGPTMessage(messages []llm.LlmMessage) []openai.ChatCompletionMessage {
50+
51+
chatGPTMessages := make([]openai.ChatCompletionMessage, 0, len(messages))
52+
for _, m := range messages {
53+
chatGPTMessages = append(chatGPTMessages, openai.ChatCompletionMessage{
54+
Role: m.Role.String(),
55+
Content: m.Content,
56+
})
57+
}
58+
59+
return chatGPTMessages
60+
}
61+
62+
func (c ChatGPT) send(ctx context.Context, messages []openai.ChatCompletionMessage) (*llm.LlmAnswer, error) {
63+
64+
resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
65+
Model: c.model,
66+
Messages: messages,
67+
})
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
if choices := resp.Choices; len(choices) == 0 {
73+
return nil, errors.New("got empty ChatGPT response")
74+
}
75+
76+
answer := c.convertLlmAnswer(resp)
77+
return answer, nil
78+
}
79+
80+
func (c ChatGPT) convertLlmAnswer(openaiResp openai.ChatCompletionResponse) *llm.LlmAnswer {
81+
82+
choices := openaiResp.Choices[0]
83+
84+
return &llm.LlmAnswer{
85+
Role: choices.Message.Role,
86+
Content: choices.Message.Content,
87+
}
88+
}

plugin.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package llmplugin
2+
3+
import "context"
4+
5+
type Plugin interface {
6+
Do(ctx context.Context, query string) (answer string, err error)
7+
8+
GetName() string
9+
10+
GetDesc() string
11+
}
12+
13+
var _ Plugin = (*SimplePlugin)(nil)
14+
15+
type SimplePlugin struct {
16+
Name string
17+
Desc string
18+
DoFunc func(ctx context.Context, query string) (answer string, err error)
19+
}
20+
21+
func (p SimplePlugin) GetName() string {
22+
return p.Name
23+
}
24+
25+
func (p SimplePlugin) GetDesc() string {
26+
return p.Desc
27+
}
28+
29+
func (p SimplePlugin) Do(ctx context.Context, query string) (answer string, err error) {
30+
return p.DoFunc(ctx, query)
31+
}

plugin_manager.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package llmplugin
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/agi-cn/llmplugin/llm"
9+
10+
"github.com/sirupsen/logrus"
11+
)
12+
13+
type PluginManager struct {
14+
llmer llm.LLMer
15+
16+
// plugins <key:name, value:Plugin>
17+
plugins map[string]Plugin
18+
}
19+
20+
type PluginManagerOpt func(manager *PluginManager)
21+
22+
// WithPlugin enable one plugin.
23+
func WithPlugin(p Plugin) PluginManagerOpt {
24+
25+
return func(manager *PluginManager) {
26+
name := strings.ToLower(p.GetName())
27+
if _, ok := manager.plugins[name]; !ok {
28+
manager.plugins[name] = p
29+
}
30+
}
31+
}
32+
33+
// WithPlugins enable multiple plugins.
34+
func WithPlugins(plugins []Plugin) PluginManagerOpt {
35+
36+
return func(manager *PluginManager) {
37+
38+
for _, p := range plugins {
39+
opt := WithPlugin(p)
40+
opt(manager)
41+
}
42+
}
43+
}
44+
45+
// NewPluginManager create plugin manager.
46+
func NewPluginManager(llmer llm.LLMer, opts ...PluginManagerOpt) *PluginManager {
47+
48+
manager := &PluginManager{
49+
llmer: llmer,
50+
plugins: make(map[string]Plugin, 4),
51+
}
52+
53+
for _, opt := range opts {
54+
opt(manager)
55+
}
56+
57+
return manager
58+
}
59+
60+
// Select to choice some plugin to finish the task.
61+
func (m *PluginManager) Select(ctx context.Context, query string) ([]Plugin, error) {
62+
63+
prompt := m.makePrompt(query)
64+
65+
answer, err := m.chatWithLlm(ctx, prompt)
66+
if err != nil {
67+
logrus.Errorf("chat with llm error: %v", err)
68+
return nil, err
69+
}
70+
71+
plugins := m.choicePlugins(answer)
72+
return plugins, nil
73+
}
74+
75+
func (m *PluginManager) makePrompt(query string) string {
76+
77+
prompt := fmt.Sprintf(`You are an helpful and kind assistant to answer questions that can use tools to interact with real world and get access to the latest information.
78+
You will performs one task based on the following object:
79+
%s
80+
81+
You can call one of the following functions:
82+
83+
- Calculator, INPUT: (expr string): ACT ON A calculator, capable of performing mathematical calculations, where the input is a description of a mathematical expression and the return is the result of the calculation. For example: the input is: one plus two, the return is three.
84+
- Weather, INPUT: no input: ACT ON You can check the weather forecast.
85+
86+
In each response, you must start with a function call like Tool name and args, split by space,like:
87+
Google "query"
88+
Weather
89+
90+
Don't explain why you use a tool. If you cannot figure out the answer, you say ’I don’t know’.
91+
92+
You can choose one tool or multiple tools to accomplish the corresponding goal.
93+
Select only the corresponding tool and do not return any results.`,
94+
query,
95+
)
96+
97+
return prompt
98+
}
99+
100+
func (m *PluginManager) chatWithLlm(ctx context.Context, query string) (string, error) {
101+
messages := []llm.LlmMessage{
102+
{
103+
Role: llm.RoleUser,
104+
Content: query,
105+
},
106+
}
107+
108+
answer, err := m.llmer.Chat(ctx, messages)
109+
if err != nil {
110+
return "", err
111+
}
112+
113+
logrus.Debugf("query: %s\n answer: %+v", query, answer)
114+
115+
return answer.Content, nil
116+
}
117+
118+
func (m *PluginManager) choicePlugins(answer string) []Plugin {
119+
120+
lines := strings.Split(answer, "\n")
121+
122+
plugins := make([]Plugin, 0, len(lines))
123+
124+
for _, line := range lines {
125+
126+
// Split by space
127+
// IF only ONE column, it's function name without args.
128+
// IF TWO column, it's function name with args.
129+
130+
ss := strings.Split(line, " ")
131+
if len(ss) == 0 {
132+
continue
133+
}
134+
135+
n := strings.ToLower(ss[0])
136+
137+
if p, ok := m.plugins[n]; ok {
138+
plugins = append(plugins, p)
139+
}
140+
}
141+
142+
return plugins
143+
}

0 commit comments

Comments
 (0)