From 1c7ae7951e2c9334183bad5cc8ef8502b7c95bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=A6=E7=A6=BB?= <101491308+jueli12@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:08:28 +0800 Subject: [PATCH 01/11] feat: support ai backends (#551) /kind feature support an AI backend service that other modules can use to access AI language models. --- cmd/karpor/app/options/ai.go | 69 +++++++++++++++++++++++++++ cmd/karpor/app/server.go | 10 ++++ go.mod | 4 +- go.sum | 8 +++- pkg/core/handler/search/search.go | 5 +- pkg/core/manager/ai/manager.go | 36 +++++++++++++++ pkg/core/route/route.go | 10 +++- pkg/infra/ai/azureopenai.go | 75 ++++++++++++++++++++++++++++++ pkg/infra/ai/huggingface.go | 54 ++++++++++++++++++++++ pkg/infra/ai/openai.go | 77 +++++++++++++++++++++++++++++++ pkg/infra/ai/prompts.go | 25 ++++++++++ pkg/infra/ai/types.go | 71 ++++++++++++++++++++++++++++ pkg/kubernetes/registry/types.go | 6 +++ 13 files changed, 445 insertions(+), 5 deletions(-) create mode 100644 cmd/karpor/app/options/ai.go create mode 100644 pkg/core/manager/ai/manager.go create mode 100644 pkg/infra/ai/azureopenai.go create mode 100644 pkg/infra/ai/huggingface.go create mode 100644 pkg/infra/ai/openai.go create mode 100644 pkg/infra/ai/prompts.go create mode 100644 pkg/infra/ai/types.go diff --git a/cmd/karpor/app/options/ai.go b/cmd/karpor/app/options/ai.go new file mode 100644 index 00000000..f47d5592 --- /dev/null +++ b/cmd/karpor/app/options/ai.go @@ -0,0 +1,69 @@ +// Copyright The Karpor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + "github.com/KusionStack/karpor/pkg/kubernetes/registry" + "github.com/spf13/pflag" +) + +type AIOptions struct { + Backend string + AuthToken string + BaseURL string + Model string + Temperature float32 + TopP float32 +} + +const ( + defaultBackend = "openai" + defaultModel = "gpt-3.5-turbo" + defaultTemperature = 1 + defaultTopP = 1 +) + +func NewAIOptions() *AIOptions { + return &AIOptions{} +} + +func (o *AIOptions) Validate() []error { + return nil +} + +func (o *AIOptions) ApplyTo(config *registry.ExtraConfig) error { + // Apply the AIOptions to the provided config + config.Backend = o.Backend + config.AuthToken = o.AuthToken + config.BaseURL = o.BaseURL + config.Model = o.Model + config.Temperature = o.Temperature + config.TopP = o.TopP + return nil +} + +// AddFlags adds flags for a specific Option to the specified FlagSet +func (o *AIOptions) AddFlags(fs *pflag.FlagSet) { + if o == nil { + return + } + + fs.StringVar(&o.Backend, "ai-backend", defaultBackend, "The ai backend") + fs.StringVar(&o.AuthToken, "ai-auth-token", "", "The ai auth token") + fs.StringVar(&o.BaseURL, "ai-base-url", "", "The ai base url") + fs.StringVar(&o.Model, "ai-model", defaultModel, "The ai model") + fs.Float32Var(&o.Temperature, "ai-temperature", defaultTemperature, "The ai temperature") + fs.Float32Var(&o.TopP, "ai-top-p", defaultTopP, "The ai top-p") +} diff --git a/cmd/karpor/app/server.go b/cmd/karpor/app/server.go index 9c3627c5..57ecba59 100644 --- a/cmd/karpor/app/server.go +++ b/cmd/karpor/app/server.go @@ -50,6 +50,7 @@ type Options struct { RecommendedOptions *options.RecommendedOptions SearchStorageOptions *options.SearchStorageOptions CoreOptions *options.CoreOptions + AIOptions *options.AIOptions StdOut io.Writer StdErr io.Writer @@ -66,6 +67,7 @@ func NewOptions(out, errOut io.Writer) (*Options, error) { ), SearchStorageOptions: options.NewSearchStorageOptions(), CoreOptions: options.NewCoreOptions(), + AIOptions: options.NewAIOptions(), StdOut: out, StdErr: errOut, } @@ -97,6 +99,9 @@ func NewServerCommand(ctx context.Context) *cobra.Command { expvar.Publish("StorageOptions", expvar.Func(func() interface{} { return o.SearchStorageOptions })) + expvar.Publish("AIOptions", expvar.Func(func() interface{} { + return o.AIOptions + })) expvar.Publish("Version", expvar.Func(func() interface{} { return version.GetVersion() })) @@ -132,6 +137,7 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) { o.RecommendedOptions.AddFlags(fs) o.SearchStorageOptions.AddFlags(fs) o.CoreOptions.AddFlags(fs) + o.AIOptions.AddFlags(fs) } // Validate validates Options @@ -139,6 +145,7 @@ func (o *Options) Validate(args []string) error { errors := []error{} errors = append(errors, o.RecommendedOptions.Validate()...) errors = append(errors, o.SearchStorageOptions.Validate()...) + errors = append(errors, o.AIOptions.Validate()...) return utilerrors.NewAggregate(errors) } @@ -162,6 +169,9 @@ func (o *Options) Config() (*server.Config, error) { if err := o.CoreOptions.ApplyTo(config.ExtraConfig); err != nil { return nil, err } + if err := o.AIOptions.ApplyTo(config.ExtraConfig); err != nil { + return nil, err + } config.GenericConfig.BuildHandlerChainFunc = func(handler http.Handler, c *genericapiserver.Config) http.Handler { handler = genericapiserver.DefaultBuildHandlerChain(handler, c) diff --git a/go.mod b/go.mod index e1f7f042..6344a176 100644 --- a/go.mod +++ b/go.mod @@ -15,10 +15,12 @@ require ( github.com/go-chi/render v1.0.3 github.com/go-logr/logr v1.2.3 github.com/google/gofuzz v1.2.0 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.4.0 + github.com/hupe1980/go-huggingface v0.0.15 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 github.com/pkg/errors v0.9.1 + github.com/sashabaranov/go-openai v1.27.0 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index 1f7770d9..4e8744d6 100644 --- a/go.sum +++ b/go.sum @@ -249,8 +249,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= @@ -268,6 +268,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hupe1980/go-huggingface v0.0.15 h1:tTWmUGGunC/BYz4hrwS8SSVtMYVYjceG2uhL8HxeXvw= +github.com/hupe1980/go-huggingface v0.0.15/go.mod h1:IRvsik3+b9BJyw9hCfw1arI6gDObcVto1UA8f3kt8mM= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= @@ -384,6 +386,8 @@ github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBO github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sashabaranov/go-openai v1.27.0 h1:L3hO6650YUbKrbGUC6yCjsUluhKZ9h1/jcgbTItI8Mo= +github.com/sashabaranov/go-openai v1.27.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= diff --git a/pkg/core/handler/search/search.go b/pkg/core/handler/search/search.go index cbbbe7bf..35bb3623 100644 --- a/pkg/core/handler/search/search.go +++ b/pkg/core/handler/search/search.go @@ -15,6 +15,7 @@ package search import ( + "github.com/KusionStack/karpor/pkg/core/manager/ai" "net/http" "strconv" @@ -45,12 +46,14 @@ import ( // @Failure 429 {string} string "Too Many Requests" // @Failure 500 {string} string "Internal Server Error" // @Router /rest-api/v1/search [get] -func SearchForResource(searchMgr *search.SearchManager, searchStorage storage.SearchStorage) http.HandlerFunc { +func SearchForResource(searchMgr *search.SearchManager, aiMgr *ai.AIManager, searchStorage storage.SearchStorage) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Extract the context and logger from the request. ctx := r.Context() logger := ctxutil.GetLogger(ctx) + //res, nil := aiMgr.ConvertTextToSQL("搜索集群cluster中kind为namespace的") + // Extract URL query parameters with default value searchQuery := r.URL.Query().Get("query") searchPattern := r.URL.Query().Get("pattern") diff --git a/pkg/core/manager/ai/manager.go b/pkg/core/manager/ai/manager.go new file mode 100644 index 00000000..699a4bc8 --- /dev/null +++ b/pkg/core/manager/ai/manager.go @@ -0,0 +1,36 @@ +// Copyright The Karpor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ai + +import ( + "github.com/KusionStack/karpor/pkg/infra/ai" + "github.com/KusionStack/karpor/pkg/kubernetes/registry" +) + +type AIManager struct { + client ai.AIProvider +} + +// NewAIManager returns a new AIManager object +func NewAIManager(c registry.ExtraConfig) (*AIManager, error) { + aiClient := ai.NewClient(c.Backend) + if err := aiClient.Configure(ai.ConvertToAIConfig(c)); err != nil { + return nil, err + } + + return &AIManager{ + client: aiClient, + }, nil +} diff --git a/pkg/core/route/route.go b/pkg/core/route/route.go index b35a866c..6b2eebb3 100644 --- a/pkg/core/route/route.go +++ b/pkg/core/route/route.go @@ -16,6 +16,7 @@ package route import ( "expvar" + docs "github.com/KusionStack/karpor/api/openapispec" clusterhandler "github.com/KusionStack/karpor/pkg/core/handler/cluster" detailhandler "github.com/KusionStack/karpor/pkg/core/handler/detail" @@ -29,6 +30,7 @@ import ( summaryhandler "github.com/KusionStack/karpor/pkg/core/handler/summary" topologyhandler "github.com/KusionStack/karpor/pkg/core/handler/topology" healthhandler "github.com/KusionStack/karpor/pkg/core/health" + aimanager "github.com/KusionStack/karpor/pkg/core/manager/ai" clustermanager "github.com/KusionStack/karpor/pkg/core/manager/cluster" insightmanager "github.com/KusionStack/karpor/pkg/core/manager/insight" resourcegroupmanager "github.com/KusionStack/karpor/pkg/core/manager/resourcegroup" @@ -87,6 +89,10 @@ func NewCoreRoute( if err != nil { return nil, err } + aiMgr, err := aimanager.NewAIManager(*extraConfig) + if err != nil { + return nil, err + } clusterMgr := clustermanager.NewClusterManager() searchMgr := searchmanager.NewSearchManager() @@ -94,6 +100,7 @@ func NewCoreRoute( // Set up the API routes for version 1 of the API. router.Route("/rest-api/v1", func(r chi.Router) { setupRestAPIV1(r, + aiMgr, clusterMgr, insightMgr, resourceGroupMgr, @@ -120,6 +127,7 @@ func NewCoreRoute( // resource type and setting up proper handlers. func setupRestAPIV1( r chi.Router, + aiMgr *aimanager.AIManager, clusterMgr *clustermanager.ClusterManager, insightMgr *insightmanager.InsightManager, resourceGroupMgr *resourcegroupmanager.ResourceGroupManager, @@ -144,7 +152,7 @@ func setupRestAPIV1( }) r.Route("/search", func(r chi.Router) { - r.Get("/", searchhandler.SearchForResource(searchMgr, searchStorage)) + r.Get("/", searchhandler.SearchForResource(searchMgr, aiMgr, searchStorage)) }) r.Route("/insight", func(r chi.Router) { diff --git a/pkg/infra/ai/azureopenai.go b/pkg/infra/ai/azureopenai.go new file mode 100644 index 00000000..37f9762e --- /dev/null +++ b/pkg/infra/ai/azureopenai.go @@ -0,0 +1,75 @@ +// Copyright The Karpor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ai + +import ( + "context" + "errors" + "github.com/sashabaranov/go-openai" +) + +type AzureAIClient struct { + client *openai.Client + model string + temperature float32 +} + +func (c *AzureAIClient) Configure(cfg AIConfig) error { + if cfg.AuthToken == "" { + return errors.New("auth token was not provided") + } + if cfg.BaseURL == "" { + return errors.New("base url was not provided") + } + + defaultConfig := openai.DefaultAzureConfig(cfg.AuthToken, cfg.BaseURL) + + client := openai.NewClientWithConfig(defaultConfig) + if client == nil { + return errors.New("error creating Azure OpenAI client") + } + + c.client = client + c.model = cfg.Model + c.temperature = cfg.Temperature + return nil +} + +func (c *AzureAIClient) Generate(ctx context.Context, prompt string, serviceType string) (string, error) { + servicePrompt := ServicePromptMap[serviceType] + + resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ + Model: c.model, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleSystem, + Content: servicePrompt, + }, + { + Role: openai.ChatMessageRoleUser, + Content: prompt, + }, + }, + Temperature: c.temperature, + }) + if err != nil { + return "", err + } + + if len(resp.Choices) == 0 { + return "", errors.New("no completion choices returned from response") + } + return resp.Choices[0].Message.Content, nil +} diff --git a/pkg/infra/ai/huggingface.go b/pkg/infra/ai/huggingface.go new file mode 100644 index 00000000..edd5e6eb --- /dev/null +++ b/pkg/infra/ai/huggingface.go @@ -0,0 +1,54 @@ +// Copyright The Karpor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ai + +import ( + "context" + "github.com/hupe1980/go-huggingface" +) + +type HuggingfaceClient struct { + client *huggingface.InferenceClient + model string + temperature float32 +} + +func (c *HuggingfaceClient) Configure(cfg AIConfig) error { + client := huggingface.NewInferenceClient(cfg.AuthToken) + + c.client = client + c.model = cfg.Model + c.temperature = cfg.Temperature + return nil +} + +func (c *HuggingfaceClient) Generate(ctx context.Context, prompt string, serviceType string) (string, error) { + resp, err := c.client.TextGeneration(ctx, &huggingface.TextGenerationRequest{ + Inputs: prompt, + Parameters: huggingface.TextGenerationParameters{ + Temperature: huggingface.PTR(float64(c.temperature)), + }, + Options: huggingface.Options{ + WaitForModel: huggingface.PTR(true), + }, + Model: c.model, + }) + + if err != nil { + return "", err + } + return resp[0].GeneratedText[len(prompt):], nil + +} diff --git a/pkg/infra/ai/openai.go b/pkg/infra/ai/openai.go new file mode 100644 index 00000000..82ef8c14 --- /dev/null +++ b/pkg/infra/ai/openai.go @@ -0,0 +1,77 @@ +// Copyright The Karpor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ai + +import ( + "context" + "errors" + "github.com/sashabaranov/go-openai" +) + +type OpenAIClient struct { + client *openai.Client + model string + temperature float32 + topP float32 +} + +func (c *OpenAIClient) Configure(cfg AIConfig) error { + if cfg.AuthToken == "" { + return errors.New("auth token was not provided") + } + defaultConfig := openai.DefaultConfig(cfg.AuthToken) + if cfg.BaseURL != "" { + defaultConfig.BaseURL = cfg.BaseURL + } + + client := openai.NewClientWithConfig(defaultConfig) + if client == nil { + return errors.New("error creating OpenAI client") + } + + c.client = client + c.model = cfg.Model + c.temperature = cfg.Temperature + c.topP = cfg.TopP + return nil +} + +func (c *OpenAIClient) Generate(ctx context.Context, prompt string, serviceType string) (string, error) { + servicePrompt := ServicePromptMap[serviceType] + + resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ + Model: c.model, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleSystem, + Content: servicePrompt, + }, + { + Role: openai.ChatMessageRoleUser, + Content: prompt, + }, + }, + Temperature: c.temperature, + TopP: c.topP, + }) + if err != nil { + return "", err + } + + if len(resp.Choices) == 0 { + return "", errors.New("no completion choices returned from response") + } + return resp.Choices[0].Message.Content, nil +} diff --git a/pkg/infra/ai/prompts.go b/pkg/infra/ai/prompts.go new file mode 100644 index 00000000..7d4479a5 --- /dev/null +++ b/pkg/infra/ai/prompts.go @@ -0,0 +1,25 @@ +// Copyright The Karpor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ai + +const ( + default_prompt = "You are a helpful assistant." + search_prompt = "You are an AI specialized in writing SQL queries. Please provide the SQL statement as a formatted string." +) + +var ServicePromptMap = map[string]string{ + "default": default_prompt, + "search": search_prompt, +} diff --git a/pkg/infra/ai/types.go b/pkg/infra/ai/types.go new file mode 100644 index 00000000..0d2025eb --- /dev/null +++ b/pkg/infra/ai/types.go @@ -0,0 +1,71 @@ +// Copyright The Karpor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ai + +import ( + "context" + "github.com/KusionStack/karpor/pkg/kubernetes/registry" +) + +const ( + AzureProvider = "azureopenai" + HuggingFaceProvider = "huggingface" + OpenAIProvider = "openai" +) + +var clients = map[string]AIProvider{ + AzureProvider: &AzureAIClient{}, + HuggingFaceProvider: &HuggingfaceClient{}, + OpenAIProvider: &OpenAIClient{}, +} + +// AIProvider is an interface all AI clients. +type AIProvider interface { + // Configure sets up the AI service with the provided configuration. + Configure(config AIConfig) error + // Generate generates a response from the AI service based on + // the provided prompt and service type. + Generate(ctx context.Context, prompt string, serviceType string) (string, error) +} + +// AIConfig represents the configuration settings for an AI client. +type AIConfig struct { + Name string + AuthToken string + BaseURL string + Model string + Temperature float32 + TopP float32 +} + +func ConvertToAIConfig(c registry.ExtraConfig) AIConfig { + return AIConfig{ + Name: c.Backend, + AuthToken: c.AuthToken, + BaseURL: c.BaseURL, + Model: c.Model, + Temperature: c.Temperature, + TopP: c.TopP, + } +} + +// NewClient returns a new AIProvider object +func NewClient(name string) AIProvider { + if client, exists := clients[name]; exists { + return client + } + // default client + return &OpenAIClient{} +} diff --git a/pkg/kubernetes/registry/types.go b/pkg/kubernetes/registry/types.go index 688269a0..333a8bef 100644 --- a/pkg/kubernetes/registry/types.go +++ b/pkg/kubernetes/registry/types.go @@ -37,4 +37,10 @@ type ExtraConfig struct { ElasticSearchPassword string ReadOnlyMode bool GithubBadge bool + Backend string + AuthToken string + BaseURL string + Model string + Temperature float32 + TopP float32 } From f40b5f5b24b0b3353d3f03b897186995ec7e2129 Mon Sep 17 00:00:00 2001 From: ruquanzhao <903264308@qq.com> Date: Tue, 3 Sep 2024 11:21:36 +0800 Subject: [PATCH 02/11] fix: fix gofmt --- pkg/kubernetes/registry/types.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/kubernetes/registry/types.go b/pkg/kubernetes/registry/types.go index 05f5dc74..51f4fa91 100644 --- a/pkg/kubernetes/registry/types.go +++ b/pkg/kubernetes/registry/types.go @@ -40,17 +40,17 @@ type ExtraConfig struct { ElasticSearchPassword string ReadOnlyMode bool GithubBadge bool - - // ServiceAccount configs - ServiceAccountIssuer serviceaccount.TokenGenerator + + // ServiceAccount configs + ServiceAccountIssuer serviceaccount.TokenGenerator ServiceAccountMaxExpiration time.Duration ExtendExpiration bool - - // AI configs - Backend string - AuthToken string - BaseURL string - Model string - Temperature float32 - TopP float32 + + // AI configs + Backend string + AuthToken string + BaseURL string + Model string + Temperature float32 + TopP float32 } From bc2b35822a97350eeefb9a55f3a46ba2f287de76 Mon Sep 17 00:00:00 2001 From: hai-tian Date: Tue, 3 Sep 2024 11:30:42 +0800 Subject: [PATCH 03/11] feat: search by natural language (#613) ## What type of PR is this? /kind style ## What this PR does / why we need it: TODO ## Which issue(s) this PR fixes: Fixes # --- ui/src/components/sqlSearch/index.tsx | 785 +++++++++--------- ui/src/locales/de.json | 4 +- ui/src/locales/en.json | 4 +- ui/src/locales/pt.json | 4 +- ui/src/locales/zh.json | 4 +- .../components/tagVariableSizeList/index.tsx | 1 + ui/src/pages/result/index.tsx | 203 ++++- ui/src/pages/search/index.tsx | 99 ++- ui/src/utils/constants.ts | 4 + ui/src/utils/tools.ts | 23 + 10 files changed, 703 insertions(+), 428 deletions(-) diff --git a/ui/src/components/sqlSearch/index.tsx b/ui/src/components/sqlSearch/index.tsx index 66d9b166..b7df71a8 100644 --- a/ui/src/components/sqlSearch/index.tsx +++ b/ui/src/components/sqlSearch/index.tsx @@ -35,6 +35,11 @@ import { searchSqlPrefix, } from '@/utils/constants' import { useAxios } from '@/utils/request' +import { + cacheHistory, + deleteHistoryByItem, + getHistoryList, +} from '@/utils/tools' import styles from './styles.module.less' @@ -90,436 +95,426 @@ const focusHandlerExtension = EditorView.domEventHandlers({ type SqlSearchIProps = { sqlEditorValue: string - handleSearch: (val: string) => void -} - -function getHistoryList() { - return localStorage?.getItem('sqlEditorHistory') - ? JSON.parse(localStorage?.getItem('sqlEditorHistory')) - : [] + handleSqlSearch: (val: string) => void } -function deleteHistoryByItem(val: string) { - const lastHistory: any = localStorage.getItem('sqlEditorHistory') - const tmp = lastHistory ? JSON.parse(lastHistory) : [] - if (tmp?.length > 0 && tmp?.includes(val)) { - const newList = tmp?.filter(item => item !== val) - localStorage.setItem('sqlEditorHistory', JSON.stringify(newList)) - } -} +const SqlSearch = memo( + ({ sqlEditorValue, handleSqlSearch }: SqlSearchIProps) => { + const editorRef = useRef(null) + const { t, i18n } = useTranslation() + const clusterListRef = useRef(null) + const [clusterList, setClusterList] = useState([]) + const [historyCompletions, setHistoryCompletions] = useState< + { value: string }[] + >([]) + const historyCompletionsRef = useRef( + getHistoryList('sqlEditorHistory'), + ) + + function cacheSqlHistory(val: string) { + const result = cacheHistory('sqlEditorHistory', val) + historyCompletionsRef.current = result + setHistoryCompletions(historyCompletionsRef.current) + } -const SqlSearch = memo(({ sqlEditorValue, handleSearch }: SqlSearchIProps) => { - const editorRef = useRef(null) - const { t, i18n } = useTranslation() - const clusterListRef = useRef(null) - const [clusterList, setClusterList] = useState([]) - const [historyCompletions, setHistoryCompletions] = useState< - { value: string }[] - >([]) - const historyCompletionsRef = useRef(getHistoryList()) - - function cacheHistory(val: string) { - const lastHistory: any = localStorage.getItem('sqlEditorHistory') - const tmp = lastHistory ? JSON.parse(lastHistory) : [] - const newList = [val, ...tmp?.filter(item => item !== val)] - localStorage.setItem('sqlEditorHistory', JSON.stringify(newList)) - historyCompletionsRef.current = getHistoryList() - setHistoryCompletions(historyCompletionsRef.current) - } - - const { response } = useAxios({ - url: '/rest-api/v1/clusters', - option: { - params: { - orderBy: 'name', - ascending: true, + const { response } = useAxios({ + url: '/rest-api/v1/clusters', + option: { + params: { + orderBy: 'name', + ascending: true, + }, }, - }, - manual: false, - method: 'GET', - }) - - useEffect(() => { - if (response?.success) { - clusterListRef.current = response?.data?.items - setClusterList(response?.data?.items) - } - }, [response]) - - useEffect(() => { - getHistoryList() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - function getCustomCompletions(regMatch, cusCompletions, pos) { - const filterTerm = regMatch[2] - const customCompletions = cusCompletions - .filter(completion => - completion.toLowerCase().includes(filterTerm.toLowerCase()), - ) - .map(completion => ({ - label: completion, - type: 'custom', - apply: completion, - boost: 0, - })) - - const from = pos - filterTerm?.length - if (customCompletions?.length > 0) { - return { from, options: customCompletions } - } - return null - } - - useEffect(() => { - if (editorRef.current) { - const contentEditableElement = editorRef.current.querySelector( - '.cm-content', - ) as any - if (contentEditableElement) { - contentEditableElement.style.outline = 'none' + manual: false, + method: 'GET', + }) + + useEffect(() => { + if (response?.success) { + clusterListRef.current = response?.data?.items + setClusterList(response?.data?.items) } - const customCompletionKeymap: KeyBinding[] = [ - { key: 'Tab', run: acceptCompletion }, - ] - const overrideKeymap = keymap.of( - customCompletionKeymap.concat( - completionKeymap.filter(b => b.key !== 'Enter'), - ), - ) - const mySQLHighlightStyle = HighlightStyle.define([ - { tag: tags.keyword, color: 'blue' }, - ]) - - const customCompletion = context => { - const { state, pos } = context - const beforeCursor = state.doc.sliceString(0, pos) - if (state.doc?.length === 0) { - const historyOptions: any[] = historyCompletionsRef?.current?.map( - record => ({ - label: record, - type: 'history', - apply: record, - }), - ) - return { - from: context.pos, - options: historyOptions, - filter: false, - } - } - - const whereMatch = /\b(where|or|and) (\S*)$/.exec(beforeCursor) - if (whereMatch) { - return getCustomCompletions(whereMatch, whereKeywords, pos) - } - - const kindMatch = /(kind\s*=\s*)(\S*)$/i.exec(beforeCursor) - if (kindMatch) { - return getCustomCompletions(kindMatch, kindCompletions, pos) - } + }, [response]) + + useEffect(() => { + getHistoryList('sqlEditorHistory') + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + function getCustomCompletions(regMatch, cusCompletions, pos) { + const filterTerm = regMatch[2] + const customCompletions = cusCompletions + .filter(completion => + completion.toLowerCase().includes(filterTerm.toLowerCase()), + ) + .map(completion => ({ + label: completion, + type: 'custom', + apply: completion, + boost: 0, + })) + + const from = pos - filterTerm?.length + if (customCompletions?.length > 0) { + return { from, options: customCompletions } + } + return null + } - if (whereKeywords?.some(item => beforeCursor?.endsWith(`${item} `))) { - const customCompletions = operatorKeywords.map(completion => ({ - label: completion, - type: 'custom', - validFor: () => false, - })) - return { from: pos, options: customCompletions } + useEffect(() => { + if (editorRef.current) { + const contentEditableElement = editorRef.current.querySelector( + '.cm-content', + ) as any + if (contentEditableElement) { + contentEditableElement.style.outline = 'none' } + const customCompletionKeymap: KeyBinding[] = [ + { key: 'Tab', run: acceptCompletion }, + ] + const overrideKeymap = keymap.of( + customCompletionKeymap.concat( + completionKeymap.filter(b => b.key !== 'Enter'), + ), + ) + const mySQLHighlightStyle = HighlightStyle.define([ + { tag: tags.keyword, color: 'blue' }, + ]) + + const customCompletion = context => { + const { state, pos } = context + const beforeCursor = state.doc.sliceString(0, pos) + if (state.doc?.length === 0) { + const historyOptions: any[] = historyCompletionsRef?.current?.map( + record => ({ + label: record, + type: 'history', + apply: record, + }), + ) + return { + from: context.pos, + options: historyOptions, + filter: false, + } + } - const clusterMatch = /(cluster\s*=\s*)(\S*)$/i.exec(beforeCursor) - if (clusterMatch) { - const clusterNameList = clusterListRef.current?.map( - item => `'${item?.metadata?.name}'`, - ) - return getCustomCompletions(clusterMatch, clusterNameList, pos) - } + const whereMatch = /\b(where|or|and) (\S*)$/.exec(beforeCursor) + if (whereMatch) { + return getCustomCompletions(whereMatch, whereKeywords, pos) + } - const word = context?.matchBefore(/\w*/) + const kindMatch = /(kind\s*=\s*)(\S*)$/i.exec(beforeCursor) + if (kindMatch) { + return getCustomCompletions(kindMatch, kindCompletions, pos) + } - if (!word || (word?.from === word?.to && !context?.explicit)) { - return null - } + if (whereKeywords?.some(item => beforeCursor?.endsWith(`${item} `))) { + const customCompletions = operatorKeywords.map(completion => ({ + label: completion, + type: 'custom', + validFor: () => false, + })) + return { from: pos, options: customCompletions } + } - const options = defaultKeywords - .filter(kw => kw.toLowerCase().includes(word.text?.toLowerCase())) - .map(stmt => ({ label: stmt, type: 'custom' })) - if (options?.length === 0) { - return null - } + const clusterMatch = /(cluster\s*=\s*)(\S*)$/i.exec(beforeCursor) + if (clusterMatch) { + const clusterNameList = clusterListRef.current?.map( + item => `'${item?.metadata?.name}'`, + ) + return getCustomCompletions(clusterMatch, clusterNameList, pos) + } - return completeFromList(options)(context) - } + const word = context?.matchBefore(/\w*/) - const completionPlugin = ViewPlugin.fromClass( - class { - constructor(view) { - this.addDeleteButtons(view) + if (!word || (word?.from === word?.to && !context?.explicit)) { + return null } - update(update) { - this.addDeleteButtons(update.view) + const options = defaultKeywords + .filter(kw => kw.toLowerCase().includes(word.text?.toLowerCase())) + .map(stmt => ({ label: stmt, type: 'custom' })) + if (options?.length === 0) { + return null } - addDeleteButtons(view) { - const compState: any = completionStatus(view.state) - if (compState === 'active') { - const completions: any = currentCompletions(view.state) - setTimeout(() => { - if (completions?.[0]?.type === 'history') { - view.dom - .querySelectorAll( - '.cm-tooltip.cm-tooltip-autocomplete > ul > li', - ) - .forEach((item, index) => { - if ( - item.querySelector( - '.cm-tooltip-autocomplete_item_label', - ) - ) { - return - } - if (item.querySelector('.delete-btn')) { - return - } - const labelSpan = document.createElement('span') - labelSpan.className = 'cm-tooltip-autocomplete_item_label' - labelSpan.innerText = completions?.[index]?.label - item.style.display = 'flex' - item.style.justifyContent = 'space-between' - item.style.alignItems = 'center' - labelSpan.style.flex = '1' - labelSpan.style.overflow = 'hidden' - labelSpan.style.textOverflow = 'ellipsis' - labelSpan.style.whiteSpace = 'nowrap' - labelSpan.style.padding = '0 10px' - labelSpan.style.fontSize = '14px' - const btn = document.createElement('span') - btn.innerText = '✕' - btn.className = 'delete-btn' - btn.style.border = 'none' - btn.style.fontSize = '20px' - btn.style.display = 'flex' - btn.style.justifyContent = 'center' - btn.style.alignItems = 'center' - btn.style.height = '100%' - btn.style.padding = '0 15px' - item.innerText = '' - item.appendChild(labelSpan) - item.appendChild(btn) - btn.addEventListener('mousedown', event => { - event.preventDefault() - event.stopPropagation() - const completionOption = completions?.[index] - historyCompletionsRef.current = - historyCompletionsRef?.current?.filter( - item => item !== completionOption?.label, + return completeFromList(options)(context) + } + + const completionPlugin = ViewPlugin.fromClass( + class { + constructor(view) { + this.addDeleteButtons(view) + } + + update(update) { + this.addDeleteButtons(update.view) + } + + addDeleteButtons(view) { + const compState: any = completionStatus(view.state) + if (compState === 'active') { + const completions: any = currentCompletions(view.state) + setTimeout(() => { + if (completions?.[0]?.type === 'history') { + view.dom + .querySelectorAll( + '.cm-tooltip.cm-tooltip-autocomplete > ul > li', + ) + .forEach((item, index) => { + if ( + item.querySelector( + '.cm-tooltip-autocomplete_item_label', ) - if (view) { - startCompletion(view) - deleteHistoryByItem(completionOption?.label) + ) { + return } + if (item.querySelector('.delete-btn')) { + return + } + const labelSpan = document.createElement('span') + labelSpan.className = + 'cm-tooltip-autocomplete_item_label' + labelSpan.innerText = completions?.[index]?.label + item.style.display = 'flex' + item.style.justifyContent = 'space-between' + item.style.alignItems = 'center' + labelSpan.style.flex = '1' + labelSpan.style.overflow = 'hidden' + labelSpan.style.textOverflow = 'ellipsis' + labelSpan.style.whiteSpace = 'nowrap' + labelSpan.style.padding = '0 10px' + labelSpan.style.fontSize = '14px' + const btn = document.createElement('span') + btn.innerText = '✕' + btn.className = 'delete-btn' + btn.style.border = 'none' + btn.style.fontSize = '20px' + btn.style.display = 'flex' + btn.style.justifyContent = 'center' + btn.style.alignItems = 'center' + btn.style.height = '100%' + btn.style.padding = '0 15px' + item.innerText = '' + item.appendChild(labelSpan) + item.appendChild(btn) + btn.addEventListener('mousedown', event => { + event.preventDefault() + event.stopPropagation() + const completionOption = completions?.[index] + historyCompletionsRef.current = + historyCompletionsRef?.current?.filter( + item => item !== completionOption?.label, + ) + if (view) { + startCompletion(view) + deleteHistoryByItem( + 'sqlEditorHistory', + completionOption?.label, + ) + } + }) }) - }) - } - }, 0) + } + }, 0) + } } - } - }, - ) - - const startState = EditorState.create({ - doc: '', - extensions: [ - completionPlugin, - placeholder(`${t('SearchUsingSQL')} ......`), - placeholderStyle, - new LanguageSupport(sql() as any), - highlightSpecialChars(), - syntaxHighlighting(mySQLHighlightStyle), - autocompletion({ - override: [customCompletion], - }), - focusHandlerExtension, - autocompletion(), - overrideKeymap, - EditorView.domEventHandlers({ - keydown: (event, view) => { - if (event.key === 'Enter') { - const completions = currentCompletions(view.state) - if (!completions || completions?.length === 0) { - event.preventDefault() - handleClick() - return true + }, + ) + + const startState = EditorState.create({ + doc: '', + extensions: [ + completionPlugin, + placeholder(`${t('SearchUsingSQL')} ......`), + placeholderStyle, + new LanguageSupport(sql() as any), + highlightSpecialChars(), + syntaxHighlighting(mySQLHighlightStyle), + autocompletion({ + override: [customCompletion], + }), + focusHandlerExtension, + autocompletion(), + overrideKeymap, + EditorView.domEventHandlers({ + keydown: (event, view) => { + if (event.key === 'Enter') { + const completions = currentCompletions(view.state) + if (!completions || completions?.length === 0) { + event.preventDefault() + handleClick() + return true + } + } + return false + }, + }), + EditorState.allowMultipleSelections.of(false), + EditorView.updateListener.of(update => { + if (update.docChanged) { + if (update.state.doc.lines > 1) { + update.view.dispatch({ + changes: { + from: update.startState.doc?.length, + to: update.state.doc?.length, + }, + }) } } - return false - }, - }), - EditorState.allowMultipleSelections.of(false), - EditorView.updateListener.of(update => { - if (update.docChanged) { - if (update.state.doc.lines > 1) { - update.view.dispatch({ - changes: { - from: update.startState.doc?.length, - to: update.state.doc?.length, - }, - }) - } - } - }), - ], - }) + }), + ], + }) - const view = new EditorView({ - state: startState, - parent: editorRef.current, - }) + const view = new EditorView({ + state: startState, + parent: editorRef.current, + }) - editorRef.current.view = view + editorRef.current.view = view - return () => { - if (editorRef.current?.view) { - // eslint-disable-next-line react-hooks/exhaustive-deps - editorRef.current?.view?.destroy() + return () => { + if (editorRef.current?.view) { + // eslint-disable-next-line react-hooks/exhaustive-deps + editorRef.current?.view?.destroy() + } } } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [editorRef.current, historyCompletions, i18n?.language]) - - useEffect(() => { - if (sqlEditorValue && clusterList && editorRef.current?.view) { - editorRef.current?.view.dispatch({ - changes: { - from: 0, - to: editorRef.current?.view.state.doc?.length, - insert: sqlEditorValue, - }, - }) - } - }, [clusterList, editorRef.current?.view, sqlEditorValue]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editorRef.current, historyCompletions, i18n?.language]) + + useEffect(() => { + if (sqlEditorValue && clusterList && editorRef.current?.view) { + editorRef.current?.view.dispatch({ + changes: { + from: 0, + to: editorRef.current?.view.state.doc?.length, + insert: sqlEditorValue, + }, + }) + } + }, [clusterList, editorRef.current?.view, sqlEditorValue]) - const getContent = () => { - if (editorRef.current?.view) { - const content = editorRef.current.view.state.doc.toString() - return content + const getContent = () => { + if (editorRef.current?.view) { + const content = editorRef.current.view.state.doc.toString() + return content + } + return '' } - return '' - } - - function handleClick() { - const inputValue = getContent() - if (!inputValue) { - message.warning(t('PleaseEnterValidSQLStatement')) - return + + function handleClick() { + const inputValue = getContent() + if (!inputValue) { + message.warning(t('PleaseEnterValidSQLStatement')) + return + } + cacheSqlHistory(inputValue) + handleSqlSearch(inputValue) } - cacheHistory(inputValue) - handleSearch(inputValue) - } - - return ( -
-
-
{searchSqlPrefix}
-
-
ul { - box-sizing: border-box; - height: auto; - max-height: 40vh; - overflow-y: auto !important; - } - .cm-tooltip.cm-tooltip-autocomplete > ul > li { - background-color: #f5f5f5 !important; - margin: 5px 0 !important; - padding: 10px 0 !important; - border-radius: 6px !important; - width: auto !important; - box-sizing: border-box; - } - .cm-tooltip.cm-tooltip-autocomplete - > ul - > li[aria-selected='true'], - .cm-tooltip.cm-tooltip-autocomplete > ul > li:hover { - background-color: #97a9f5 !important; - color: white !important; - } - `} - /> -
- -
-
-
- + return ( +
+
+
{searchSqlPrefix}
+
+
ul { + box-sizing: border-box; + height: auto; + max-height: 40vh; + overflow-y: auto !important; + } + .cm-tooltip.cm-tooltip-autocomplete > ul > li { + background-color: #f5f5f5 !important; + margin: 5px 0 !important; + padding: 10px 0 !important; + border-radius: 6px !important; + width: auto !important; + box-sizing: border-box; + } + + .cm-tooltip.cm-tooltip-autocomplete + > ul + > li[aria-selected='true'], + .cm-tooltip.cm-tooltip-autocomplete > ul > li:hover { + background-color: #97a9f5 !important; + color: white !important; + } + `} + /> +
+ +
+
+
+ +
-
- ) -}) + ) + }, +) export default SqlSearch diff --git a/ui/src/locales/de.json b/ui/src/locales/de.json index df6d8c68..e1c874a8 100644 --- a/ui/src/locales/de.json +++ b/ui/src/locales/de.json @@ -119,5 +119,7 @@ "LoginSuccess": "Erfolgreich eingeloggt", "Login": "Einloggen", "LogoutSuccess": "Erfolgreich abgemeldet", - "InputToken": "Geben Sie bitte den Token ein" + "InputToken": "Geben Sie bitte den Token ein", + "SearchByNaturalLanguage": "Suche mit natürlicher Sprache", + "CannotBeEmpty": "Darf nicht leer sein" } diff --git a/ui/src/locales/en.json b/ui/src/locales/en.json index b96d3ffa..1b153882 100644 --- a/ui/src/locales/en.json +++ b/ui/src/locales/en.json @@ -119,5 +119,7 @@ "LoginSuccess": "Login successful", "Login": "Login", "LogoutSuccess": "Successfully logged out", - "InputToken": "Please enter the token" + "InputToken": "Please enter the token", + "SearchByNaturalLanguage": "Search By Natural Language", + "CannotBeEmpty": "Cannot be empty" } diff --git a/ui/src/locales/pt.json b/ui/src/locales/pt.json index 112f8f10..f295d23a 100644 --- a/ui/src/locales/pt.json +++ b/ui/src/locales/pt.json @@ -119,5 +119,7 @@ "LoginSuccess": "Login bem-sucedido", "Login": "Login", "LogoutSuccess": "Sessão encerrada com sucesso", - "InputToken": "Por favor, insira o token" + "InputToken": "Por favor, insira o token", + "SearchByNaturalLanguage": "Procure por linguagem natural", + "CannotBeEmpty": "Não pode estar vazio" } diff --git a/ui/src/locales/zh.json b/ui/src/locales/zh.json index 662434f7..b2a73e50 100644 --- a/ui/src/locales/zh.json +++ b/ui/src/locales/zh.json @@ -119,5 +119,7 @@ "LoginSuccess": "登录成功", "Login": "登录", "LogoutSuccess": "登出成功", - "InputToken": "请输入 token" + "InputToken": "请输入 token", + "SearchByNaturalLanguage": "自然语言搜索", + "CannotBeEmpty": "不能为空" } diff --git a/ui/src/pages/insightDetail/components/tagVariableSizeList/index.tsx b/ui/src/pages/insightDetail/components/tagVariableSizeList/index.tsx index cbf3b5b1..02014977 100644 --- a/ui/src/pages/insightDetail/components/tagVariableSizeList/index.tsx +++ b/ui/src/pages/insightDetail/components/tagVariableSizeList/index.tsx @@ -68,6 +68,7 @@ const TagVariableSizeList = ({ allTags, containerWidth }: IProps) => { return rows } + // eslint-disable-next-line react-hooks/exhaustive-deps const transformedData = useMemo(() => convertDataToRows(allTags), [allTags]) const itemSize = 30 // lineHeight const itemCount = transformedData?.length // row count diff --git a/ui/src/pages/result/index.tsx b/ui/src/pages/result/index.tsx index 358c8a4d..90f38f53 100644 --- a/ui/src/pages/result/index.tsx +++ b/ui/src/pages/result/index.tsx @@ -1,36 +1,109 @@ import React, { useState, useEffect } from 'react' -import { Pagination, Empty, Divider, Tooltip } from 'antd' +import { + Pagination, + Empty, + Divider, + Tooltip, + Input, + message, + AutoComplete, + Space, +} from 'antd' import { useLocation, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { ClockCircleOutlined } from '@ant-design/icons' +import { ClockCircleOutlined, CloseOutlined } from '@ant-design/icons' import queryString from 'query-string' import classNames from 'classnames' import SqlSearch from '@/components/sqlSearch' import KarporTabs from '@/components/tabs/index' -import { utcDateToLocalDate } from '@/utils/tools' +import { + cacheHistory, + deleteHistoryByItem, + getHistoryList, + utcDateToLocalDate, +} from '@/utils/tools' import Loading from '@/components/loading' import { ICON_MAP } from '@/utils/images' import { searchSqlPrefix, tabsList } from '@/utils/constants' import { useAxios } from '@/utils/request' +// import useDebounce from '@/hooks/useDebounce' import styles from './styles.module.less' +const { Search } = Input +const Option = AutoComplete.Option + +export const CustomDropdown = props => { + const { options } = props + + return ( +
+ {options.map((option, index) => ( +
+ +
+ ))} +
+ ) +} + const Result = () => { const { t } = useTranslation() const location = useLocation() const navigate = useNavigate() const [pageData, setPageData] = useState() - const urlSearchParams = queryString.parse(location.search) - const [searchType, setSearchType] = useState('sql') + const urlSearchParams: any = queryString.parse(location.search) + const [searchType, setSearchType] = useState(urlSearchParams?.pattern) const [searchParams, setSearchParams] = useState({ pageSize: 20, page: 1, query: urlSearchParams?.query || '', total: 0, }) + const [naturalValue, setNaturalValue] = useState('') + const [sqlValue, setSqlValue] = useState('') + const [naturalOptions, setNaturalOptions] = useState( + getHistoryList('naturalHistory') || [], + ) + + function cacheNaturalHistory(key, val) { + const result = cacheHistory(key, val) + setNaturalOptions(result) + } + + useEffect(() => { + if (searchType === 'natural') { + setNaturalValue(urlSearchParams?.query) + handleNaturalSearch(urlSearchParams?.query) + } + if (searchType === 'sql') { + setSqlValue(urlSearchParams?.query) + handleSqlSearch(urlSearchParams?.query) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + if (urlSearchParams?.pattern) { + setSearchType(urlSearchParams?.pattern) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [urlSearchParams?.pattern, urlSearchParams?.query]) function handleTabChange(value: string) { setSearchType(value) + const urlString = queryString.stringify({ + pattern: value, + query: + value === 'natural' ? naturalValue : value === 'sql' ? sqlValue : '', + }) + navigate(`${location?.pathname}?${urlString}`, { replace: true }) } function handleChangePage(page: number, pageSize: number) { @@ -48,11 +121,23 @@ const Result = () => { useEffect(() => { if (response?.success) { - setPageData(response?.data?.items || {}) const objParams = { ...urlSearchParams, + pattern: 'sql', query: response?.successParams?.query || searchParams?.query, } + if (searchType === 'natural') { + let sqlVal + if (response?.data?.sqlQuery?.includes('WHERE')) { + sqlVal = `where ${response?.data?.sqlQuery?.split(' WHERE ')?.[1]}` + } + if (response?.data?.sqlQuery?.includes('where')) { + sqlVal = `where ${response?.data?.sqlQuery?.split(' where ')?.[1]}` + } + setSearchType('sql') + setSqlValue(sqlVal) + } + setPageData(response?.data?.items || {}) const urlString = queryString.stringify(objParams) navigate(`${location?.pathname}?${urlString}`, { replace: true }) } @@ -60,11 +145,19 @@ const Result = () => { }, [response]) function getPageData(params) { + const pattern = + searchType === 'natural' ? 'nl' : searchType === 'sql' ? 'sql' : '' + const query = + searchType === 'natural' + ? params?.query + : searchType === 'sql' + ? `${searchSqlPrefix} ${params?.query}` + : '' refetch({ option: { params: { - query: `${searchSqlPrefix} ${params?.query || searchParams?.query}`, - ...(searchType === 'sql' ? { pattern: 'sql' } : {}), + pattern, + query, page: params?.page || searchParams?.page, pageSize: params?.pageSize || searchParams?.pageSize, }, @@ -78,11 +171,8 @@ const Result = () => { }) } - useEffect(() => { - getPageData(searchParams) - }, []) // eslint-disable-line react-hooks/exhaustive-deps - - function handleSearch(inputValue) { + function handleSqlSearch(inputValue) { + setSqlValue(inputValue) setSearchParams({ ...searchParams, query: inputValue, @@ -128,6 +218,23 @@ const Result = () => { navigate(`/insightDetail/${nav}?${urlParams}`) } + function handleNaturalAutoCompleteChange(val) { + setNaturalValue(val) + } + + function handleNaturalSearch(value) { + if (!value && !naturalValue) { + message.warning(t('CannotBeEmpty')) + return + } + cacheNaturalHistory('naturalHistory', value) + getPageData({ + pageSize: searchParams?.pageSize, + page: 1, + query: value, + }) + } + function renderEmpty() { return (
{ ) } + const handleDelete = val => { + deleteHistoryByItem('naturalHistory', val) + const list = getHistoryList('naturalHistory') || [] + setNaturalOptions(list) + } + + const renderOption = val => { + return ( + + {val} + { + event?.stopPropagation() + handleDelete(val) + }} + /> + + ) + } + + const tmpOptions = naturalOptions?.map(val => ({ + value: val, + label: renderOption(val), + })) + return (
@@ -264,12 +402,39 @@ const Result = () => { onChange={handleTabChange} />
- + {searchType === 'sql' && ( + + )} + {searchType === 'natural' && ( +
+ { + if (option?.value) { + return ( + (option?.value as string) + ?.toUpperCase() + .indexOf(inputValue.toUpperCase()) !== -1 + ) + } + }} + > + + +
+ )}
{loading ? renderLoading() diff --git a/ui/src/pages/search/index.tsx b/ui/src/pages/search/index.tsx index ace766dd..dbe96df2 100644 --- a/ui/src/pages/search/index.tsx +++ b/ui/src/pages/search/index.tsx @@ -15,14 +15,21 @@ */ import React, { useCallback, useState } from 'react' -import { Tag } from 'antd' -import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons' +import { AutoComplete, Input, message, Space, Tag } from 'antd' +import { + DoubleLeftOutlined, + DoubleRightOutlined, + CloseOutlined, +} from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import KarporTabs from '@/components/tabs/index' import logoFull from '@/assets/img/logo-full.svg' import SqlSearch from '@/components/sqlSearch' import { defaultSqlExamples, tabsList } from '@/utils/constants' +import { deleteHistoryByItem, getHistoryList } from '@/utils/tools' + +const { Search } = Input import styles from './styles.module.less' @@ -32,6 +39,9 @@ const SearchPage = () => { const [searchType, setSearchType] = useState('sql') const [sqlEditorValue, setSqlEditorValue] = useState('') const [showAll, setShowAll] = useState(false) + const [naturalOptions, setNaturalOptions] = useState( + getHistoryList('naturalHistory') || [], + ) const toggleTags = () => { setShowAll(!showAll) @@ -45,7 +55,7 @@ const SearchPage = () => { setSqlEditorValue(str) } - const handleSearch = useCallback( + const handleSqlSearch = useCallback( inputValue => { navigate(`/search/result?query=${inputValue}&pattern=sql`) }, @@ -75,6 +85,45 @@ const SearchPage = () => { return renderSqlExamples(sqlExamples) } + function handleNaturalSearch(value) { + if (!value) { + message.warning(t('CannotBeEmpty')) + return + } + navigate(`/search/result?query=${value}&pattern=natural`) + } + + const handleDelete = val => { + deleteHistoryByItem('naturalHistory', val) + const list = getHistoryList('naturalHistory') || [] + setNaturalOptions(list) + } + + const renderOption = val => { + return ( + + {val} + { + event?.stopPropagation() + handleDelete(val) + }} + /> + + ) + } + + const tmpOptions = naturalOptions?.map(val => ({ + value: val, + label: renderOption(val), + })) + return (
@@ -88,12 +137,42 @@ const SearchPage = () => { onChange={handleTabChange} />
-
- -
+ + {searchType === 'sql' && ( +
+ +
+ )} + + {searchType === 'natural' && ( +
+ { + if (option?.value) { + return ( + (option?.value as string) + ?.toUpperCase() + .indexOf(inputValue.toUpperCase()) !== -1 + ) + } + }} + > + + +
+ )} +
{searchType === 'keyword' ? (
@@ -108,7 +187,7 @@ const SearchPage = () => {
- ) : ( + ) : searchType === 'natural' ? null : (
{renderSqlExamples(null)} {!showAll && ( diff --git a/ui/src/utils/constants.ts b/ui/src/utils/constants.ts index 11d42923..931a345b 100644 --- a/ui/src/utils/constants.ts +++ b/ui/src/utils/constants.ts @@ -81,6 +81,10 @@ export const defaultKeywords = [ export const tabsList = [ { label: 'KeywordSearch', value: 'keyword', disabled: true }, { label: 'SQLSearch', value: 'sql' }, + { + label: 'SearchByNaturalLanguage', + value: 'natural', + }, ] export const insightTabsList = [ diff --git a/ui/src/utils/tools.ts b/ui/src/utils/tools.ts index 75e91664..d7ef0494 100644 --- a/ui/src/utils/tools.ts +++ b/ui/src/utils/tools.ts @@ -199,3 +199,26 @@ export function getTextSizeByCanvas( canvas.remove() return width + 2 } + +export function getHistoryList(key) { + return localStorage?.getItem(key) + ? JSON.parse(localStorage?.getItem(key)) + : [] +} + +export function deleteHistoryByItem(key, val: string) { + const lastHistory: any = localStorage.getItem(key) + const tmp = lastHistory ? JSON.parse(lastHistory) : [] + if (tmp?.length > 0 && tmp?.includes(val)) { + const newList = tmp?.filter(item => item !== val) + localStorage.setItem(key, JSON.stringify(newList)) + } +} + +export function cacheHistory(key, val: string) { + const lastHistory: any = localStorage.getItem(key) + const tmp = lastHistory ? JSON.parse(lastHistory) : [] + const newList = [val, ...tmp?.filter(item => item !== val)] + localStorage.setItem(key, JSON.stringify(newList)) + return getHistoryList(key) +} From 0ddb27a98240bfac9453729698a3c5b237f619e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=A6=E7=A6=BB?= <101491308+jueli12@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:44:09 +0800 Subject: [PATCH 04/11] feat: support natural language as a search method (#556) ## What type of PR is this? /kind feature ## What this PR does / why we need it: Support natural language as a search method for kubernetes resources. ## Which issue(s) this PR fixes: Fixes #452 --------- Co-authored-by: ruquanzhao <903264308@qq.com> --- pkg/core/handler/search/search.go | 36 +++++++++- pkg/core/manager/ai/search.go | 43 +++++++++++ pkg/core/manager/ai/util.go | 24 +++++++ pkg/core/manager/ai/util_test.go | 44 ++++++++++++ pkg/core/manager/search/types.go | 1 + pkg/infra/ai/azureopenai.go | 7 +- pkg/infra/ai/huggingface.go | 2 +- pkg/infra/ai/openai.go | 8 +-- pkg/infra/ai/prompts.go | 71 ++++++++++++++++++- pkg/infra/ai/types.go | 7 +- .../search/storage/elasticsearch/search.go | 2 +- pkg/infra/search/storage/types.go | 1 + 12 files changed, 226 insertions(+), 20 deletions(-) create mode 100644 pkg/core/manager/ai/search.go create mode 100644 pkg/core/manager/ai/util.go create mode 100644 pkg/core/manager/ai/util_test.go diff --git a/pkg/core/handler/search/search.go b/pkg/core/handler/search/search.go index 35bb3623..df91bbdb 100644 --- a/pkg/core/handler/search/search.go +++ b/pkg/core/handler/search/search.go @@ -35,7 +35,7 @@ import ( // @Tags search // @Produce json // @Param query query string true "The query to use for search. Required" -// @Param pattern query string true "The search pattern. Can be either sql or dsl. Required" +// @Param pattern query string true "The search pattern. Can be either sql, dsl or nl. Required" // @Param pageSize query string false "The size of the page. Default to 10" // @Param page query string false "The current page to fetch. Default to 1" // @Success 200 {array} runtime.Object "Array of runtime.Object" @@ -66,9 +66,42 @@ func SearchForResource(searchMgr *search.SearchManager, aiMgr *ai.AIManager, sea searchPage = 1 } + query := searchQuery + + if searchPattern == storage.NLPatternType { + //logger.Info(searchQuery) + res, err := aiMgr.ConvertTextToSQL(searchQuery) + if err != nil { + handler.FailureRender(ctx, w, r, err) + return + } + searchQuery = res + } + + //logger.Info(searchQuery) logger.Info("Searching for resources...", "page", searchPage, "pageSize", searchPageSize) res, err := searchStorage.Search(ctx, searchQuery, searchPattern, &storage.Pagination{Page: searchPage, PageSize: searchPageSize}) + if err != nil { + if searchPattern == storage.NLPatternType { + //logger.Info(err.Error()) + fixedQuery, fixErr := aiMgr.FixSQL(query, searchQuery, err.Error()) + if fixErr != nil { + handler.FailureRender(ctx, w, r, err) + return + } + searchQuery = fixedQuery + res, err = searchStorage.Search(ctx, searchQuery, searchPattern, &storage.Pagination{Page: searchPage, PageSize: searchPageSize}) + if err != nil { + handler.FailureRender(ctx, w, r, err) + return + } + } else { + handler.FailureRender(ctx, w, r, err) + return + } + } + if err != nil { handler.FailureRender(ctx, w, r, err) return @@ -83,6 +116,7 @@ func SearchForResource(searchMgr *search.SearchManager, aiMgr *ai.AIManager, sea Object: unObj, }) } + rt.SQLQuery = searchQuery rt.Total = res.Total rt.CurrentPage = searchPage rt.PageSize = searchPageSize diff --git a/pkg/core/manager/ai/search.go b/pkg/core/manager/ai/search.go new file mode 100644 index 00000000..4c814518 --- /dev/null +++ b/pkg/core/manager/ai/search.go @@ -0,0 +1,43 @@ +// Copyright The Karpor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ai + +import ( + "context" + "fmt" + "github.com/KusionStack/karpor/pkg/infra/ai" +) + +// ConvertTextToSQL converts natural language text to an SQL query +func (a *AIManager) ConvertTextToSQL(query string) (string, error) { + servicePrompt := ai.ServicePromptMap[ai.Text2sqlType] + prompt := fmt.Sprintf(servicePrompt, query) + res, err := a.client.Generate(context.Background(), prompt) + if err != nil { + return "", err + } + return ExtractSelectSQL(res), nil +} + +// FixSQL fix the error SQL +func (a *AIManager) FixSQL(sql string, query string, error string) (string, error) { + servicePrompt := ai.ServicePromptMap[ai.SqlFixType] + prompt := fmt.Sprintf(servicePrompt, query, sql, error) + res, err := a.client.Generate(context.Background(), prompt) + if err != nil { + return "", err + } + return ExtractSelectSQL(res), nil +} diff --git a/pkg/core/manager/ai/util.go b/pkg/core/manager/ai/util.go new file mode 100644 index 00000000..effe2be3 --- /dev/null +++ b/pkg/core/manager/ai/util.go @@ -0,0 +1,24 @@ +// Copyright The Karpor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ai + +import "regexp" + +// ExtractSelectSQL extracts SQL statements that start with "SELECT * FROM" +func ExtractSelectSQL(sql string) string { + res := regexp.MustCompile(`(?i)SELECT \* FROM [^;]+`) + match := res.FindString(sql) + return match +} diff --git a/pkg/core/manager/ai/util_test.go b/pkg/core/manager/ai/util_test.go new file mode 100644 index 00000000..d356d0b4 --- /dev/null +++ b/pkg/core/manager/ai/util_test.go @@ -0,0 +1,44 @@ +// Copyright The Karpor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ai + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +// TestExtractSelectSQL tests the correctness of the ExtractSelectSQL function. +func TestExtractSelectSQL(t *testing.T) { + testCases := []struct { + name string + sql string + expected string + }{ + { + name: "NormalCase", + sql: "Q: 所有kind=namespace " + + "Schema_links: [kind, namespace] " + + "SQL: select * from resources where kind='namespace';", + expected: "select * from resources where kind='namespace'", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := ExtractSelectSQL(tc.sql) + require.Equal(t, tc.expected, actual) + }) + } +} diff --git a/pkg/core/manager/search/types.go b/pkg/core/manager/search/types.go index a6cc5c99..0117e73e 100644 --- a/pkg/core/manager/search/types.go +++ b/pkg/core/manager/search/types.go @@ -34,6 +34,7 @@ type UniResource struct { type UniResourceList struct { metav1.TypeMeta Items []UniResource `json:"items"` + SQLQuery string `json:"sqlQuery"` Total int `json:"total"` CurrentPage int `json:"currentPage"` PageSize int `json:"pageSize"` diff --git a/pkg/infra/ai/azureopenai.go b/pkg/infra/ai/azureopenai.go index 37f9762e..d48aae38 100644 --- a/pkg/infra/ai/azureopenai.go +++ b/pkg/infra/ai/azureopenai.go @@ -47,16 +47,11 @@ func (c *AzureAIClient) Configure(cfg AIConfig) error { return nil } -func (c *AzureAIClient) Generate(ctx context.Context, prompt string, serviceType string) (string, error) { - servicePrompt := ServicePromptMap[serviceType] +func (c *AzureAIClient) Generate(ctx context.Context, prompt string) (string, error) { resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ Model: c.model, Messages: []openai.ChatCompletionMessage{ - { - Role: openai.ChatMessageRoleSystem, - Content: servicePrompt, - }, { Role: openai.ChatMessageRoleUser, Content: prompt, diff --git a/pkg/infra/ai/huggingface.go b/pkg/infra/ai/huggingface.go index edd5e6eb..b61e28fe 100644 --- a/pkg/infra/ai/huggingface.go +++ b/pkg/infra/ai/huggingface.go @@ -34,7 +34,7 @@ func (c *HuggingfaceClient) Configure(cfg AIConfig) error { return nil } -func (c *HuggingfaceClient) Generate(ctx context.Context, prompt string, serviceType string) (string, error) { +func (c *HuggingfaceClient) Generate(ctx context.Context, prompt string) (string, error) { resp, err := c.client.TextGeneration(ctx, &huggingface.TextGenerationRequest{ Inputs: prompt, Parameters: huggingface.TextGenerationParameters{ diff --git a/pkg/infra/ai/openai.go b/pkg/infra/ai/openai.go index 82ef8c14..8de36a2d 100644 --- a/pkg/infra/ai/openai.go +++ b/pkg/infra/ai/openai.go @@ -48,16 +48,10 @@ func (c *OpenAIClient) Configure(cfg AIConfig) error { return nil } -func (c *OpenAIClient) Generate(ctx context.Context, prompt string, serviceType string) (string, error) { - servicePrompt := ServicePromptMap[serviceType] - +func (c *OpenAIClient) Generate(ctx context.Context, prompt string) (string, error) { resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ Model: c.model, Messages: []openai.ChatCompletionMessage{ - { - Role: openai.ChatMessageRoleSystem, - Content: servicePrompt, - }, { Role: openai.ChatMessageRoleUser, Content: prompt, diff --git a/pkg/infra/ai/prompts.go b/pkg/infra/ai/prompts.go index 7d4479a5..06feceae 100644 --- a/pkg/infra/ai/prompts.go +++ b/pkg/infra/ai/prompts.go @@ -16,10 +16,75 @@ package ai const ( default_prompt = "You are a helpful assistant." - search_prompt = "You are an AI specialized in writing SQL queries. Please provide the SQL statement as a formatted string." + + text2sql_prompt = ` + You are an AI specialized in writing SQL queries. + Please convert the text %s to sql. + The output tokens only need to give the SQL first, the other thought process please do not give. + The SQL should begin with "select * from" and end with ";". + + 1. The database now only supports one table resources. + + Table resources, columns = [cluster, apiVersion, kind, + namespace, name, creationTimestamp, deletionTimestamp, ownerReferences, + resourceVersion, labels.[key], annotations.[key], content] + + 2. find the schema_links for generating SQL queries for each question based on the database schema. + + Follow are some examples. + + Q: find the kind which is not equal to pod + A: Let’s think step by step. In the question "find the kind column which is not equal to pod", we are asked: + "find the kind" so we need column = [kind] + Based on the columns, the set of possible cell values are = [pod]. + So the Schema_links are: + Schema_links: [kind, pod] + + Q: find the kind Deployment which created before January 1, 2024, at 18:00:00 + A: Let’s think step by step. In the question "find the kind Deployment which created before January 1, 2024, at 18:00:00", we are asked: + "find the kind Deployment" so we need column = [kind] + "created before" so we need column = [creationTimestamp] + Based on the columns, the set of possible cell values are = [Deployment, 2024-01-01T18:00:00Z]. + So the Schema_links are: + Schema_links: [kind, creationTimestamp, Deployment, 2024-01-01T18:00:00Z] + + 3. Use the the schema links to generate the SQL queries for each of the questions. + + Follow are some examples. + + Q: find the kind which is not equal to pod + Schema_links: [kind, pod] + SQL: select * from resources where kind!='Pod'; + + Q: find the kind Deployment which created before January 1, 2024, at 18:00:00 + Schema_links: [kind, creationTimestamp, Deployment, 2024-01-01T18:00:00Z] + SQL: select * from resources where kind='Deployment' and creationTimestamp < '2024-01-01T18:00:00Z'; + + Q: find the namespace which does not contain banan + Schema_links: [namespace, banan] + SQL: select * from resources where namespace notlike 'banan_'; + + Please convert the text to sql. + ` + + sql_fix_prompt = ` + You are an AI specialized in writing SQL queries. + Please convert the text %s to sql. + The SQL should begin with "select * from". + + The database now only supports one table resources. + + Table resources, columns = [cluster, apiVersion, kind, + namespace, name, creationTimestamp, deletionTimestamp, ownerReferences, + resourceVersion, labels.[key], annotations.[key], content] + + After we executed SQL %s, we observed the following error %s. + Please fix the SQL. + ` ) var ServicePromptMap = map[string]string{ - "default": default_prompt, - "search": search_prompt, + "default": default_prompt, + "Text2sql": text2sql_prompt, + "SqlFix": sql_fix_prompt, } diff --git a/pkg/infra/ai/types.go b/pkg/infra/ai/types.go index 0d2025eb..4437506e 100644 --- a/pkg/infra/ai/types.go +++ b/pkg/infra/ai/types.go @@ -25,6 +25,11 @@ const ( OpenAIProvider = "openai" ) +const ( + Text2sqlType = "Text2sql" + SqlFixType = "SqlFix" +) + var clients = map[string]AIProvider{ AzureProvider: &AzureAIClient{}, HuggingFaceProvider: &HuggingfaceClient{}, @@ -37,7 +42,7 @@ type AIProvider interface { Configure(config AIConfig) error // Generate generates a response from the AI service based on // the provided prompt and service type. - Generate(ctx context.Context, prompt string, serviceType string) (string, error) + Generate(ctx context.Context, prompt string) (string, error) } // AIConfig represents the configuration settings for an AI client. diff --git a/pkg/infra/search/storage/elasticsearch/search.go b/pkg/infra/search/storage/elasticsearch/search.go index 2118a68d..86245eab 100644 --- a/pkg/infra/search/storage/elasticsearch/search.go +++ b/pkg/infra/search/storage/elasticsearch/search.go @@ -47,7 +47,7 @@ func (s *Storage) Search(ctx context.Context, queryStr string, patternType strin if err != nil { return nil, errors.Wrap(err, "search by DSL failed") } - case storage.SQLPatternType: + case storage.SQLPatternType, storage.NLPatternType: sr, err = s.searchBySQL(ctx, queryStr, pagination) if err != nil { return nil, errors.Wrap(err, "search by SQL failed") diff --git a/pkg/infra/search/storage/types.go b/pkg/infra/search/storage/types.go index 7cb6c73b..c90a44c1 100644 --- a/pkg/infra/search/storage/types.go +++ b/pkg/infra/search/storage/types.go @@ -31,6 +31,7 @@ import ( const ( Equals = "=" + NLPatternType = "nl" DSLPatternType = "dsl" SQLPatternType = "sql" ) From dbf281e8b8d41983095333789b4f64cfa84af1ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=A6=E7=A6=BB?= <101491308+jueli12@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:23:38 +0800 Subject: [PATCH 05/11] feat: change the AI command line argument to optional (#617) ## What type of PR is this? /kind feature ## What this PR does / why we need it: change the AI command line argument to optional ## Which issue(s) this PR fixes: Fixes #551 --- cmd/karpor/app/options/ai.go | 36 +++++++++++++++---------------- pkg/core/handler/search/search.go | 10 ++++----- pkg/core/manager/ai/manager.go | 13 ++++++++++- pkg/core/manager/ai/types.go | 21 ++++++++++++++++++ pkg/core/route/route.go | 9 ++++++-- pkg/infra/ai/azureopenai.go | 4 ---- pkg/infra/ai/openai.go | 3 --- pkg/infra/ai/types.go | 12 +++++------ pkg/kubernetes/registry/types.go | 12 +++++------ 9 files changed, 75 insertions(+), 45 deletions(-) create mode 100644 pkg/core/manager/ai/types.go diff --git a/cmd/karpor/app/options/ai.go b/cmd/karpor/app/options/ai.go index f47d5592..2603e5b2 100644 --- a/cmd/karpor/app/options/ai.go +++ b/cmd/karpor/app/options/ai.go @@ -20,12 +20,12 @@ import ( ) type AIOptions struct { - Backend string - AuthToken string - BaseURL string - Model string - Temperature float32 - TopP float32 + AIBackend string + AIAuthToken string + AIBaseURL string + AIModel string + AITemperature float32 + AITopP float32 } const ( @@ -45,12 +45,12 @@ func (o *AIOptions) Validate() []error { func (o *AIOptions) ApplyTo(config *registry.ExtraConfig) error { // Apply the AIOptions to the provided config - config.Backend = o.Backend - config.AuthToken = o.AuthToken - config.BaseURL = o.BaseURL - config.Model = o.Model - config.Temperature = o.Temperature - config.TopP = o.TopP + config.AIBackend = o.AIBackend + config.AIAuthToken = o.AIAuthToken + config.AIBaseURL = o.AIBaseURL + config.AIModel = o.AIModel + config.AITemperature = o.AITemperature + config.AITopP = o.AITopP return nil } @@ -60,10 +60,10 @@ func (o *AIOptions) AddFlags(fs *pflag.FlagSet) { return } - fs.StringVar(&o.Backend, "ai-backend", defaultBackend, "The ai backend") - fs.StringVar(&o.AuthToken, "ai-auth-token", "", "The ai auth token") - fs.StringVar(&o.BaseURL, "ai-base-url", "", "The ai base url") - fs.StringVar(&o.Model, "ai-model", defaultModel, "The ai model") - fs.Float32Var(&o.Temperature, "ai-temperature", defaultTemperature, "The ai temperature") - fs.Float32Var(&o.TopP, "ai-top-p", defaultTopP, "The ai top-p") + fs.StringVar(&o.AIBackend, "ai-backend", defaultBackend, "The ai backend") + fs.StringVar(&o.AIAuthToken, "ai-auth-token", "", "The ai auth token") + fs.StringVar(&o.AIBaseURL, "ai-base-url", "", "The ai base url") + fs.StringVar(&o.AIModel, "ai-model", defaultModel, "The ai model") + fs.Float32Var(&o.AITemperature, "ai-temperature", defaultTemperature, "The ai temperature") + fs.Float32Var(&o.AITopP, "ai-top-p", defaultTopP, "The ai top-p") } diff --git a/pkg/core/handler/search/search.go b/pkg/core/handler/search/search.go index df91bbdb..db94a495 100644 --- a/pkg/core/handler/search/search.go +++ b/pkg/core/handler/search/search.go @@ -52,8 +52,6 @@ func SearchForResource(searchMgr *search.SearchManager, aiMgr *ai.AIManager, sea ctx := r.Context() logger := ctxutil.GetLogger(ctx) - //res, nil := aiMgr.ConvertTextToSQL("搜索集群cluster中kind为namespace的") - // Extract URL query parameters with default value searchQuery := r.URL.Query().Get("query") searchPattern := r.URL.Query().Get("pattern") @@ -69,7 +67,11 @@ func SearchForResource(searchMgr *search.SearchManager, aiMgr *ai.AIManager, sea query := searchQuery if searchPattern == storage.NLPatternType { - //logger.Info(searchQuery) + if err := ai.CheckAIManager(aiMgr); err != nil { + handler.FailureRender(ctx, w, r, err) + return + } + res, err := aiMgr.ConvertTextToSQL(searchQuery) if err != nil { handler.FailureRender(ctx, w, r, err) @@ -78,13 +80,11 @@ func SearchForResource(searchMgr *search.SearchManager, aiMgr *ai.AIManager, sea searchQuery = res } - //logger.Info(searchQuery) logger.Info("Searching for resources...", "page", searchPage, "pageSize", searchPageSize) res, err := searchStorage.Search(ctx, searchQuery, searchPattern, &storage.Pagination{Page: searchPage, PageSize: searchPageSize}) if err != nil { if searchPattern == storage.NLPatternType { - //logger.Info(err.Error()) fixedQuery, fixErr := aiMgr.FixSQL(query, searchQuery, err.Error()) if fixErr != nil { handler.FailureRender(ctx, w, r, err) diff --git a/pkg/core/manager/ai/manager.go b/pkg/core/manager/ai/manager.go index 699a4bc8..2333abfa 100644 --- a/pkg/core/manager/ai/manager.go +++ b/pkg/core/manager/ai/manager.go @@ -25,7 +25,10 @@ type AIManager struct { // NewAIManager returns a new AIManager object func NewAIManager(c registry.ExtraConfig) (*AIManager, error) { - aiClient := ai.NewClient(c.Backend) + if c.AIAuthToken == "" { + return nil, ErrMissingAuthToken + } + aiClient := ai.NewClient(c.AIBackend) if err := aiClient.Configure(ai.ConvertToAIConfig(c)); err != nil { return nil, err } @@ -34,3 +37,11 @@ func NewAIManager(c registry.ExtraConfig) (*AIManager, error) { client: aiClient, }, nil } + +// CheckAIManager check if the AI manager is created +func CheckAIManager(aiMgr *AIManager) error { + if aiMgr == nil { + return ErrMissingAuthToken + } + return nil +} diff --git a/pkg/core/manager/ai/types.go b/pkg/core/manager/ai/types.go new file mode 100644 index 00000000..39e8b501 --- /dev/null +++ b/pkg/core/manager/ai/types.go @@ -0,0 +1,21 @@ +// Copyright The Karpor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ai + +import "errors" + +var ( + ErrMissingAuthToken = errors.New("auth token is required") +) diff --git a/pkg/core/route/route.go b/pkg/core/route/route.go index 50401d67..fffc06ef 100644 --- a/pkg/core/route/route.go +++ b/pkg/core/route/route.go @@ -15,8 +15,8 @@ package route import ( + "errors" "expvar" - docs "github.com/KusionStack/karpor/api/openapispec" authnhandler "github.com/KusionStack/karpor/pkg/core/handler/authn" clusterhandler "github.com/KusionStack/karpor/pkg/core/handler/cluster" @@ -44,6 +44,7 @@ import ( "github.com/go-chi/chi/v5/middleware" httpswagger "github.com/swaggo/http-swagger/v2" genericapiserver "k8s.io/apiserver/pkg/server" + "k8s.io/klog/v2" ) // NewCoreRoute creates and configures an instance of chi.Mux with the given @@ -92,7 +93,11 @@ func NewCoreRoute( } aiMgr, err := aimanager.NewAIManager(*extraConfig) if err != nil { - return nil, err + if errors.Is(err, aimanager.ErrMissingAuthToken) { + klog.Warning("Auth token is empty.") + } else { + return nil, err + } } clusterMgr := clustermanager.NewClusterManager() diff --git a/pkg/infra/ai/azureopenai.go b/pkg/infra/ai/azureopenai.go index d48aae38..235b7205 100644 --- a/pkg/infra/ai/azureopenai.go +++ b/pkg/infra/ai/azureopenai.go @@ -27,9 +27,6 @@ type AzureAIClient struct { } func (c *AzureAIClient) Configure(cfg AIConfig) error { - if cfg.AuthToken == "" { - return errors.New("auth token was not provided") - } if cfg.BaseURL == "" { return errors.New("base url was not provided") } @@ -48,7 +45,6 @@ func (c *AzureAIClient) Configure(cfg AIConfig) error { } func (c *AzureAIClient) Generate(ctx context.Context, prompt string) (string, error) { - resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ Model: c.model, Messages: []openai.ChatCompletionMessage{ diff --git a/pkg/infra/ai/openai.go b/pkg/infra/ai/openai.go index 8de36a2d..9ec3f98e 100644 --- a/pkg/infra/ai/openai.go +++ b/pkg/infra/ai/openai.go @@ -28,9 +28,6 @@ type OpenAIClient struct { } func (c *OpenAIClient) Configure(cfg AIConfig) error { - if cfg.AuthToken == "" { - return errors.New("auth token was not provided") - } defaultConfig := openai.DefaultConfig(cfg.AuthToken) if cfg.BaseURL != "" { defaultConfig.BaseURL = cfg.BaseURL diff --git a/pkg/infra/ai/types.go b/pkg/infra/ai/types.go index 4437506e..e18ad67c 100644 --- a/pkg/infra/ai/types.go +++ b/pkg/infra/ai/types.go @@ -57,12 +57,12 @@ type AIConfig struct { func ConvertToAIConfig(c registry.ExtraConfig) AIConfig { return AIConfig{ - Name: c.Backend, - AuthToken: c.AuthToken, - BaseURL: c.BaseURL, - Model: c.Model, - Temperature: c.Temperature, - TopP: c.TopP, + Name: c.AIBackend, + AuthToken: c.AIAuthToken, + BaseURL: c.AIBaseURL, + Model: c.AIModel, + Temperature: c.AITemperature, + TopP: c.AITopP, } } diff --git a/pkg/kubernetes/registry/types.go b/pkg/kubernetes/registry/types.go index 51f4fa91..36549eeb 100644 --- a/pkg/kubernetes/registry/types.go +++ b/pkg/kubernetes/registry/types.go @@ -47,10 +47,10 @@ type ExtraConfig struct { ExtendExpiration bool // AI configs - Backend string - AuthToken string - BaseURL string - Model string - Temperature float32 - TopP float32 + AIBackend string + AIAuthToken string + AIBaseURL string + AIModel string + AITemperature float32 + AITopP float32 } From f8a83c02e4f0ea7f7d0415f5f6f293eddf13c80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=A6=E7=A6=BB?= <101491308+jueli12@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:49:01 +0800 Subject: [PATCH 06/11] feat: filter invalid queries for natural language search (#623) ## What type of PR is this? /kind feature ## What this PR does / why we need it: Filter invalid queries for natural language search. ## Which issue(s) this PR fixes: Fixes #556 --- pkg/core/handler/search/search.go | 1 - pkg/core/manager/ai/search.go | 3 +++ pkg/core/manager/ai/types.go | 1 + pkg/core/manager/ai/util.go | 10 +++++++++- pkg/core/manager/ai/util_test.go | 27 +++++++++++++++++++++++++++ pkg/infra/ai/prompts.go | 26 +++++++++++++++++++++----- 6 files changed, 61 insertions(+), 7 deletions(-) diff --git a/pkg/core/handler/search/search.go b/pkg/core/handler/search/search.go index db94a495..0bc58d17 100644 --- a/pkg/core/handler/search/search.go +++ b/pkg/core/handler/search/search.go @@ -71,7 +71,6 @@ func SearchForResource(searchMgr *search.SearchManager, aiMgr *ai.AIManager, sea handler.FailureRender(ctx, w, r, err) return } - res, err := aiMgr.ConvertTextToSQL(searchQuery) if err != nil { handler.FailureRender(ctx, w, r, err) diff --git a/pkg/core/manager/ai/search.go b/pkg/core/manager/ai/search.go index 4c814518..62589f6b 100644 --- a/pkg/core/manager/ai/search.go +++ b/pkg/core/manager/ai/search.go @@ -28,6 +28,9 @@ func (a *AIManager) ConvertTextToSQL(query string) (string, error) { if err != nil { return "", err } + if IsInvalidQuery(res) { + return "", ErrInvalidQuery + } return ExtractSelectSQL(res), nil } diff --git a/pkg/core/manager/ai/types.go b/pkg/core/manager/ai/types.go index 39e8b501..eedd6285 100644 --- a/pkg/core/manager/ai/types.go +++ b/pkg/core/manager/ai/types.go @@ -18,4 +18,5 @@ import "errors" var ( ErrMissingAuthToken = errors.New("auth token is required") + ErrInvalidQuery = errors.New("query is invalid") ) diff --git a/pkg/core/manager/ai/util.go b/pkg/core/manager/ai/util.go index effe2be3..275849b0 100644 --- a/pkg/core/manager/ai/util.go +++ b/pkg/core/manager/ai/util.go @@ -14,7 +14,15 @@ package ai -import "regexp" +import ( + "regexp" + "strings" +) + +// IsInvalidQuery check if the query is invalid +func IsInvalidQuery(sql string) bool { + return strings.Contains(strings.ToLower(sql), "error") +} // ExtractSelectSQL extracts SQL statements that start with "SELECT * FROM" func ExtractSelectSQL(sql string) string { diff --git a/pkg/core/manager/ai/util_test.go b/pkg/core/manager/ai/util_test.go index d356d0b4..fb5aa047 100644 --- a/pkg/core/manager/ai/util_test.go +++ b/pkg/core/manager/ai/util_test.go @@ -42,3 +42,30 @@ func TestExtractSelectSQL(t *testing.T) { }) } } + +// TestIsInvalidQuery tests the IsInvalidQuery function. +func TestIsInvalidQuery(t *testing.T) { + testCases := []struct { + name string + sql string + expected bool + }{ + { + name: "ValidQueryWithoutError", + sql: "select * from resources where kind='namespace';", + expected: false, + }, + { + name: "InvalidQuery", + sql: "Error", + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := IsInvalidQuery(tc.sql) + require.Equal(t, tc.expected, actual) + }) + } +} diff --git a/pkg/infra/ai/prompts.go b/pkg/infra/ai/prompts.go index 06feceae..1ef63cb8 100644 --- a/pkg/infra/ai/prompts.go +++ b/pkg/infra/ai/prompts.go @@ -20,6 +20,7 @@ const ( text2sql_prompt = ` You are an AI specialized in writing SQL queries. Please convert the text %s to sql. + If the text is not accurate enough, please output "Error". The output tokens only need to give the SQL first, the other thought process please do not give. The SQL should begin with "select * from" and end with ";". @@ -30,23 +31,33 @@ const ( resourceVersion, labels.[key], annotations.[key], content] 2. find the schema_links for generating SQL queries for each question based on the database schema. + If there are Chinese expressions, please translate them into English. Follow are some examples. Q: find the kind which is not equal to pod A: Let’s think step by step. In the question "find the kind column which is not equal to pod", we are asked: - "find the kind" so we need column = [kind] + "find the kind" so we need column = [kind]. Based on the columns, the set of possible cell values are = [pod]. So the Schema_links are: Schema_links: [kind, pod] Q: find the kind Deployment which created before January 1, 2024, at 18:00:00 A: Let’s think step by step. In the question "find the kind Deployment which created before January 1, 2024, at 18:00:00", we are asked: - "find the kind Deployment" so we need column = [kind] - "created before" so we need column = [creationTimestamp] + "find the kind Deployment" so we need column = [kind]. + "created before" so we need column = [creationTimestamp]. Based on the columns, the set of possible cell values are = [Deployment, 2024-01-01T18:00:00Z]. So the Schema_links are: - Schema_links: [kind, creationTimestamp, Deployment, 2024-01-01T18:00:00Z] + Schema_links: [[kind, Deployment], [creationTimestamp, 2024-01-01T18:00:00Z]] + + Q: find the kind Namespace which which created + A: Let’s think step by step. In the question "find the kind", we are asked: + "find the kind Namespace " so we need column = [kind] + "created before" so we need column = [creationTimestamp] + Based on the columns, the set of possible cell values are = [kind, creationTimestamp]. + There is no creationTimestamp corresponding cell values, so the text is not accurate enough. + So the Schema_links are: + Schema_links: error 3. Use the the schema links to generate the SQL queries for each of the questions. @@ -57,14 +68,19 @@ const ( SQL: select * from resources where kind!='Pod'; Q: find the kind Deployment which created before January 1, 2024, at 18:00:00 - Schema_links: [kind, creationTimestamp, Deployment, 2024-01-01T18:00:00Z] + Schema_links: [[kind, Deployment], [creationTimestamp, 2024-01-01T18:00:00Z]] SQL: select * from resources where kind='Deployment' and creationTimestamp < '2024-01-01T18:00:00Z'; Q: find the namespace which does not contain banan Schema_links: [namespace, banan] SQL: select * from resources where namespace notlike 'banan_'; + Q: find the kind Namespace which which created + Schema_links: error + Error; + Please convert the text to sql. + If the text is not accurate enough, please output "Error". ` sql_fix_prompt = ` From db621ffa621469e0953e8b659b77bc1c21115fd7 Mon Sep 17 00:00:00 2001 From: ruquanzhao <903264308@qq.com> Date: Fri, 22 Nov 2024 15:55:47 +0800 Subject: [PATCH 07/11] fix: recover accidentally deleted code --- ui/src/pages/result/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/result/index.tsx b/ui/src/pages/result/index.tsx index a81e78b1..fa9297dc 100644 --- a/ui/src/pages/result/index.tsx +++ b/ui/src/pages/result/index.tsx @@ -1,4 +1,4 @@ - +import React, { useState, useEffect } from 'react' import { Pagination, Empty, From 089cdc67428d585e9de1c795950da14c1f900ace Mon Sep 17 00:00:00 2001 From: ruquanzhao <903264308@qq.com> Date: Mon, 25 Nov 2024 20:23:12 +0800 Subject: [PATCH 08/11] fix: fix typo --- pkg/infra/ai/prompts.go | 3 +-- pkg/kubernetes/registry/types.go | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/infra/ai/prompts.go b/pkg/infra/ai/prompts.go index 1ef63cb8..92559906 100644 --- a/pkg/infra/ai/prompts.go +++ b/pkg/infra/ai/prompts.go @@ -19,7 +19,7 @@ const ( text2sql_prompt = ` You are an AI specialized in writing SQL queries. - Please convert the text %s to sql. + Please convert the text :"%s" to sql. If the text is not accurate enough, please output "Error". The output tokens only need to give the SQL first, the other thought process please do not give. The SQL should begin with "select * from" and end with ";". @@ -80,7 +80,6 @@ const ( Error; Please convert the text to sql. - If the text is not accurate enough, please output "Error". ` sql_fix_prompt = ` diff --git a/pkg/kubernetes/registry/types.go b/pkg/kubernetes/registry/types.go index d2cc214c..45095f1f 100644 --- a/pkg/kubernetes/registry/types.go +++ b/pkg/kubernetes/registry/types.go @@ -40,8 +40,8 @@ type ExtraConfig struct { ElasticSearchPassword string ReadOnlyMode bool GithubBadge bool - EnableRBAC bool - + EnableRBAC bool + // ServiceAccount configs ServiceAccountIssuer serviceaccount.TokenGenerator ServiceAccountMaxExpiration time.Duration From 137b5518b4dc5ec181ad7c53acf7b6fb120ae593 Mon Sep 17 00:00:00 2001 From: ruquanzhao <903264308@qq.com> Date: Tue, 26 Nov 2024 16:45:27 +0800 Subject: [PATCH 09/11] fix: fix go-lint --- pkg/core/manager/ai/search.go | 7 ++++--- pkg/infra/ai/azureopenai.go | 1 + pkg/infra/ai/huggingface.go | 2 +- pkg/infra/ai/prompts.go | 18 +++++++++--------- pkg/infra/ai/types.go | 3 ++- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/pkg/core/manager/ai/search.go b/pkg/core/manager/ai/search.go index 62589f6b..7f675a7d 100644 --- a/pkg/core/manager/ai/search.go +++ b/pkg/core/manager/ai/search.go @@ -17,6 +17,7 @@ package ai import ( "context" "fmt" + "github.com/KusionStack/karpor/pkg/infra/ai" ) @@ -35,9 +36,9 @@ func (a *AIManager) ConvertTextToSQL(query string) (string, error) { } // FixSQL fix the error SQL -func (a *AIManager) FixSQL(sql string, query string, error string) (string, error) { - servicePrompt := ai.ServicePromptMap[ai.SqlFixType] - prompt := fmt.Sprintf(servicePrompt, query, sql, error) +func (a *AIManager) FixSQL(sql string, query string, sqlErr string) (string, error) { + servicePrompt := ai.ServicePromptMap[ai.SQLFixType] + prompt := fmt.Sprintf(servicePrompt, query, sql, sqlErr) res, err := a.client.Generate(context.Background(), prompt) if err != nil { return "", err diff --git a/pkg/infra/ai/azureopenai.go b/pkg/infra/ai/azureopenai.go index 235b7205..70c80b7c 100644 --- a/pkg/infra/ai/azureopenai.go +++ b/pkg/infra/ai/azureopenai.go @@ -17,6 +17,7 @@ package ai import ( "context" "errors" + "github.com/sashabaranov/go-openai" ) diff --git a/pkg/infra/ai/huggingface.go b/pkg/infra/ai/huggingface.go index b61e28fe..573e2677 100644 --- a/pkg/infra/ai/huggingface.go +++ b/pkg/infra/ai/huggingface.go @@ -16,6 +16,7 @@ package ai import ( "context" + "github.com/hupe1980/go-huggingface" ) @@ -50,5 +51,4 @@ func (c *HuggingfaceClient) Generate(ctx context.Context, prompt string) (string return "", err } return resp[0].GeneratedText[len(prompt):], nil - } diff --git a/pkg/infra/ai/prompts.go b/pkg/infra/ai/prompts.go index 92559906..7d2ba872 100644 --- a/pkg/infra/ai/prompts.go +++ b/pkg/infra/ai/prompts.go @@ -15,11 +15,11 @@ package ai const ( - default_prompt = "You are a helpful assistant." + defaultPrompt = "You are a helpful assistant." - text2sql_prompt = ` + text2sqlPrompt = ` You are an AI specialized in writing SQL queries. - Please convert the text :"%s" to sql. + Please convert the text: "%s" to sql. If the text is not accurate enough, please output "Error". The output tokens only need to give the SQL first, the other thought process please do not give. The SQL should begin with "select * from" and end with ";". @@ -82,9 +82,9 @@ const ( Please convert the text to sql. ` - sql_fix_prompt = ` + sqlFixPrompt = ` You are an AI specialized in writing SQL queries. - Please convert the text %s to sql. + Please convert the text: "%s" to sql. The SQL should begin with "select * from". The database now only supports one table resources. @@ -93,13 +93,13 @@ const ( namespace, name, creationTimestamp, deletionTimestamp, ownerReferences, resourceVersion, labels.[key], annotations.[key], content] - After we executed SQL %s, we observed the following error %s. + After we executed SQL: "%s", we observed the following error "%s". Please fix the SQL. ` ) var ServicePromptMap = map[string]string{ - "default": default_prompt, - "Text2sql": text2sql_prompt, - "SqlFix": sql_fix_prompt, + "default": defaultPrompt, + "Text2sql": text2sqlPrompt, + "SqlFix": sqlFixPrompt, } diff --git a/pkg/infra/ai/types.go b/pkg/infra/ai/types.go index e18ad67c..d4321ddf 100644 --- a/pkg/infra/ai/types.go +++ b/pkg/infra/ai/types.go @@ -16,6 +16,7 @@ package ai import ( "context" + "github.com/KusionStack/karpor/pkg/kubernetes/registry" ) @@ -27,7 +28,7 @@ const ( const ( Text2sqlType = "Text2sql" - SqlFixType = "SqlFix" + SQLFixType = "SqlFix" ) var clients = map[string]AIProvider{ From ab1fc002489282e863364dd109d810349b2a5e5f Mon Sep 17 00:00:00 2001 From: ruquanzhao <903264308@qq.com> Date: Tue, 26 Nov 2024 16:54:07 +0800 Subject: [PATCH 10/11] fix: fix go-lint commit2 --- pkg/core/handler/search/search.go | 3 ++- pkg/core/manager/ai/util_test.go | 3 ++- pkg/infra/ai/huggingface.go | 2 +- pkg/infra/ai/openai.go | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/core/handler/search/search.go b/pkg/core/handler/search/search.go index edccbe9a..2954cbd8 100644 --- a/pkg/core/handler/search/search.go +++ b/pkg/core/handler/search/search.go @@ -15,10 +15,11 @@ package search import ( - "github.com/KusionStack/karpor/pkg/core/manager/ai" "net/http" "strconv" + "github.com/KusionStack/karpor/pkg/core/manager/ai" + "github.com/KusionStack/karpor/pkg/core/handler" "github.com/KusionStack/karpor/pkg/core/manager/search" "github.com/KusionStack/karpor/pkg/infra/search/storage" diff --git a/pkg/core/manager/ai/util_test.go b/pkg/core/manager/ai/util_test.go index fb5aa047..e1e2cd85 100644 --- a/pkg/core/manager/ai/util_test.go +++ b/pkg/core/manager/ai/util_test.go @@ -15,8 +15,9 @@ package ai import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) // TestExtractSelectSQL tests the correctness of the ExtractSelectSQL function. diff --git a/pkg/infra/ai/huggingface.go b/pkg/infra/ai/huggingface.go index 573e2677..bfe09a93 100644 --- a/pkg/infra/ai/huggingface.go +++ b/pkg/infra/ai/huggingface.go @@ -46,9 +46,9 @@ func (c *HuggingfaceClient) Generate(ctx context.Context, prompt string) (string }, Model: c.model, }) - if err != nil { return "", err } + return resp[0].GeneratedText[len(prompt):], nil } diff --git a/pkg/infra/ai/openai.go b/pkg/infra/ai/openai.go index 9ec3f98e..1150776b 100644 --- a/pkg/infra/ai/openai.go +++ b/pkg/infra/ai/openai.go @@ -17,6 +17,7 @@ package ai import ( "context" "errors" + "github.com/sashabaranov/go-openai" ) From 7df78212b0782bba4a87c007bb6bb730230a6fbb Mon Sep 17 00:00:00 2001 From: ruquanzhao <903264308@qq.com> Date: Tue, 26 Nov 2024 17:17:02 +0800 Subject: [PATCH 11/11] fix: fix i18n --- pkg/core/handler/search/search.go | 3 +-- ui/src/locales/de.json | 3 ++- ui/src/locales/en.json | 3 ++- ui/src/locales/pt.json | 3 ++- ui/src/locales/zh.json | 3 ++- ui/src/pages/result/index.tsx | 5 ++++- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pkg/core/handler/search/search.go b/pkg/core/handler/search/search.go index 2954cbd8..054b2640 100644 --- a/pkg/core/handler/search/search.go +++ b/pkg/core/handler/search/search.go @@ -18,9 +18,8 @@ import ( "net/http" "strconv" - "github.com/KusionStack/karpor/pkg/core/manager/ai" - "github.com/KusionStack/karpor/pkg/core/handler" + "github.com/KusionStack/karpor/pkg/core/manager/ai" "github.com/KusionStack/karpor/pkg/core/manager/search" "github.com/KusionStack/karpor/pkg/infra/search/storage" "github.com/KusionStack/karpor/pkg/util/ctxutil" diff --git a/ui/src/locales/de.json b/ui/src/locales/de.json index e1c874a8..c6dee3a8 100644 --- a/ui/src/locales/de.json +++ b/ui/src/locales/de.json @@ -121,5 +121,6 @@ "LogoutSuccess": "Erfolgreich abgemeldet", "InputToken": "Geben Sie bitte den Token ein", "SearchByNaturalLanguage": "Suche mit natürlicher Sprache", - "CannotBeEmpty": "Darf nicht leer sein" + "CannotBeEmpty": "Darf nicht leer sein", + "DefaultTag": "Standard-Tag" } diff --git a/ui/src/locales/en.json b/ui/src/locales/en.json index e3859256..40b02524 100644 --- a/ui/src/locales/en.json +++ b/ui/src/locales/en.json @@ -121,5 +121,6 @@ "LogoutSuccess": "Successfully Logged Out", "InputToken": "Please Enter the Token", "SearchByNaturalLanguage": "Search By Natural Language", - "CannotBeEmpty": "Cannot be empty" + "CannotBeEmpty": "Cannot be empty", + "DefaultTag": "default tag" } diff --git a/ui/src/locales/pt.json b/ui/src/locales/pt.json index f295d23a..67965cc0 100644 --- a/ui/src/locales/pt.json +++ b/ui/src/locales/pt.json @@ -121,5 +121,6 @@ "LogoutSuccess": "Sessão encerrada com sucesso", "InputToken": "Por favor, insira o token", "SearchByNaturalLanguage": "Procure por linguagem natural", - "CannotBeEmpty": "Não pode estar vazio" + "CannotBeEmpty": "Não pode estar vazio", + "DefaultTag": "Tag padrão" } diff --git a/ui/src/locales/zh.json b/ui/src/locales/zh.json index b2a73e50..bd61e526 100644 --- a/ui/src/locales/zh.json +++ b/ui/src/locales/zh.json @@ -121,5 +121,6 @@ "LogoutSuccess": "登出成功", "InputToken": "请输入 token", "SearchByNaturalLanguage": "自然语言搜索", - "CannotBeEmpty": "不能为空" + "CannotBeEmpty": "不能为空", + "DefaultTag": "默认标签" } diff --git a/ui/src/pages/result/index.tsx b/ui/src/pages/result/index.tsx index fa9297dc..468482dd 100644 --- a/ui/src/pages/result/index.tsx +++ b/ui/src/pages/result/index.tsx @@ -32,6 +32,7 @@ import { useAxios } from '@/utils/request' import styles from './styles.module.less' const { Search } = Input +const { t } = useTranslation() const Option = AutoComplete.Option export const CustomDropdown = props => { @@ -46,7 +47,9 @@ export const CustomDropdown = props => { >
))}