From 7eccd96e0d045de78347e16958abfaf0cd692486 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Thu, 23 May 2024 17:42:36 +0200 Subject: [PATCH 1/2] Print used OpenAI Assistant tools for AI assistant --- go.mod | 8 +- go.sum | 16 ++-- internal/source/ai-brain/assistant.go | 41 +++++++- internal/source/ai-brain/response.go | 74 ++++++++++----- internal/source/ai-brain/response_test.go | 93 ++++++++++++++++++- .../aiAnswer.md | 79 ++++++++++++++++ .../discord-tools.golden.json | 24 +++++ .../slack-tools.golden.json | 40 ++++++++ .../teams-tools.golden.json | 25 +++++ internal/source/ai-brain/tool_calls.go | 70 ++++++++++++++ 10 files changed, 431 insertions(+), 39 deletions(-) create mode 100644 internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/aiAnswer.md create mode 100644 internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/discord-tools.golden.json create mode 100644 internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/slack-tools.golden.json create mode 100644 internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/teams-tools.golden.json create mode 100644 internal/source/ai-brain/tool_calls.go diff --git a/go.mod b/go.mod index 09fdfc4..60b859c 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/honeycombio/otel-config-go v1.13.1 github.com/huandu/xstrings v1.4.0 github.com/keptn/go-utils v0.20.4 - github.com/kubeshop/botkube v0.13.1-0.20240426162820-c3c1bbe3c039 + github.com/kubeshop/botkube v0.13.1-0.20240520120227-304528f09aeb github.com/muesli/reflow v0.3.0 github.com/olekukonko/tablewriter v0.0.5 github.com/prometheus/client_golang v1.16.0 @@ -74,7 +74,7 @@ require ( github.com/evanphx/json-patch v5.7.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect - github.com/fatih/color v1.15.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/fluxcd/pkg/apis/kustomize v1.1.1 // indirect @@ -155,7 +155,7 @@ require ( github.com/shirou/gopsutil/v3 v3.23.10 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shopspring/decimal v1.3.1 // indirect - github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cobra v1.8.0 // indirect github.com/spiffe/spire v1.5.6 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect @@ -215,3 +215,5 @@ require ( sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) + +replace github.com/kubeshop/botkube => github.com/pkosiec/botkube v0.12.5-0.20240524144006-573a14549bcf diff --git a/go.sum b/go.sum index 68e52f9..ce0895a 100644 --- a/go.sum +++ b/go.sum @@ -323,8 +323,8 @@ github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZM github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= @@ -337,8 +337,8 @@ github.com/fluxcd/pkg/apis/meta v1.1.1 h1:sLAKLbEu7rRzJ+Mytffu3NcpfdbOBTa6hcpOQz github.com/fluxcd/pkg/apis/meta v1.1.1/go.mod h1:soCfzjFWbm1mqybDcOywWKTCEYlH3skpoNGTboVk234= github.com/fluxcd/source-controller/api v1.0.1 h1:nycylbNBnQd+EO4UHpqXqAQJ1cGAPxgBbrfERCQ1pp8= github.com/fluxcd/source-controller/api v1.0.1/go.mod h1:rAY5FRFGZUKpIFNyYANHIgPgJPvbALBHWsq/zHw/cXQ= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= @@ -667,8 +667,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kubeshop/botkube v0.13.1-0.20240426162820-c3c1bbe3c039 h1:x3KNRktuu0QYzjSr4P39igf2PneVFrgrpKWD7cAJ69I= -github.com/kubeshop/botkube v0.13.1-0.20240426162820-c3c1bbe3c039/go.mod h1:5QZExpXEutttEAn32I/Mf4FvoaVCoeNoFsznf8j905k= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= @@ -776,6 +774,8 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkosiec/botkube v0.12.5-0.20240524144006-573a14549bcf h1:Jzbq+RluUjYd39C2ali0hvEGZpIEVMB4Vh+JUYlc5Os= +github.com/pkosiec/botkube v0.12.5-0.20240524144006-573a14549bcf/go.mod h1:OZeY4kLDrVQlaGxCE3XnTX8UUUhSpENIZ41PmBVIePg= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -848,8 +848,8 @@ github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/internal/source/ai-brain/assistant.go b/internal/source/ai-brain/assistant.go index 49c3f61..56f3719 100644 --- a/internal/source/ai-brain/assistant.go +++ b/internal/source/ai-brain/assistant.go @@ -10,6 +10,8 @@ import ( "strings" "time" + "github.com/kubeshop/botkube/pkg/ptr" + "github.com/kubeshop/botkube-cloud-plugins/internal/otelx" "github.com/kubeshop/botkube-cloud-plugins/internal/remote" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" @@ -281,6 +283,14 @@ func (i *assistant) handleStatusCompleted(ctx context.Context, run openai.Run, p return nil } + toolCalls, err := i.listToolCalls(ctx, run.ThreadID, run.ID) + if err != nil { + err = fmt.Errorf("while listing tool calls: %w", err) + span.RecordError(err) + i.log.WithError(err).Error("failed to get tool calls") + // not returning as the tool usage is optional + } + // Iterate over text content to build messages. We're only interested in text // context, since the assistant is instructed to return text only. for _, c := range msgList.Messages[0].Content { @@ -291,7 +301,7 @@ func (i *assistant) handleStatusCompleted(ctx context.Context, run openai.Run, p textValue := i.trimCitationsIfPresent(i.log, c.Text) i.out <- source.Event{ - Message: msgAIAnswer(run, p, textValue), + Message: msgAIAnswer(run, p, textValue, toolCalls), } } @@ -379,6 +389,35 @@ func (i *assistant) trimCitationsIfPresent(log logrus.FieldLogger, in *openai.Me return outValue } +func (i *assistant) listToolCalls(ctx context.Context, threadID, runID string) (map[string]struct{}, error) { + const maxLimit = 100 + var ( + runSteps []openai.RunStep + after *string + ) + + for { + res, err := i.openaiClient.ListRunSteps(ctx, threadID, runID, openai.Pagination{Limit: ptr.FromType(maxLimit), After: after}) + if err != nil { + return nil, fmt.Errorf("while getting assistant run steps: %w", err) + } + + runSteps = append(runSteps, res.RunSteps...) + if !res.HasMore { + break + } + + after = &res.LastID + i.log.WithFields(logrus.Fields{ + "lastID": res.LastID, + "count": len(runSteps), + }).Debug("Paginating assistant run steps...") + } + i.log.WithField("count", len(runSteps)).Debug("Finished paginating assistant run steps") + + return getFriendlyToolCallsFromRunSteps(runSteps), nil +} + type apiKeySecuredTransport struct { transport http.RoundTripper } diff --git a/internal/source/ai-brain/response.go b/internal/source/ai-brain/response.go index 33c1aa2..d81b9aa 100644 --- a/internal/source/ai-brain/response.go +++ b/internal/source/ai-brain/response.go @@ -15,6 +15,7 @@ const ( teamsMessageIDSubstr = "thread.tacv2" reportResponseBtnName = "🚩Report response" maxPromptLen = 500 + aiContentWarning = "AI-generated content may be incorrect." ) var ( @@ -107,13 +108,15 @@ func msgNoAIAnswer(messageID string) api.Message { } } -func msgAIAnswer(run openai.Run, payload *Payload, response string) api.Message { +func msgAIAnswer(run openai.Run, payload *Payload, response string, toolCalls map[string]struct{}) api.Message { var ( - msgID = payload.MessageID - btnBldr = api.NewMessageButtonBuilder() + msgID = payload.MessageID + btnBldr = api.NewMessageButtonBuilder() + usedToolsMsg = printUsedTools(toolCalls) ) - if strings.Contains(msgID, teamsMessageIDSubstr) { // teams + // MS Teams + if strings.Contains(msgID, teamsMessageIDSubstr) { return api.Message{ Type: api.BasicCardWithButtonsInSeparateMsg, ParentActivityID: msgID, @@ -122,7 +125,7 @@ func msgAIAnswer(run openai.Run, payload *Payload, response string) api.Message // as simplified card (https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/format-your-bot-messages#format-text-content) // instead of AdaptiveCard (https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format?tabs=adaptive-md%2Cdesktop%2Cconnector-html#format-cards-with-markdown) // which doesn't support most of the markdown elements. - Plaintext: markdownToTeams(response), + Plaintext: markdownToTeams(response, usedToolsMsg), }, Sections: []api.Section{ { @@ -138,29 +141,45 @@ func msgAIAnswer(run openai.Run, payload *Payload, response string) api.Message // from the AI to prevent it from being interpreted as formatting. response = strings.ReplaceAll(response, "%", "%%") - sections := []api.Section{ - { - Base: api.Base{ - Body: api.Body{Plaintext: response}, - }, - Context: []api.ContextItem{ - {Text: "AI-generated content may be incorrect."}, - }, - }, - } - - if msgID != "" { // msgID is set only for Teams or Slack, Teams is handled above, so here is Slack - sections[0].Body.Plaintext = markdownToSlack(sections[0].Body.Plaintext) - sections = append(sections, api.Section{ - Buttons: api.Buttons{ - btnBldr.ForCommandWithoutDesc(reportResponseBtnName, reportCmd(run, payload), api.ButtonStyleDanger), + // Slack (msgID is set only for Teams or Slack, Teams is handled above) + if msgID != "" { + return api.Message{ + ParentActivityID: msgID, + Sections: []api.Section{ + { + Base: api.Base{ + Body: api.Body{Plaintext: markdownToSlack(response)}, + }, + Context: []api.ContextItem{ + {Text: markdownToSlack(usedToolsMsg)}, + }, + }, + { + Style: api.SectionStyle{ + Divider: api.DividerStyleTopNone, + }, + Buttons: api.Buttons{ + btnBldr.ForCommandWithItalicDesc(reportResponseBtnName, aiContentWarning, reportCmd(run, payload)), + }, + }, }, - }) + } } + // Other cases (e.g. Discord etc.) return api.Message{ ParentActivityID: msgID, - Sections: sections, + Sections: []api.Section{ + { + Base: api.Base{ + Body: api.Body{Plaintext: response}, + }, + Context: []api.ContextItem{ + {Text: usedToolsMsg}, + {Text: fmt.Sprintf("_%s_", aiContentWarning)}, + }, + }, + }, } } @@ -175,12 +194,17 @@ func markdownToSlack(text string) string { return mdLinks.ReplaceAllString(text, "<$2|$1>") } -func markdownToTeams(text string) string { +func markdownToTeams(text, additionalFooterMessage string) string { text = mdHeadings.ReplaceAllString(text, "**$1**") text = mdImages.ReplaceAllString(text, "[$1]($2)") // get rid of ! to define it as a link instead of image + text += "\n\n" + if additionalFooterMessage != "" { + text += fmt.Sprintf("~%s~\n\n", additionalFooterMessage) // add + } + // https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format?tabs=adaptive-md%2Cdesktop%2Cconnector-html#newlines-for-adaptive-cards - text += "\n\n~AI-generated content may be incorrect.~\n" // add the warning + text += "~AI-generated content may be incorrect.~\n" // add the warning return text } diff --git a/internal/source/ai-brain/response_test.go b/internal/source/ai-brain/response_test.go index e9caf9f..77a18cc 100644 --- a/internal/source/ai-brain/response_test.go +++ b/internal/source/ai-brain/response_test.go @@ -1,6 +1,7 @@ package aibrain import ( + "encoding/json" "os" "path/filepath" "testing" @@ -25,17 +26,105 @@ func TestConvertProperlyAIAnswer(t *testing.T) { out := msgAIAnswer(openai.Run{}, &Payload{ MessageID: "42.42", Prompt: "This is a test", - }, string(md)) + }, string(md), nil) assertGolden(t, out.Sections[0].Base.Body.Plaintext, "slack.golden.md") // Teams out = msgAIAnswer(openai.Run{}, &Payload{ MessageID: "19:d25cbf7cbfa74d22b42a2918452e1153@thread.tacv2", Prompt: "This is a test", - }, string(md)) + }, string(md), nil) assertGolden(t, out.BaseBody.Plaintext, "teams.golden.md") } +// TestConvertProperlyAIAnswerWithTools tests that we can convert proper AI answer to Slack or Teams message format. +// +// To update golden files run: +// +// go test ./internal/source/ai-brain/... -run=TestConvertProperlyAIAnswerWithTools -update +func TestConvertProperlyAIAnswerWithTools(t *testing.T) { + // given + md, err := os.ReadFile(filepath.Join("testdata", t.Name(), "aiAnswer.md")) + require.NoError(t, err) + + toolCalls := []openai.RunStep{ + { + StepDetails: openai.StepDetails{ + Type: openai.RunStepTypeToolCalls, + ToolCalls: []openai.ToolCall{ + { + Type: openai.ToolType("file_search"), + }, + { + Type: openai.ToolTypeFunction, + Function: openai.FunctionCall{ + Name: "botkubeGetStartupAgentConfiguration", + Arguments: "foo", + }, + }, + }, + }, + }, + { + StepDetails: openai.StepDetails{ + Type: openai.RunStepTypeToolCalls, + ToolCalls: []openai.ToolCall{ + { + Type: openai.ToolTypeFunction, + Function: openai.FunctionCall{ + Name: "botkubeGetAgentStatus", + Arguments: "foo", + }, + }, + { + Type: openai.ToolTypeFunction, + Function: openai.FunctionCall{ + Name: "kubectlGetResource", + Arguments: "foo", + }, + }, + { + Type: openai.ToolTypeFunction, + Function: openai.FunctionCall{ + Name: "kubectlLogs", + Arguments: "foo", + }, + }, + }, + }, + }, + } + convertedToolCalls := getFriendlyToolCallsFromRunSteps(toolCalls) + + // Slack + out := msgAIAnswer(openai.Run{}, &Payload{ + MessageID: "42.42", + Prompt: "This is a test", + }, string(md), convertedToolCalls) + + outBytes, err := json.MarshalIndent(out, "", " ") + require.NoError(t, err) + assertGolden(t, string(outBytes), "slack-tools.golden.json") + + // Teams + out = msgAIAnswer(openai.Run{}, &Payload{ + MessageID: "19:d25cbf7cbfa74d22b42a2918452e1153@thread.tacv2", + Prompt: "This is a test", + }, string(md), convertedToolCalls) + outBytes, err = json.MarshalIndent(out, "", " ") + require.NoError(t, err) + assertGolden(t, string(outBytes), "teams-tools.golden.json") + + // Discord and others + out = msgAIAnswer(openai.Run{}, &Payload{ + MessageID: "", + Prompt: "This is a test", + }, string(md), convertedToolCalls) + outBytes, err = json.MarshalIndent(out, "", " ") + require.NoError(t, err) + assertGolden(t, string(outBytes), "discord-tools.golden.json") +} + func assertGolden(t *testing.T, actual, goldenPath string) { golden.Assert(t, actual, filepath.Join(t.Name(), goldenPath)) } diff --git a/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/aiAnswer.md b/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/aiAnswer.md new file mode 100644 index 0000000..0ed8fa3 --- /dev/null +++ b/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/aiAnswer.md @@ -0,0 +1,79 @@ +## Found Issues + +The issue you're facing is due to a missing secret named `ngin`. Secrets in Kubernetes are used to store and handle sensitive information, such as passwords, OAuth tokens, ssh keys, etc. The specific error indicates that a pod is trying to use a secret called `ngin` that doesn’t exist in the cluster. Here’s a simple step-by-step guide on how to fix it: + +1. **Identify the Missing Secret Requirement:** + + > Ensure that the name **"ngin"** is correct. Sometimes, a typo in the pod configuration or secret name can cause such issues. + +2. Create the Secret: + If the secret is indeed missing `##` and you know the data that should be within it (like a token or password), you will need to create the secret. This can be done using the kubectl command line. For example, if you're creating a generic secret, you might use: + + ```shell + kubectl create secret generic ngin --from-literal=key=value -n test + ``` + + Replace key and value with the actual data you need to store. The -n test specifies the namespace (test in this case) where the secret will be created. Adjust as necessary. +3. **Update the Pod or Deployment Configuration:** + If the secret name was incorrect due to a typo in your pod or deployment configuration, you should update the relevant YAML file to correct the secret name and then apply the changes. For example: + + ```yaml + apiVersion: v1 + kind: Pod + metadata: + name: your-pod-name + spec: + containers: + - name: your-container-name + image: nginx + env: + - name: SECRET_KEY + valueFrom: + secretKeyRef: + name: ngin # Make sure this matches the correct secret name + key: key + ``` + +If you had to update your configuration, apply the changes: + +``` +kubectl apply -f your-deployment.yaml +``` + +### Ecosystem and Community + +![logo](https://raw.githubusercontent.com/kubeshop/botkube/main/branding/logos/botkube-black-32x32.png) + +_Kubernetes_ has a ~~large~~, rapidly growing `ecosystem`. Kubernetes' services, support, and tools are widely available. + +> Kubernetes has entered the chat +> Intelligent Kubernetes Monitoring & Troubleshooting Platform + +[Botkube](https://botkube.io/) + + +| Tables | Are | Cool | +|----------|:-------------:|------:| +| col 1 is | left-aligned | $1600 | +| col 2 is | centered | $12 | +| col 3 is | right-aligned | $1 | + + +### Notes + +This message includes: +- link +- image +- text + - italic + - bold + - strike +- multi-line code block + - with syntax + - without syntax +- single code block +- bullet list +- numbered list +- headers +- blockquote +- table diff --git a/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/discord-tools.golden.json b/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/discord-tools.golden.json new file mode 100644 index 0000000..03e19c6 --- /dev/null +++ b/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/discord-tools.golden.json @@ -0,0 +1,24 @@ +{ + "baseBody": {}, + "timestamp": "0001-01-01T00:00:00Z", + "sections": [ + { + "style": {}, + "body": { + "plaintext": "## Found Issues\n\nThe issue you're facing is due to a missing secret named `ngin`. Secrets in Kubernetes are used to store and handle sensitive information, such as passwords, OAuth tokens, ssh keys, etc. The specific error indicates that a pod is trying to use a secret called `ngin` that doesn’t exist in the cluster. Here’s a simple step-by-step guide on how to fix it:\n\n1. **Identify the Missing Secret Requirement:**\n\n \u003e Ensure that the name **\"ngin\"** is correct. Sometimes, a typo in the pod configuration or secret name can cause such issues.\n\n2. Create the Secret:\n If the secret is indeed missing `##` and you know the data that should be within it (like a token or password), you will need to create the secret. This can be done using the kubectl command line. For example, if you're creating a generic secret, you might use:\n\n ```shell\n kubectl create secret generic ngin --from-literal=key=value -n test\n ```\n\n Replace key and value with the actual data you need to store. The -n test specifies the namespace (test in this case) where the secret will be created. Adjust as necessary.\n3. **Update the Pod or Deployment Configuration:**\n If the secret name was incorrect due to a typo in your pod or deployment configuration, you should update the relevant YAML file to correct the secret name and then apply the changes. For example:\n\n ```yaml\n apiVersion: v1\n kind: Pod\n metadata:\n name: your-pod-name\n spec:\n containers:\n - name: your-container-name\n image: nginx\n env:\n - name: SECRET_KEY\n valueFrom:\n secretKeyRef:\n name: ngin # Make sure this matches the correct secret name\n key: key\n ```\n\nIf you had to update your configuration, apply the changes:\n\n```\nkubectl apply -f your-deployment.yaml\n```\n\n### Ecosystem and Community\n\n![logo](https://raw.githubusercontent.com/kubeshop/botkube/main/branding/logos/botkube-black-32x32.png)\n\n_Kubernetes_ has a ~~large~~, rapidly growing `ecosystem`. Kubernetes' services, support, and tools are widely available.\n\n\u003e Kubernetes has entered the chat\n\u003e Intelligent Kubernetes Monitoring \u0026 Troubleshooting Platform\n\n[Botkube](https://botkube.io/)\n\n\n| Tables | Are | Cool |\n|----------|:-------------:|------:|\n| col 1 is | left-aligned | $1600 |\n| col 2 is | centered | $12 |\n| col 3 is | right-aligned | $1 |\n\n\n### Notes\n\nThis message includes:\n- link\n- image\n- text \n - italic\n - bold\n - strike\n- multi-line code block\n - with syntax\n - without syntax\n- single code block\n- bullet list\n- numbered list\n- headers\n- blockquote\n- table\n" + }, + "multiSelect": { + "description": {} + }, + "selects": {}, + "context": [ + { + "text": "ℹ️ **Sources used:** Botkube agent configuration | Botkube agent status | Botkube content search | kubectl get | kubectl logs" + }, + { + "text": "_AI-generated content may be incorrect._" + } + ] + } + ] +} \ No newline at end of file diff --git a/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/slack-tools.golden.json b/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/slack-tools.golden.json new file mode 100644 index 0000000..aabcb0b --- /dev/null +++ b/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/slack-tools.golden.json @@ -0,0 +1,40 @@ +{ + "baseBody": {}, + "timestamp": "0001-01-01T00:00:00Z", + "sections": [ + { + "style": {}, + "body": { + "plaintext": "*Found Issues*\n\nThe issue you're facing is due to a missing secret named `ngin`. Secrets in Kubernetes are used to store and handle sensitive information, such as passwords, OAuth tokens, ssh keys, etc. The specific error indicates that a pod is trying to use a secret called `ngin` that doesn’t exist in the cluster. Here’s a simple step-by-step guide on how to fix it:\n\n1. *Identify the Missing Secret Requirement:*\n\n \u003e Ensure that the name *\"ngin\"* is correct. Sometimes, a typo in the pod configuration or secret name can cause such issues.\n\n2. Create the Secret:\n If the secret is indeed missing `##` and you know the data that should be within it (like a token or password), you will need to create the secret. This can be done using the kubectl command line. For example, if you're creating a generic secret, you might use:\n\n ```\n kubectl create secret generic ngin --from-literal=key=value -n test\n ```\n\n Replace key and value with the actual data you need to store. The -n test specifies the namespace (test in this case) where the secret will be created. Adjust as necessary.\n3. *Update the Pod or Deployment Configuration:*\n If the secret name was incorrect due to a typo in your pod or deployment configuration, you should update the relevant YAML file to correct the secret name and then apply the changes. For example:\n\n ```\n apiVersion: v1\n kind: Pod\n metadata:\n name: your-pod-name\n spec:\n containers:\n - name: your-container-name\n image: nginx\n env:\n - name: SECRET_KEY\n valueFrom:\n secretKeyRef:\n name: ngin # Make sure this matches the correct secret name\n key: key\n ```\n\nIf you had to update your configuration, apply the changes:\n\n```\nkubectl apply -f your-deployment.yaml\n```\n\n*Ecosystem and Community*\n\n\u003chttps://raw.githubusercontent.com/kubeshop/botkube/main/branding/logos/botkube-black-32x32.png|logo\u003e\n\n_Kubernetes_ has a ~large~, rapidly growing `ecosystem`. Kubernetes' services, support, and tools are widely available.\n\n\u003e Kubernetes has entered the chat\n\u003e Intelligent Kubernetes Monitoring \u0026 Troubleshooting Platform\n\n\u003chttps://botkube.io/|Botkube\u003e\n\n\n| Tables | Are | Cool |\n|----------|:-------------:|------:|\n| col 1 is | left-aligned | $1600 |\n| col 2 is | centered | $12 |\n| col 3 is | right-aligned | $1 |\n\n\n*Notes*\n\nThis message includes:\n- link\n- image\n- text \n - italic\n - bold\n - strike\n- multi-line code block\n - with syntax\n - without syntax\n- single code block\n- bullet list\n- numbered list\n- headers\n- blockquote\n- table\n" + }, + "multiSelect": { + "description": {} + }, + "selects": {}, + "context": [ + { + "text": "ℹ️ *Sources used:* Botkube agent configuration | Botkube agent status | Botkube content search | kubectl get | kubectl logs" + } + ] + }, + { + "style": { + "divider": "none" + }, + "body": {}, + "buttons": [ + { + "description": "AI-generated content may be incorrect.", + "descriptionStyle": "italic", + "name": "🚩Report response", + "command": "{{BotName}} cloud report analytics --bk-cmd-header=\"Report invalid AI response\" -t=ai-invalid-response -f=MESSAGE_ID=\"42.42\" -f=INSTANCE_ID=\"\" -f=RUN_ID=\"\" -f=THREAD_ID=\"\" -f=PROMPT=\"This is a test\"" + } + ], + "multiSelect": { + "description": {} + }, + "selects": {} + } + ], + "parentActivityId": "42.42" +} \ No newline at end of file diff --git a/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/teams-tools.golden.json b/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/teams-tools.golden.json new file mode 100644 index 0000000..cf22839 --- /dev/null +++ b/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/teams-tools.golden.json @@ -0,0 +1,25 @@ +{ + "type": "basicCardWithButtonsInSeparateMessage", + "baseBody": { + "plaintext": "**Found Issues**\n\nThe issue you're facing is due to a missing secret named `ngin`. Secrets in Kubernetes are used to store and handle sensitive information, such as passwords, OAuth tokens, ssh keys, etc. The specific error indicates that a pod is trying to use a secret called `ngin` that doesn’t exist in the cluster. Here’s a simple step-by-step guide on how to fix it:\n\n1. **Identify the Missing Secret Requirement:**\n\n \u003e Ensure that the name **\"ngin\"** is correct. Sometimes, a typo in the pod configuration or secret name can cause such issues.\n\n2. Create the Secret:\n If the secret is indeed missing `##` and you know the data that should be within it (like a token or password), you will need to create the secret. This can be done using the kubectl command line. For example, if you're creating a generic secret, you might use:\n\n ```shell\n kubectl create secret generic ngin --from-literal=key=value -n test\n ```\n\n Replace key and value with the actual data you need to store. The -n test specifies the namespace (test in this case) where the secret will be created. Adjust as necessary.\n3. **Update the Pod or Deployment Configuration:**\n If the secret name was incorrect due to a typo in your pod or deployment configuration, you should update the relevant YAML file to correct the secret name and then apply the changes. For example:\n\n ```yaml\n apiVersion: v1\n kind: Pod\n metadata:\n name: your-pod-name\n spec:\n containers:\n - name: your-container-name\n image: nginx\n env:\n - name: SECRET_KEY\n valueFrom:\n secretKeyRef:\n name: ngin # Make sure this matches the correct secret name\n key: key\n ```\n\nIf you had to update your configuration, apply the changes:\n\n```\nkubectl apply -f your-deployment.yaml\n```\n\n**Ecosystem and Community**\n\n[logo](https://raw.githubusercontent.com/kubeshop/botkube/main/branding/logos/botkube-black-32x32.png)\n\n_Kubernetes_ has a ~~large~~, rapidly growing `ecosystem`. Kubernetes' services, support, and tools are widely available.\n\n\u003e Kubernetes has entered the chat\n\u003e Intelligent Kubernetes Monitoring \u0026 Troubleshooting Platform\n\n[Botkube](https://botkube.io/)\n\n\n| Tables | Are | Cool |\n|----------|:-------------:|------:|\n| col 1 is | left-aligned | $1600 |\n| col 2 is | centered | $12 |\n| col 3 is | right-aligned | $1 |\n\n\n**Notes**\n\nThis message includes:\n- link\n- image\n- text \n - italic\n - bold\n - strike\n- multi-line code block\n - with syntax\n - without syntax\n- single code block\n- bullet list\n- numbered list\n- headers\n- blockquote\n- table\n\n\n~ℹ️ **Sources used:** Botkube agent configuration | Botkube agent status | Botkube content search | kubectl get | kubectl logs~\n\n~AI-generated content may be incorrect.~\n" + }, + "timestamp": "0001-01-01T00:00:00Z", + "sections": [ + { + "style": {}, + "body": {}, + "buttons": [ + { + "descriptionStyle": "", + "name": "🚩Report response", + "command": "{{BotName}} cloud report analytics --bk-cmd-header=\"Report invalid AI response\" -t=ai-invalid-response -f=MESSAGE_ID=\"19:d25cbf7cbfa74d22b42a2918452e1153@thread.tacv2\" -f=INSTANCE_ID=\"\" -f=RUN_ID=\"\" -f=THREAD_ID=\"\" -f=PROMPT=\"This is a test\"" + } + ], + "multiSelect": { + "description": {} + }, + "selects": {} + } + ], + "parentActivityId": "19:d25cbf7cbfa74d22b42a2918452e1153@thread.tacv2" +} \ No newline at end of file diff --git a/internal/source/ai-brain/tool_calls.go b/internal/source/ai-brain/tool_calls.go new file mode 100644 index 0000000..91c5271 --- /dev/null +++ b/internal/source/ai-brain/tool_calls.go @@ -0,0 +1,70 @@ +package aibrain + +import ( + "fmt" + "strings" + + "github.com/sashabaranov/go-openai" + "golang.org/x/exp/slices" +) + +// https://platform.openai.com/docs/api-reference/runs/object#runs/object-tools +var userFriendlyToolNames = map[string]string{ + "file_search": "Botkube content search", // for now it is only that, later when we will have the "Bring your own docs" functionality, we should rename it + + "function/botkubeGetStartupAgentConfiguration": "Botkube agent configuration", + "function/botkubeGetAgentStatus": "Botkube agent status", + "function/kubectlGetResource": "kubectl get", + "function/kubectlGetEvents": "kubectl get events", + "function/kubectlDescribeResource": "kubectl describe", + "function/kubectlTopPods": "kubectl top pods", + "function/kubectlTopNodes": "kubectl top nodes", + "function/kubectlLogs": "kubectl logs", + + "code_interpreter": "Code Interpreter", // we don't use this one, at least yet +} + +func getFriendlyToolCallsFromRunSteps(runSteps []openai.RunStep) map[string]struct{} { + toolCalls := make(map[string]struct{}) + for _, step := range runSteps { + if step.StepDetails.Type != openai.RunStepTypeToolCalls { + continue + } + + for _, t := range step.StepDetails.ToolCalls { + key := getUserFriendlyToolNameKey(t) + outName, ok := userFriendlyToolNames[key] + if !ok { + continue + } + + toolCalls[outName] = struct{}{} + } + } + + return toolCalls +} + +func getUserFriendlyToolNameKey(toolCall openai.ToolCall) string { + var keySuffix string + if toolCall.Function.Name != "" { + keySuffix = fmt.Sprintf("/%s", toolCall.Function.Name) + } + + key := fmt.Sprintf("%s%s", toolCall.Type, keySuffix) + return key +} + +func printUsedTools(usedTools map[string]struct{}) string { + if len(usedTools) == 0 { + return "" + } + + var outputToolNames []string + for name := range usedTools { + outputToolNames = append(outputToolNames, name) + } + slices.Sort(outputToolNames) + + return fmt.Sprintf("ℹ️ **Sources used:** %s", strings.Join(outputToolNames, " | ")) +} From 1a6574ee09f590ac2e58c0ca7af94e76975c4be1 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Mon, 27 May 2024 16:06:44 +0200 Subject: [PATCH 2/2] Use latest main of Botkube repo in go.mod --- go.mod | 4 +--- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 60b859c..f73f7ac 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/honeycombio/otel-config-go v1.13.1 github.com/huandu/xstrings v1.4.0 github.com/keptn/go-utils v0.20.4 - github.com/kubeshop/botkube v0.13.1-0.20240520120227-304528f09aeb + github.com/kubeshop/botkube v0.13.1-0.20240527133334-a3f727e2a30f github.com/muesli/reflow v0.3.0 github.com/olekukonko/tablewriter v0.0.5 github.com/prometheus/client_golang v1.16.0 @@ -215,5 +215,3 @@ require ( sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) - -replace github.com/kubeshop/botkube => github.com/pkosiec/botkube v0.12.5-0.20240524144006-573a14549bcf diff --git a/go.sum b/go.sum index ce0895a..0a35ce7 100644 --- a/go.sum +++ b/go.sum @@ -667,6 +667,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kubeshop/botkube v0.13.1-0.20240527133334-a3f727e2a30f h1:vnOhS1p8eJkxSiqc0ms7NSNLwRmxbK58k4Ig1DX7b6U= +github.com/kubeshop/botkube v0.13.1-0.20240527133334-a3f727e2a30f/go.mod h1:OZeY4kLDrVQlaGxCE3XnTX8UUUhSpENIZ41PmBVIePg= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= @@ -774,8 +776,6 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkosiec/botkube v0.12.5-0.20240524144006-573a14549bcf h1:Jzbq+RluUjYd39C2ali0hvEGZpIEVMB4Vh+JUYlc5Os= -github.com/pkosiec/botkube v0.12.5-0.20240524144006-573a14549bcf/go.mod h1:OZeY4kLDrVQlaGxCE3XnTX8UUUhSpENIZ41PmBVIePg= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=