github.com/davinci-std/kanvas@v0.11.1/configai/openaichat.go (about) 1 package configai 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "os" 9 "sync" 10 "time" 11 12 "github.com/davinci-std/kanvas/openaichat" 13 ) 14 15 var APIKey = os.Getenv("OPENAI_API_KEY") 16 17 const ( 18 TEMPLATE = `You are who recommends a configuration for a tool called kanvas. Kanvas is the tool that abstracts the usages of various Infra-as-Code toolks like Terraform, Docker, ArgoCD. Kanvas's configuration file is named kanvas.yaml. A reference kanvas.yaml looks like the below. 19 20 --start of kanvas.yaml-- 21 22 components: 23 appimage: 24 # The repo is the GitHub repository where files for the component is located. 25 # This is usually omitted if the Dockerfile is located in the same repository as the kanvas.yaml. 26 #repo: davinci-std/exampleapp 27 # The dir is the directory where Dockerfile and the docker build context is located. 28 # If the dockerfile is at container/images/app/Dockerfile, the dir is container/images/app. 29 dir: containerimages/app 30 docker: 31 # The image is the name and the tag prefix of the container image to be built and pushed by kanvas. 32 image: "davinci-std/example:myownprefix-" 33 # base is a common component that is used to represent the cloud infrastructure. 34 # It often refers to a Terraform module that creates a Kubernetes cluster and its dependencies. 35 base: 36 # The repo is the GitHub repository where files for the component is located. 37 # For the base component, the repo usually contains a Terraform module that creates a Kubernetes cluster and its dependencies. 38 # It must be in ORG/REPO format. 39 # If ORG is not present, you can omit it. 40 repo: davinci-std/myinfra 41 # The dir is the directory where files for the component is located. 42 # This is the directory where the Terraform module for the specific environment (like development) is located. 43 # If the *.tf files are located in the path/to/exampleapp/terraform/module/*.tf, the dir is path/to/exampleapp/terraform/module. 44 dir: path/to/exampleapp/terraform/module 45 needs: 46 - appimage 47 terraform: 48 target: null_resource.eks_cluster 49 vars: 50 - name: containerimage_name 51 valueFrom: appimage.id 52 # argocd component exists only when you deploy your apps to Kubernetes clusters 53 # using ArgoCD, and you want to manage ArgoCD resources using kanvas. 54 # If you don't use ArgoCD, you can omit this component. 55 argocd: 56 # This is the directory where the Terraform module for ArgoCD is located. 57 dir: /tf2 58 needs: 59 - base 60 terraform: 61 target: aws_alb.argocd_api 62 vars: 63 - name: cluster_endpoint 64 valueFrom: base.cluster_endpoint 65 - name: cluster_token 66 valueFrom: base.cluster_token 67 argocd_resources: 68 # only path relative to where the command has run is supported 69 # maybe the top-project-level "dir" might be supported in the future 70 # which in combination with the relative path support for sub-project dir might be handy for DRY 71 dir: /tf2 72 needs: 73 - argocd 74 terraform: 75 target: argocd_application.kanvas 76 77 --end of kanvas.yaml-- 78 79 That said, please suggest a kanvas.yaml that fits my use-case. 80 %s 81 82 Here are relevant information: 83 84 We have repositories with following contents in the *nix tree command style: 85 86 Repositories: 87 88 %s 89 90 Contents: 91 %s 92 ` 93 ) 94 95 type ConfigRecommender struct { 96 APIKey string 97 98 once sync.Once 99 client *openaichat.Client 100 } 101 102 type SuggestOptions struct { 103 // If true, the recommender will ask questions to the user. 104 DoAsk bool 105 // If true, the recommender will use this API key instead of the default one. 106 APIKey string 107 // If true, the recommender will use functions. 108 UseFun bool 109 // If true, the recommender will use SSE. 110 SSE bool 111 // If true, the recommender will use this writer to log. 112 Log io.Writer 113 } 114 115 type SuggestOption func(*SuggestOptions) 116 117 func WithDoAsk(doAsk bool) SuggestOption { 118 return func(o *SuggestOptions) { 119 o.DoAsk = doAsk 120 } 121 } 122 123 func WithAPIKey(apiKey string) SuggestOption { 124 return func(o *SuggestOptions) { 125 o.APIKey = apiKey 126 } 127 } 128 129 func WithUseFun(useFun bool) SuggestOption { 130 return func(o *SuggestOptions) { 131 o.UseFun = useFun 132 } 133 } 134 135 func WithSSE(sse bool) SuggestOption { 136 return func(o *SuggestOptions) { 137 o.SSE = sse 138 } 139 } 140 141 func WithLog(log io.Writer) SuggestOption { 142 return func(o *SuggestOptions) { 143 o.Log = log 144 } 145 } 146 147 func (c *ConfigRecommender) Suggest(repos, contents string, opt ...SuggestOption) (*string, error) { 148 c.once.Do(func() { 149 if c.APIKey == "" { 150 c.APIKey = APIKey 151 } 152 c.client = &openaichat.Client{APIKey: c.APIKey} 153 }) 154 155 var opts SuggestOptions 156 157 for _, o := range opt { 158 o(&opts) 159 } 160 161 var ( 162 doAsk, useFun, sse bool 163 out io.Writer 164 ) 165 166 doAsk = opts.DoAsk 167 useFun = opts.UseFun 168 sse = opts.SSE 169 out = opts.Log 170 171 var conds string 172 if doAsk { 173 conds = "Please ask questions to me if needed." 174 } 175 content := fmt.Sprintf(TEMPLATE, conds, repos, contents) 176 177 messages := []openaichat.Message{ 178 {Role: "user", Content: content}, 179 } 180 181 var funcs []openaichat.Function 182 if useFun { 183 funcs = []openaichat.Function{ 184 { 185 Name: "this_is_the_kanvas_yaml", 186 Description: "Send the kanvas.yaml you generated to the user. You suggest and send the yaml to me by calling function. Don't use this to let me(user) suggest a kanvas.yaml. It's you(AI)'s job to suggest and send the yaml.", 187 Parameters: openaichat.FunctionParameters{ 188 Type: "object", 189 Properties: map[string]openaichat.FunctionParameterProperty{ 190 "kanvas_yaml": { 191 Type: "string", 192 Description: "Generated kanvas.yaml", 193 }, 194 }, 195 }, 196 }, 197 } 198 } 199 200 if !sse { 201 fmt.Fprintf(os.Stderr, "Starting to generate kanvas.yaml...\n") 202 203 // Measure the time to complete the request. 204 205 start := time.Now() 206 207 r, err := c.client.Complete(messages, funcs, openaichat.WithLog(out)) 208 if err != nil { 209 return nil, err 210 } 211 212 end := time.Now() 213 214 // Print the time to complete the request. 215 duration := end.Sub(start) 216 fmt.Fprintf(os.Stderr, "Completed to generate kanvas.yaml in %s\n", duration) 217 218 if useFun { 219 if r.Choice.FinishReason != "function_call" { 220 return nil, fmt.Errorf("unexpected finish reason: %s", r.Choice.FinishReason) 221 } 222 223 if r.Choice.Message.FunctionCall == nil { 224 return nil, fmt.Errorf("unexpected missing function call") 225 } 226 227 fc := r.Choice.Message.FunctionCall 228 229 if fc.Name != "this_is_the_kanvas_yaml" { 230 return nil, fmt.Errorf("unexpected function name: %s", fc.Name) 231 } 232 233 funRes, err := parseFunctionResult(bytes.NewBufferString(*fc.Arguments)) 234 if err != nil { 235 return nil, err 236 } 237 238 return &funRes.KanvasYAML, nil 239 } 240 241 return &r.Choice.Message.Content, nil 242 } 243 244 res, err := c.client.SSE(messages, funcs, openaichat.WithLog(out)) 245 if err != nil { 246 return nil, err 247 } 248 249 var ( 250 currentFunName string 251 generatedYAML string 252 ) 253 if useFun { 254 var jsonBuf bytes.Buffer 255 for _, c := range res.Choices { 256 if c.Delta == nil { 257 panic("unexpected missing delta") 258 } 259 d := c.Delta 260 fc := d.FunctionCall 261 if fc != nil { 262 if currentFunName == "" { 263 currentFunName = fc.Name 264 } else if fc.Name != "" { 265 currentFunName = fc.Name 266 } 267 268 if currentFunName != "this_is_the_kanvas_yaml" { 269 panic("unexpected funname " + currentFunName) 270 } 271 jsonBuf.WriteString(*fc.Arguments) 272 } 273 } 274 funRes, err := parseFunctionResult(&jsonBuf) 275 if err != nil { 276 return nil, err 277 } 278 generatedYAML = funRes.KanvasYAML 279 } 280 281 return &generatedYAML, nil 282 } 283 284 type FunRes struct { 285 KanvasYAML string `json:"kanvas_yaml"` 286 } 287 288 func parseFunctionResult(buf *bytes.Buffer) (*FunRes, error) { 289 var funRes FunRes 290 err := json.Unmarshal(buf.Bytes(), &funRes) 291 if err != nil { 292 return nil, err 293 } 294 295 return &funRes, nil 296 }