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  }