Skip to content

Commit d2c4ec4

Browse files
committed
chore: migrate code from forked test-infra repo
Signed-off-by: wuhuizuo <wuhuizuo@126.com>
1 parent 8e8a5cf commit d2c4ec4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+3707
-288
lines changed

.ko.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,13 @@ builds:
285285
- -s -w
286286
- -X sigs.k8s.io/prow/pkg/version.Version={{.Env.VERSION}}
287287
- -X sigs.k8s.io/prow/pkg/version.Name=cherrypicker
288+
- id: chatgpt
289+
dir: .
290+
main: cmd/external-plugins/chatgpt
291+
ldflags:
292+
- -s -w
293+
- -X sigs.k8s.io/prow/version.Version={{.Env.VERSION}}
294+
- -X sigs.k8s.io/prow/version.Name=chatgpt
288295
- id: refresh
289296
dir: .
290297
main: cmd/external-plugins/refresh

.prow-images.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ images:
3939
- dir: cmd/external-plugins/needs-rebase
4040
- dir: cmd/external-plugins/cherrypicker
4141
- dir: cmd/external-plugins/refresh
42+
- dir: cmd/external-plugins/chatgpt
4243
- dir: cmd/ghproxy
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
token: <openai-token>
3+
base_url: https://<api-url>/
4+
api_type: AZURE
5+
api_version: 2023-03-15-preview
6+
engine: <deploy-engine-name>
7+
model: gpt-3.5-turbo
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"path"
10+
"sync"
11+
"time"
12+
13+
"github.com/sirupsen/logrus"
14+
"gopkg.in/yaml.v3"
15+
)
16+
17+
// ConfigAgent agent for fetch tasks with watching and hot reload.
18+
type ConfigAgent[T any] struct {
19+
path string
20+
config T
21+
mu sync.RWMutex
22+
}
23+
24+
// WatchConfig monitors a file for changes and sends a message on the channel when the file changes
25+
func (c *ConfigAgent[T]) WatchConfig(ctx context.Context, interval time.Duration, onChangeHandler func(f string) error) {
26+
var lastMod time.Time
27+
ticker := time.NewTicker(interval)
28+
defer ticker.Stop()
29+
30+
for {
31+
select {
32+
case <-ctx.Done():
33+
return
34+
case <-ticker.C:
35+
logrus.Debug("ticker")
36+
info, err := os.Stat(c.path)
37+
if err != nil {
38+
fmt.Printf("Error getting file info: %v\n", err)
39+
} else if modTime := info.ModTime(); modTime.After(lastMod) {
40+
lastMod = modTime
41+
onChangeHandler(c.path)
42+
}
43+
}
44+
}
45+
}
46+
47+
// Reload read and update config data.
48+
func (c *ConfigAgent[T]) Reload(file string, optionFuncs ...func() error) error {
49+
data, err := os.ReadFile(file)
50+
if err != nil {
51+
return fmt.Errorf("could no load config file %s: %w", file, err)
52+
}
53+
54+
c.mu.Lock()
55+
defer c.mu.Unlock()
56+
switch path.Ext(file) {
57+
case ".json":
58+
if err := json.Unmarshal(data, &c.config); err != nil {
59+
return fmt.Errorf("could not unmarshal JSON config: %w", err)
60+
}
61+
case ".yaml", ".yml":
62+
if err := yaml.Unmarshal(data, &c.config); err != nil {
63+
return fmt.Errorf("could not unmarshal YAML config: %w", err)
64+
}
65+
default:
66+
return errors.New("only support file with `.json` or `.yaml` extension")
67+
}
68+
69+
for _, f := range optionFuncs {
70+
if err := f(); err != nil {
71+
return err
72+
}
73+
}
74+
75+
return nil
76+
}
77+
78+
func (c *ConfigAgent[T]) Data() T {
79+
c.mu.RLock()
80+
defer c.mu.RUnlock()
81+
82+
return c.config
83+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/sashabaranov/go-openai"
8+
)
9+
10+
type OpenaiConfig struct {
11+
Token string `yaml:"token,omitempty" json:"token,omitempty"`
12+
BaseURL string `yaml:"base_url,omitempty" json:"base_url,omitempty"`
13+
OrgID string `yaml:"org_id,omitempty" json:"org_id,omitempty"`
14+
APIType string `yaml:"api_type,omitempty" json:"api_type,omitempty"` // OPEN_AI | AZURE | AZURE_AD
15+
APIVersion string `yaml:"api_version,omitempty" json:"api_version,omitempty"` // 2023-03-15-preview, required when APIType is APITypeAzure or APITypeAzureAD
16+
Engine string `yaml:"engine,omitempty" json:"engine,omitempty"` // required when APIType is APITypeAzure or APITypeAzureAD, it's the deploy instance name.
17+
Model string `yaml:"model,omitempty" json:"model,omitempty"` // OpenAI models, list ref: https://github.com/sashabaranov/go-openai/blob/master/completion.go#L15-L38
18+
19+
client *openai.Client
20+
}
21+
22+
func (cfg *OpenaiConfig) initClient() error {
23+
if cfg.client == nil {
24+
openaiCfg := openai.DefaultConfig(cfg.Token)
25+
openaiCfg.BaseURL = cfg.BaseURL
26+
openaiCfg.OrgID = cfg.OrgID
27+
openaiCfg.APIType = openai.APIType(cfg.APIType)
28+
openaiCfg.APIVersion = cfg.APIVersion
29+
openaiCfg.Engine = cfg.Engine
30+
31+
cfg.client = openai.NewClientWithConfig(openaiCfg)
32+
}
33+
34+
return nil
35+
}
36+
37+
// OpenaiAgent agent for openai clients with watching and hot reload.
38+
type OpenaiAgent struct {
39+
ConfigAgent[OpenaiConfig]
40+
}
41+
42+
// NewOpenaiAgent returns a new openai loader.
43+
func NewOpenaiAgent(path string, watchInterval time.Duration) (*OpenaiAgent, error) {
44+
c := &OpenaiAgent{ConfigAgent: ConfigAgent[OpenaiConfig]{path: path}}
45+
if err := c.Reload(path); err != nil {
46+
return nil, err
47+
}
48+
49+
go c.WatchConfig(context.Background(), watchInterval, c.Reload)
50+
51+
return c, nil
52+
}
53+
54+
func (a *OpenaiAgent) Reload(file string) error {
55+
return a.ConfigAgent.Reload(file, a.config.initClient)
56+
}
57+
58+
type OpenaiWrapAgent struct {
59+
small *OpenaiAgent
60+
large *OpenaiAgent
61+
largeDownThreshold int
62+
}
63+
64+
// NewOpenaiAgent returns a new openai loader.
65+
func NewWrapOpenaiAgent(defaultPath, largePath string, largeDownThreshold int, watchInterval time.Duration) (*OpenaiWrapAgent, error) {
66+
d, err := NewOpenaiAgent(defaultPath, watchInterval)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
ret := &OpenaiWrapAgent{small: d, largeDownThreshold: largeDownThreshold}
72+
if largePath != "" {
73+
l, err := NewOpenaiAgent(largePath, watchInterval)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
ret.large = l
79+
}
80+
81+
return ret, nil
82+
}
83+
84+
func (a *OpenaiWrapAgent) ClientFor(msgLen int) (*openai.Client, string) {
85+
if a.large != nil &&
86+
a.largeDownThreshold > 0 &&
87+
msgLen > a.largeDownThreshold {
88+
return a.large.Data().client, a.large.Data().Model
89+
}
90+
91+
return a.small.Data().client, a.small.Data().Model
92+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"regexp"
8+
"time"
9+
10+
"github.com/sirupsen/logrus"
11+
)
12+
13+
const (
14+
defaultSystemMessage = "You are an experienced software developer. You will act as a reviewer for a GitHub Pull Request, and you should answer by markdown format."
15+
defaultPromte = "Please identify potential problems and give some fixing suggestions."
16+
defaultPrPatchIntroducePromte = "This is the diff for the pull request:"
17+
defaultMaxResponseTokens = 500
18+
defaultTemperature = 0.7
19+
defaultStaticOutHeadnote = `> **I have already done a preliminary review for you, and I hope to help you do a better job.**
20+
------
21+
`
22+
)
23+
24+
// TasksConfig represent the all tasks store for the plugin.
25+
//
26+
// layer: org|repo / task-name / task-config
27+
type TasksConfig map[string]RepoTasks
28+
29+
// RepoTasks represent the tasks for a repo or ORG.
30+
type RepoTasks map[string]*Task
31+
32+
// Task reprensent the config for AI task item.
33+
//
34+
// $SystemMessage
35+
// --------------
36+
// < $Prompt
37+
// < Here are the serval context contents:
38+
// $ExternalContexts.each do
39+
//
40+
// < - format(it.PromptTpl, fetch(it.ResURL))
41+
//
42+
// < $PatchIntroducerPrompt
43+
// < ```diff
44+
// < diff content
45+
// < ```
46+
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
47+
// > <OutputStaticHeadNote>
48+
// > responses from AI server.
49+
//
50+
// TODO(wuhuizuo): using go template to comose the question.
51+
type Task struct {
52+
Description string `yaml:"description,omitempty" json:"description,omitempty"`
53+
SystemMessage string `yaml:"system_message,omitempty" json:"system_message,omitempty"`
54+
UserPrompt string `yaml:"user_prompt,omitempty" json:"user_prompt,omitempty"`
55+
PatchIntroducePrompt string `yaml:"patch_introduce_prompt,omitempty" json:"patch_introduce_prompt,omitempty"`
56+
OutputStaticHeadNote string `yaml:"output_static_head_note,omitempty" json:"output_static_head_note,omitempty"`
57+
MaxResponseTokens int `yaml:"max_response_tokens,omitempty" json:"max_response_tokens,omitempty"`
58+
ExternalContexts []*ExternalContext `yaml:"external_contexts,omitempty" json:"external_contexts,omitempty"`
59+
60+
AlwaysRun bool `yaml:"always_run,omitempty" json:"always_run,omitempty"` // automatic run or should triggered by comments.
61+
SkipAuthors []string `yaml:"skip_authors,omitempty" json:"skip_authors,omitempty"` // skip the pull request created by the authors.
62+
SkipBrancheRegs []string `yaml:"skip_branche_regs,omitempty" json:"skip_branche_regs,omitempty"` // skip the pull requests whiches target branch matched the regex.
63+
SkipLabelRegs []string `yaml:"skip_label_regs,omitempty" json:"skip_label_regs,omitempty"` // skip the pull reqeusts when any labels matched on the pull request.
64+
65+
skipBrancheRegs []*regexp.Regexp `yaml:"-" json:"-"`
66+
skipLabelRegs []*regexp.Regexp `yaml:"-" json:"-"`
67+
}
68+
69+
func (t *Task) initRegexps() error {
70+
if len(t.SkipBrancheRegs) != 0 {
71+
for _, br := range t.SkipBrancheRegs {
72+
reg, err := regexp.Compile(br)
73+
if err != nil {
74+
return err
75+
}
76+
t.skipBrancheRegs = append(t.skipBrancheRegs, reg)
77+
}
78+
}
79+
80+
if len(t.SkipLabelRegs) != 0 {
81+
for _, br := range t.SkipLabelRegs {
82+
reg, err := regexp.Compile(br)
83+
if err != nil {
84+
return err
85+
}
86+
t.skipLabelRegs = append(t.skipLabelRegs, reg)
87+
}
88+
}
89+
90+
return nil
91+
}
92+
93+
type ExternalContext struct {
94+
PromptTpl string `yaml:"prompt_tpl,omitempty" json:"prompt_tpl,omitempty"`
95+
ResURL string `yaml:"res_url,omitempty" json:"res_url,omitempty"`
96+
resContent []byte //nolint: unused // to be implemented.
97+
}
98+
99+
func (ec *ExternalContext) Content() ([]byte, error) {
100+
return nil, errors.New("not implemented")
101+
}
102+
103+
// TaskAgent agent for fetch tasks with watching and hot reload.
104+
type TaskAgent struct {
105+
ConfigAgent[TasksConfig]
106+
}
107+
108+
// NewTaskAgent returns a new ConfigLoader.
109+
func NewTaskAgent(path string, watchInterval time.Duration) (*TaskAgent, error) {
110+
c := &TaskAgent{ConfigAgent: ConfigAgent[TasksConfig]{path: path}}
111+
if err := c.Reload(path); err != nil {
112+
return nil, err
113+
}
114+
115+
go c.WatchConfig(context.Background(), watchInterval, c.Reload)
116+
117+
return c, nil
118+
}
119+
120+
func (a *TaskAgent) Reload(file string) error {
121+
return a.ConfigAgent.Reload(file, func() error {
122+
for _, repoCfg := range a.config {
123+
for _, task := range repoCfg {
124+
if err := task.initRegexps(); err != nil {
125+
return err
126+
}
127+
}
128+
}
129+
return nil
130+
})
131+
}
132+
133+
// Get return the config data.
134+
func (c *TaskAgent) TasksFor(org, repo string) (map[string]*Task, error) {
135+
c.mu.RLock()
136+
defer c.mu.RUnlock()
137+
138+
fullName := fmt.Sprintf("%s/%s", org, repo)
139+
repoTasks, ok := c.Data()[fullName]
140+
if ok {
141+
return repoTasks, nil
142+
}
143+
logrus.Debugf("no tasks for repo: %s", fullName)
144+
145+
orgTasks, ok := c.config[org]
146+
if ok {
147+
return orgTasks, nil
148+
}
149+
logrus.Debugf("no tasks for org %s", org)
150+
logrus.Debugf("all tasks: %#v", c.config)
151+
return nil, nil
152+
}
153+
154+
// Task return the given task config.
155+
func (c *TaskAgent) Task(org, repo, taskName string, needDefault bool) (*Task, error) {
156+
tasks, err := c.TasksFor(org, repo)
157+
if err != nil {
158+
return nil, err
159+
}
160+
161+
task := tasks[taskName]
162+
if task != nil {
163+
return task, nil
164+
}
165+
166+
if needDefault {
167+
return c.DefaultTask(), nil
168+
}
169+
170+
return nil, nil
171+
}
172+
173+
func (c *TaskAgent) DefaultTask() *Task {
174+
return &Task{
175+
SystemMessage: defaultSystemMessage,
176+
UserPrompt: defaultPromte,
177+
MaxResponseTokens: defaultMaxResponseTokens,
178+
PatchIntroducePrompt: defaultPrPatchIntroducePromte,
179+
}
180+
}

0 commit comments

Comments
 (0)