Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add azure openai provider #309

Merged
merged 11 commits into from
May 2, 2023
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,33 @@ _Analysis with serve mode_
```
curl -X GET "http://localhost:8080/analyze?namespace=k8sgpt&explain=false"
```
</details>

## Additional AI providers

### Azure OpenAI
<em>Prerequisites:</em> 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.
<details>

### Run k8sgpt
To run k8sgpt, run `k8sgpt auth` with the `azureopenai` backend:
```
k8sgpt auth --backend azureopenai --baseurl https://<your Azure OpenAI endpoint> --engine <deployment_name> --model <model_name>
```
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
```

</details>

### Running local models
arbreezy marked this conversation as resolved.
Show resolved Hide resolved
</details>

## 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.
Expand Down Expand Up @@ -293,6 +317,7 @@ The Kubernetes system is trying to scale a StatefulSet named fake-deployment usi


## Running local models
>>>>>>> main

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.

Expand All @@ -312,8 +337,6 @@ To run k8sgpt, run `k8sgpt auth` with the `localai` backend:
k8sgpt auth --backend localai --model <model_name> --baseurl http://localhost:8080/v1
```

When being asked for an API key, just press enter.

Now you can analyze with the `localai` backend:

```
Expand Down
26 changes: 23 additions & 3 deletions cmd/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,21 @@ var (
password string
baseURL string
model string
engine string
)

// authCmd represents the auth command
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 backend == "azureopenai" {
AlexsJones marked this conversation as resolved.
Show resolved Hide resolved
cmd.MarkFlagRequired("engine")
cmd.MarkFlagRequired("baseurl")
}
},
Run: func(cmd *cobra.Command, args []string) {

// get ai configuration
Expand All @@ -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)
}

Expand Down Expand Up @@ -88,6 +105,7 @@ var AuthCmd = &cobra.Command{
Model: model,
Password: password,
BaseURL: baseURL,
Engine: engine,
}

if providerIndex == -1 {
Expand Down Expand Up @@ -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")
arbreezy marked this conversation as resolved.
Show resolved Hide resolved
}
14 changes: 7 additions & 7 deletions cmd/serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
AlexsJones marked this conversation as resolved.
Show resolved Hide resolved
// 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)
Expand All @@ -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
}
Expand Down
98 changes: 98 additions & 0 deletions pkg/ai/azureopenai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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
sEnc := base64.StdEncoding.EncodeToString([]byte(inputKey))
cacheKey := util.GetCacheKey(a.GetName(), a.language, sEnc)
// find in viper cache
if cache.Exists(cacheKey) {
AlexsJones marked this conversation as resolved.
Show resolved Hide resolved
// retrieve data from cache
response, err := cache.Load(cacheKey)

if err != nil {
return "", err
}

if response == "" {
color.Red("error retrieving cached data")
return "", nil
}
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"
}
40 changes: 29 additions & 11 deletions pkg/ai/iai.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -30,19 +45,17 @@ type IAIConfig interface {
GetPassword() string
GetModel() string
GetBaseURL() string
GetEngine() string
}

arbreezy marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand All @@ -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"`
AlexsJones marked this conversation as resolved.
Show resolved Hide resolved
}

func (p *AIProvider) GetBaseURL() string {
Expand All @@ -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"
}