diff --git a/README.md b/README.md index 2d673d5..96198e7 100644 --- a/README.md +++ b/README.md @@ -262,9 +262,58 @@ _Analysis with serve mode_ ``` curl -X GET "http://localhost:8080/analyze?namespace=k8sgpt&explain=false" ``` + + +## Additional AI providers + +### Azure OpenAI +Prerequisites: an Azure OpenAI deployment is needed, please visit MS official [documentation](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource) to create your own. + +To authenticate with k8sgpt, you will need the Azure OpenAI endpoint of your tenant `"https://your Azure OpenAI Endpoint"`, the api key to access your deployment, the deployment name of your model and the model name itself. +
+ +### Run k8sgpt +To run k8sgpt, run `k8sgpt auth` with the `azureopenai` backend: +``` +k8sgpt auth --backend azureopenai --baseurl https:// --engine --model +``` +Lastly, enter your Azure API key, after the prompt. + +Now you are ready to analyze with the azure openai backend: +``` +k8sgpt analyze --explain --backend azureopenai +```
+### Running local models + +To run local models, it is possible to use OpenAI compatible APIs, for instance [LocalAI](https://github.com/go-skynet/LocalAI) which uses [llama.cpp](https://github.com/ggerganov/llama.cpp) and [ggml](https://github.com/ggerganov/ggml) to run inference on consumer-grade hardware. Models supported by LocalAI for instance are Vicuna, Alpaca, LLaMA, Cerebras, GPT4ALL, GPT4ALL-J and koala. + +
+ +To run local inference, you need to download the models first, for instance you can find `ggml` compatible models in [huggingface.com](https://huggingface.co/models?search=ggml) (for example vicuna, alpaca and koala). + +### Start the API server + +To start the API server, follow the instruction in [LocalAI](https://github.com/go-skynet/LocalAI#example-use-gpt4all-j-model). + +### Run k8sgpt + +To run k8sgpt, run `k8sgpt auth` with the `localai` backend: + +``` +k8sgpt auth --backend localai --model --baseurl http://localhost:8080/v1 +``` + +Now you can analyze with the `localai` backend: + +``` +k8sgpt analyze --explain --backend localai +``` + +
+ ## How does anonymization work? With this option, the data is anonymized before being sent to the AI Backend. During the analysis execution, `k8sgpt` retrieves sensitive data (Kubernetes object names, labels, etc.). This data is masked when sent to the AI backend and replaced by a key that can be used to de-anonymize the data when the solution is returned to the user. @@ -295,37 +344,6 @@ The Kubernetes system is trying to scale a StatefulSet named fake-deployment usi - -## Running local models - -To run local models, it is possible to use OpenAI compatible APIs, for instance [LocalAI](https://github.com/go-skynet/LocalAI) which uses [llama.cpp](https://github.com/ggerganov/llama.cpp) and [ggml](https://github.com/ggerganov/ggml) to run inference on consumer-grade hardware. Models supported by LocalAI for instance are Vicuna, Alpaca, LLaMA, Cerebras, GPT4ALL, GPT4ALL-J and koala. - -
- -To run local inference, you need to download the models first, for instance you can find `ggml` compatible models in [huggingface.co](https://huggingface.co/models?search=ggml) (for example vicuna, alpaca and koala). - -### Start the API server - -To start the API server, follow the instruction in [LocalAI](https://github.com/go-skynet/LocalAI#example-use-gpt4all-j-model). - -### Run k8sgpt - -To run k8sgpt, run `k8sgpt auth` with the `localai` backend: - -``` -k8sgpt auth --backend localai --model --baseurl http://localhost:8080/v1 -``` - -When being asked for an API key, just press enter. - -Now you can analyze with the `localai` backend: - -``` -k8sgpt analyze --explain --backend localai -``` - -
- ## Configuration
diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index cb077a1..3aa2975 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -31,6 +31,7 @@ var ( password string baseURL string model string + engine string ) // authCmd represents the auth command @@ -38,6 +39,13 @@ var AuthCmd = &cobra.Command{ Use: "auth", Short: "Authenticate with your chosen backend", Long: `Provide the necessary credentials to authenticate with your chosen backend.`, + PreRun: func(cmd *cobra.Command, args []string) { + backend, _ := cmd.Flags().GetString("backend") + if strings.ToLower(backend) == "azureopenai" { + cmd.MarkFlagRequired("engine") + cmd.MarkFlagRequired("baseurl") + } + }, Run: func(cmd *cobra.Command, args []string) { // get ai configuration @@ -57,9 +65,18 @@ var AuthCmd = &cobra.Command{ } } - // check if backend is not empty - if backend == "" { - color.Red("Error: Backend AI cannot be empty.") + validBackend := func(validBackends []string, backend string) bool { + for _, b := range validBackends { + if b == backend { + return true + } + } + return false + } + + // check if backend is not empty and a valid value + if backend == "" || !validBackend(ai.Backends, backend) { + color.Red("Error: Backend AI cannot be empty and accepted values are '%v'", strings.Join(ai.Backends, ", ")) os.Exit(1) } @@ -88,6 +105,7 @@ var AuthCmd = &cobra.Command{ Model: model, Password: password, BaseURL: baseURL, + Engine: engine, } if providerIndex == -1 { @@ -117,4 +135,6 @@ func init() { AuthCmd.Flags().StringVarP(&password, "password", "p", "", "Backend AI password") // add flag for url AuthCmd.Flags().StringVarP(&baseURL, "baseurl", "u", "", "URL AI provider, (e.g `http://localhost:8080/v1`)") + // add flag for azure open ai engine/deployment name + AuthCmd.Flags().StringVarP(&engine, "engine", "e", "", "Azure AI deployment name") } diff --git a/cmd/serve/serve.go b/cmd/serve/serve.go index 8a556a6..e00265b 100644 --- a/cmd/serve/serve.go +++ b/cmd/serve/serve.go @@ -47,17 +47,17 @@ var ServeCmd = &cobra.Command{ password := os.Getenv("K8SGPT_PASSWORD") model := os.Getenv("K8SGPT_MODEL") baseURL := os.Getenv("K8SGPT_BASEURL") - + engine := os.Getenv("K8SGPT_ENGINE") // If the envs are set, allocate in place to the aiProvider // else exit with error - envIsSet := backend != "" || password != "" || model != "" || baseURL != "" - + envIsSet := backend != "" || password != "" || model != "" if envIsSet { aiProvider = &ai.AIProvider{ Name: backend, Password: password, Model: model, BaseURL: baseURL, + Engine: engine, } configAI.Providers = append(configAI.Providers, *aiProvider) @@ -75,10 +75,10 @@ var ServeCmd = &cobra.Command{ if aiProvider == nil { for _, provider := range configAI.Providers { if backend == provider.Name { - // he pointer to the range variable is not really an issue here, as there - // is a break right after, but to prevent potential future issues, a temp - // variable is assigned - p := provider + // the pointer to the range variable is not really an issue here, as there + // is a break right after, but to prevent potential future issues, a temp + // variable is assigned + p := provider aiProvider = &p break } diff --git a/pkg/ai/azureopenai.go b/pkg/ai/azureopenai.go new file mode 100644 index 0000000..21593a2 --- /dev/null +++ b/pkg/ai/azureopenai.go @@ -0,0 +1,94 @@ +package ai + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "strings" + + "github.com/k8sgpt-ai/k8sgpt/pkg/cache" + "github.com/k8sgpt-ai/k8sgpt/pkg/util" + + "github.com/fatih/color" + + "github.com/sashabaranov/go-openai" +) + +type AzureAIClient struct { + client *openai.Client + language string + model string +} + +func (c *AzureAIClient) Configure(config IAIConfig, lang string) error { + token := config.GetPassword() + baseURL := config.GetBaseURL() + engine := config.GetEngine() + defaultConfig := openai.DefaultAzureConfig(token, baseURL, engine) + client := openai.NewClientWithConfig(defaultConfig) + if client == nil { + return errors.New("error creating Azure OpenAI client") + } + c.language = lang + c.client = client + c.model = config.GetModel() + return nil +} + +func (c *AzureAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) { + // Create a completion request + resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ + Model: c.model, + Messages: []openai.ChatCompletionMessage{ + { + Role: "user", + Content: fmt.Sprintf(default_prompt, c.language, prompt), + }, + }, + }) + if err != nil { + return "", err + } + return resp.Choices[0].Message.Content, nil +} + +func (a *AzureAIClient) Parse(ctx context.Context, prompt []string, cache cache.ICache) (string, error) { + inputKey := strings.Join(prompt, " ") + // Check for cached data + cacheKey := util.GetCacheKey(a.GetName(), a.language, inputKey) + + if !cache.IsCacheDisabled() && cache.Exists(cacheKey) { + response, err := cache.Load(cacheKey) + if err != nil { + return "", err + } + + if response != "" { + output, err := base64.StdEncoding.DecodeString(response) + if err != nil { + color.Red("error decoding cached data: %v", err) + return "", nil + } + return string(output), nil + } + } + + response, err := a.GetCompletion(ctx, inputKey) + if err != nil { + return "", err + } + + err = cache.Store(cacheKey, base64.StdEncoding.EncodeToString([]byte(response))) + + if err != nil { + color.Red("error storing value to cache: %v", err) + return "", nil + } + + return response, nil +} + +func (a *AzureAIClient) GetName() string { + return "azureopenai" +} diff --git a/pkg/ai/iai.go b/pkg/ai/iai.go index 39e66b3..767e1e5 100644 --- a/pkg/ai/iai.go +++ b/pkg/ai/iai.go @@ -19,6 +19,21 @@ import ( "github.com/k8sgpt-ai/k8sgpt/pkg/cache" ) +var ( + clients = []IAI{ + &OpenAIClient{}, + &AzureAIClient{}, + &LocalAIClient{}, + &NoOpAIClient{}, + } + Backends = []string{ + "openai", + "localai", + "azureopenai", + "noopai", + } +) + type IAI interface { Configure(config IAIConfig, language string) error GetCompletion(ctx context.Context, prompt string) (string, error) @@ -30,19 +45,17 @@ type IAIConfig interface { GetPassword() string GetModel() string GetBaseURL() string + GetEngine() string } func NewClient(provider string) IAI { - switch provider { - case "openai": - return &OpenAIClient{} - case "localai": - return &LocalAIClient{} - case "noopai": - return &NoOpAIClient{} - default: - return &OpenAIClient{} + for _, c := range clients { + if provider == c.GetName() { + return c + } } + // default client + return &OpenAIClient{} } type AIConfiguration struct { @@ -52,8 +65,9 @@ type AIConfiguration struct { type AIProvider struct { Name string `mapstructure:"name"` Model string `mapstructure:"model"` - Password string `mapstructure:"password"` - BaseURL string `mapstructure:"baseurl"` + Password string `mapstructure:"password" yaml:"password,omitempty"` + BaseURL string `mapstructure:"baseurl" yaml:"baseurl,omitempty"` + Engine string `mapstructure:"engine" yaml:"engine,omitempty"` } func (p *AIProvider) GetBaseURL() string { @@ -68,6 +82,10 @@ func (p *AIProvider) GetModel() string { return p.Model } +func (p *AIProvider) GetEngine() string { + return p.Engine +} + func NeedPassword(backend string) bool { return backend != "localai" }