Skip to content

Commit 4e54e54

Browse files
committed
feat(ai playbook action): formats support & playbook recommender
1 parent 94f83ab commit 4e54e54

15 files changed

+412
-183
lines changed

api/v1/playbook_actions.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -370,26 +370,34 @@ func (t AIActionContext) ShouldFetchConfigChanges() bool {
370370
return true
371371
}
372372

373+
type AIActionFormat string
374+
375+
const (
376+
AIActionFormatSlack AIActionFormat = "slack"
377+
AIActionFormatMarkdown AIActionFormat = "markdown"
378+
AIActionFormatRecommendPlaybook AIActionFormat = "recommendPlaybook"
379+
)
380+
373381
type AIAction struct {
374382
AIActionClient `json:",inline" yaml:",inline"`
375383
AIActionContext `json:",inline" yaml:",inline" template:"true"`
376384

377-
// Use an AI agent that can autonomously drive the diagnosis using tools that interface directly with the database.
378-
// NOTE: Not exposed for now
379-
UseAgent bool `json:"-"`
385+
// Specify selectors for playbooks. The LLM will recommend the best suited playbooks
386+
// in response to the prompt.
387+
RecommendPlaybooks []types.ResourceSelector `json:"recommendPlaybooks,omitempty"`
380388

381389
// system prompt is a way to provide context, instructions, and guidelines to the LLM before presenting it
382390
// with a question or task.
383391
// By using a system prompt, you can set the stage for the conversation, specifying LLM's role, personality,
384392
// tone, or any other relevant information that will help it better understand and respond to the user's input.
385393
SystemPrompt string `json:"systemPrompt"`
386394

387-
// Prompt is the humna prompt
395+
// Prompt is the human prompt
388396
Prompt string `json:"prompt" template:"true"`
389397

390398
// Output format of the prompt.
391-
// Supported: markdown (default), slack.
392-
Formats []string `json:"formats,omitempty"`
399+
// Supported: markdown (default), slack, recommendPlaybook
400+
Formats []AIActionFormat `json:"formats,omitempty"`
393401
}
394402

395403
type ExecAction struct {

api/v1/zz_generated.deepcopy.go

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crds/mission-control.flanksource.com_playbooks.yaml

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ spec:
127127
formats:
128128
description: |-
129129
Output format of the prompt.
130-
Supported: markdown (default), slack.
130+
Supported: markdown (default), slack, recommendPlaybook
131131
items:
132132
type: string
133133
type: array
@@ -137,8 +137,71 @@ spec:
137137
Example: gpt-4o for openai, claude-3-5-sonnet-latest for Anthropic, llama3.1:8b for Ollama
138138
type: string
139139
prompt:
140-
description: Prompt is the humna prompt
140+
description: Prompt is the human prompt
141141
type: string
142+
recommendPlaybooks:
143+
description: |-
144+
Specify selectors for playbooks. The LLM will recommend the best suited playbooks
145+
in response to the prompt.
146+
items:
147+
properties:
148+
agent:
149+
description: |-
150+
Agent can be the agent id or the name of the agent.
151+
Additionally, the special "self" value can be used to select resources without an agent.
152+
type: string
153+
cache:
154+
description: |-
155+
Cache directives
156+
'no-cache' (should not fetch from cache but can be cached)
157+
'no-store' (should not cache)
158+
'max-age=X' (cache for X duration)
159+
type: string
160+
fieldSelector:
161+
type: string
162+
healths:
163+
description: Healths filter resources by the health
164+
items:
165+
type: string
166+
type: array
167+
id:
168+
type: string
169+
includeDeleted:
170+
type: boolean
171+
labelSelector:
172+
type: string
173+
limit:
174+
type: integer
175+
name:
176+
type: string
177+
namespace:
178+
type: string
179+
scope:
180+
description: |-
181+
Scope is the reference for parent of the resource to select.
182+
For config items, the scope is the scraper id
183+
For checks, it's canaries and
184+
For components, it's topology.
185+
It can either be a uuid or namespace/name
186+
type: string
187+
search:
188+
description: Search query that applies to the resource
189+
name, tag & labels.
190+
type: string
191+
statuses:
192+
description: Statuses filter resources by the status
193+
items:
194+
type: string
195+
type: array
196+
tagSelector:
197+
type: string
198+
types:
199+
description: Types filter resources by the type
200+
items:
201+
type: string
202+
type: array
203+
type: object
204+
type: array
142205
relationships:
143206
description: Select related configs to provide as an additional
144207
context to the AI model.

config/schemas/playbook-spec.schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@
3535
},
3636
"type": "array"
3737
},
38+
"recommendPlaybooks": {
39+
"items": {
40+
"$ref": "#/$defs/ResourceSelector"
41+
},
42+
"type": "array"
43+
},
3844
"systemPrompt": {
3945
"type": "string"
4046
},

config/schemas/playbook.schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@
3535
},
3636
"type": "array"
3737
},
38+
"recommendPlaybooks": {
39+
"items": {
40+
"$ref": "#/$defs/ResourceSelector"
41+
},
42+
"type": "array"
43+
},
3844
"systemPrompt": {
3945
"type": "string"
4046
},

db/playbooks.go

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,6 @@ import (
1818
v1 "github.com/flanksource/incident-commander/api/v1"
1919
)
2020

21-
func FindPlaybooksForEvent(ctx context.Context, eventClass, event string) ([]models.Playbook, error) {
22-
var playbooks []models.Playbook
23-
query := fmt.Sprintf(`SELECT * FROM playbooks WHERE spec->'on'->'%s' @> '[{"event": "%s"}]'`, eventClass, event)
24-
if err := ctx.DB().Raw(query).Scan(&playbooks).Error; err != nil {
25-
return nil, err
26-
}
27-
28-
return playbooks, nil
29-
}
30-
3121
func FindPlaybookRun(ctx context.Context, id uuid.UUID) (*models.PlaybookRun, error) {
3222
var p models.PlaybookRun
3323
if err := ctx.DB().Where("id = ?", id).First(&p).Error; err != nil {
@@ -83,14 +73,19 @@ func GetPlaybookRun(ctx context.Context, id string) (*models.PlaybookRun, error)
8373
return &p, nil
8474
}
8575

86-
func findPlaybooksForResourceSelector(ctx context.Context, selectable types.ResourceSelectable, selectorField string) ([]api.PlaybookListItem, error) {
76+
func findPlaybooksForResourceSelectable(ctx context.Context, selectable types.ResourceSelectable, selectorField string) (
77+
[]api.PlaybookListItem,
78+
[]*models.Playbook,
79+
error,
80+
) {
8781
var playbooks []models.Playbook
8882
if err := ctx.DB().Model(&models.Playbook{}).Where(fmt.Sprintf("spec->>'%s' IS NOT NULL", selectorField)).Where("deleted_at IS NULL").Find(&playbooks).Error; err != nil {
89-
return nil, fmt.Errorf("error finding playbooks with %s: %w", selectorField, err)
83+
return nil, nil, fmt.Errorf("error finding playbooks with %s: %w", selectorField, err)
9084
}
9185

9286
// To return empty list instead of null
9387
playbookListItems := make([]api.PlaybookListItem, 0)
88+
var matchedPlaybooks []*models.Playbook
9489

9590
for _, pb := range playbooks {
9691
var spec v1.PlaybookSpec
@@ -127,31 +122,43 @@ func findPlaybooksForResourceSelector(ctx context.Context, selectable types.Reso
127122

128123
params, err := json.Marshal(spec.Parameters)
129124
if err != nil {
130-
return nil, fmt.Errorf("error marshaling params[%v] to json: %w", spec.Parameters, err)
125+
return nil, nil, fmt.Errorf("error marshaling params[%v] to json: %w", spec.Parameters, err)
131126
}
132127
playbookListItems = append(playbookListItems, api.PlaybookListItem{
133128
ID: pb.ID,
134129
Name: pb.Name,
135130
Parameters: params,
136131
})
132+
133+
matchedPlaybooks = append(matchedPlaybooks, &pb)
137134
}
138135

139-
return playbookListItems, nil
136+
return playbookListItems, matchedPlaybooks, nil
140137
}
141138

142139
// FindPlaybooksForCheck returns all the playbooks that match the given check type and tags.
143-
func FindPlaybooksForCheck(ctx context.Context, check models.Check) ([]api.PlaybookListItem, error) {
144-
return findPlaybooksForResourceSelector(ctx, check, "checks")
140+
func FindPlaybooksForCheck(ctx context.Context, check models.Check) ([]api.PlaybookListItem, []*models.Playbook, error) {
141+
return findPlaybooksForResourceSelectable(ctx, check, "checks")
145142
}
146143

147144
// FindPlaybooksForConfig returns all the playbooks that match the given config's resource selectors
148-
func FindPlaybooksForConfig(ctx context.Context, config models.ConfigItem) ([]api.PlaybookListItem, error) {
149-
return findPlaybooksForResourceSelector(ctx, config, "configs")
145+
func FindPlaybooksForConfig(ctx context.Context, config models.ConfigItem) ([]api.PlaybookListItem, []*models.Playbook, error) {
146+
return findPlaybooksForResourceSelectable(ctx, config, "configs")
150147
}
151148

152149
// FindPlaybooksForComponent returns all the playbooks that match the given component type and tags.
153-
func FindPlaybooksForComponent(ctx context.Context, component models.Component) ([]api.PlaybookListItem, error) {
154-
return findPlaybooksForResourceSelector(ctx, component, "components")
150+
func FindPlaybooksForComponent(ctx context.Context, component models.Component) ([]api.PlaybookListItem, []*models.Playbook, error) {
151+
return findPlaybooksForResourceSelectable(ctx, component, "components")
152+
}
153+
154+
func FindPlaybooksForEvent(ctx context.Context, eventClass, event string) ([]models.Playbook, error) {
155+
var playbooks []models.Playbook
156+
query := fmt.Sprintf(`SELECT * FROM playbooks WHERE spec->'on'->'%s' @> '[{"event": "%s"}]'`, eventClass, event)
157+
if err := ctx.DB().Raw(query).Scan(&playbooks).Error; err != nil {
158+
return nil, err
159+
}
160+
161+
return playbooks, nil
155162
}
156163

157164
func FindPlaybookByWebhookPath(ctx context.Context, path string) (*models.Playbook, error) {

fixtures/playbooks/ai-diagnose-slack-notification.yaml

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ spec:
2020
- name: query LLM
2121
ai:
2222
connection: connection://mc/openai
23+
formats:
24+
- slack
2325
systemPrompt: |
2426
**Role:** Seasoned Kubernetes engineer and Diagnostic Expert
2527
@@ -30,31 +32,6 @@ spec:
3032
1. **Resource Analysis:** Examine the manifest of the unhealthy resource thoroughly.
3133
2. **Contextual Investigation:** Consider additional related resources provided (e.g., pods, replica sets, namespaces) to gain a comprehensive understanding of the issue.
3234
4. **One-Time Diagnosis:** Aim to diagnose the issue in a single response without requiring follow-up questions.
33-
34-
**Output:** Provide a concise diagnosis and potential solutions based on the analysis.
35-
The output should be in json using Block Kit(https://api.slack.com/block-kit) - a UI framework for Slack apps.
36-
Example: output
37-
{
38-
"blocks": [
39-
{
40-
"type": "section",
41-
"fields": [
42-
{
43-
"type": "mrkdwn",
44-
"text": "Statefulset: alertmanager"
45-
},
46-
{
47-
"type": "mrkdwn",
48-
"text": "*Namespace*: mc"
49-
},
50-
{
51-
"type": "mrkdwn",
52-
"text": "Deployment has pods that are in a crash loop."
53-
}
54-
]
55-
},
56-
]
57-
}
5835
prompt: '{{.params.prompt}}'
5936
changes:
6037
since: 2d
@@ -73,4 +50,4 @@ spec:
7350
notification:
7451
connection: connection://mc/flanksource-slack
7552
title: Diagnosis report
76-
message: '{{ getLastAction.result.logs }}'
53+
message: '{{ getLastAction.result.slack }}'
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
# yaml-language-server: $schema=../../config/schemas/playbook.schema.json
3+
apiVersion: mission-control.flanksource.com/v1
4+
kind: Playbook
5+
metadata:
6+
name: recommend-playbook
7+
spec:
8+
description: Use AI to diagnose unhealthy config items and send a notification slack
9+
configs:
10+
- healths:
11+
- unhealthy
12+
- warning
13+
parameters:
14+
- name: prompt
15+
label: Prompt
16+
default: Find out why {{.config.name}} is unhealthy and report in one short sentence.
17+
properties:
18+
multiline: 'true'
19+
actions:
20+
- name: query LLM
21+
ai:
22+
connection: connection://mc/anthropic
23+
formats:
24+
- slack
25+
- recommendPlaybook
26+
recommendPlaybooks:
27+
- namespace: mc
28+
systemPrompt: |
29+
**Role:** Seasoned Kubernetes engineer and Diagnostic Expert
30+
31+
**Objective:** Assist users in diagnosing issues with unhealthy Kubernetes resources by analyzing provided manifests and related resources.
32+
33+
**Instructions:**
34+
35+
1. **Resource Analysis:** Examine the manifest of the unhealthy resource thoroughly.
36+
2. **Contextual Investigation:** Consider additional related resources provided (e.g., pods, replica sets, namespaces) to gain a comprehensive understanding of the issue.
37+
4. **One-Time Diagnosis:** Aim to diagnose the issue in a single response without requiring follow-up questions.
38+
prompt: '{{.params.prompt}}'
39+
changes:
40+
since: 2d
41+
analysis:
42+
since: 2d
43+
relationships:
44+
- depth: 3
45+
direction: outgoing
46+
changes:
47+
since: 24h
48+
- depth: 5
49+
direction: incoming
50+
changes:
51+
since: 24h
52+
- name: send diagnosis report
53+
notification:
54+
connection: connection://mc/flanksource-slack
55+
title: Diagnosis report
56+
message: '$(getLastAction.result.slack)'
57+
- name: send recommended playbooks
58+
notification:
59+
connection: connection://mc/flanksource-slack
60+
title: Recommended playbooks
61+
message: '{{ index (getAction "query LLM").result.recommendedPlaybooks }}'

0 commit comments

Comments
 (0)