Skip to content

Commit d7a29b2

Browse files
authored
feat: add analyst agent support for canvas dashboard (#8669)
* feat: add chat support in canvas preview mode * Improve component identification * Add context to canvas * Fix context not being honoured * Improve chart labels * Add tests for canvas context * Fix issues * Fix CI * Address PR comments * Add missing chat toggle in cloud * Fix incorrect filters being sent * Fix lint * Improve typing
1 parent 048f2e6 commit d7a29b2

File tree

46 files changed

+2564
-1290
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2564
-1290
lines changed

proto/gen/rill/admin/v1/openapi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2307,7 +2307,7 @@ externalDocs:
23072307
info:
23082308
description: Rill Admin API enables programmatic management of Rill Cloud resources, including organizations, projects, and user access. It provides endpoints for creating, updating, and deleting these resources, as well as managing authentication and permissions.
23092309
title: Rill Admin API
2310-
version: v0.80.0
2310+
version: v0.79.5
23112311
openapi: 3.0.3
23122312
paths:
23132313
/v1/ai/complete:

proto/gen/rill/admin/v1/public.openapi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2307,7 +2307,7 @@ externalDocs:
23072307
info:
23082308
description: Rill Admin API enables programmatic management of Rill Cloud resources, including organizations, projects, and user access. It provides endpoints for creating, updating, and deleting these resources, as well as managing authentication and permissions.
23092309
title: Rill Admin API
2310-
version: v0.80.0
2310+
version: v0.79.5
23112311
openapi: 3.0.3
23122312
paths:
23132313
/v1/ai/complete: {}

proto/gen/rill/runtime/v1/api.pb.go

Lines changed: 1017 additions & 967 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/gen/rill/runtime/v1/api.pb.validate.go

Lines changed: 50 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/gen/rill/runtime/v1/runtime.swagger.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4049,6 +4049,12 @@ definitions:
40494049
explore:
40504050
type: string
40514051
description: Optional explore dashboard.
4052+
canvas:
4053+
type: string
4054+
description: Optional canvas dashboard.
4055+
canvasComponent:
4056+
type: string
4057+
description: Optional canvas component within a dashboard.
40524058
dimensions:
40534059
type: array
40544060
items:
@@ -4062,6 +4068,14 @@ definitions:
40624068
where:
40634069
$ref: '#/definitions/v1Expression'
40644070
description: Optional filters.
4071+
wherePerMetricsView:
4072+
type: object
4073+
additionalProperties:
4074+
$ref: '#/definitions/v1Expression'
4075+
title: |-
4076+
Filter expressions as key-value pairs for the canvas.
4077+
Key: Metrics view name
4078+
Value: Expression object
40654079
timeStart:
40664080
type: string
40674081
format: date-time

proto/rill/runtime/v1/api.proto

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,12 +1066,20 @@ message Message {
10661066
message AnalystAgentContext {
10671067
// Optional explore dashboard.
10681068
string explore = 4;
1069+
// Optional canvas dashboard.
1070+
string canvas = 10;
1071+
// Optional canvas component within a dashboard.
1072+
string canvas_component = 11;
10691073
// Optional dimensions.
10701074
repeated string dimensions = 5;
10711075
// Optional measures.
10721076
repeated string measures = 6;
10731077
// Optional filters.
10741078
rill.runtime.v1.Expression where = 7;
1079+
// Filter expressions as key-value pairs for the canvas.
1080+
// Key: Metrics view name
1081+
// Value: Expression object
1082+
map<string, rill.runtime.v1.Expression> where_per_metrics_view = 12;
10751083
// Optional start of a time range.
10761084
google.protobuf.Timestamp time_start = 8;
10771085
// Optional end of a time range.

runtime/ai/ai.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func NewRunner(rt *runtime.Runtime, activity *activity.Client) *Runner {
5252

5353
RegisterTool(r, &ListMetricsViews{Runtime: rt})
5454
RegisterTool(r, &GetMetricsView{Runtime: rt})
55+
RegisterTool(r, &GetCanvas{Runtime: rt})
5556
RegisterTool(r, &QueryMetricsViewSummary{Runtime: rt})
5657
RegisterTool(r, &QueryMetricsView{Runtime: rt})
5758
RegisterTool(r, &CreateChart{Runtime: rt})

runtime/ai/analyst_agent.go

Lines changed: 123 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,18 @@ type AnalystAgent struct {
2424
var _ Tool[*AnalystAgentArgs, *AnalystAgentResult] = (*AnalystAgent)(nil)
2525

2626
type AnalystAgentArgs struct {
27-
Prompt string `json:"prompt"`
28-
Explore string `json:"explore" yaml:"explore" jsonschema:"Optional explore dashboard name. If provided, the exploration will be limited to this dashboard."`
29-
Dimensions []string `json:"dimensions" yaml:"dimensions" jsonschema:"Optional list of dimensions for queries. If provided, the queries will be limited to these dimensions."`
30-
Measures []string `json:"measures" yaml:"measures" jsonschema:"Optional list of measures for queries. If provided, the queries will be limited to these measures."`
31-
Where *metricsview.Expression `json:"where" yaml:"where" jsonschema:"Optional filter for queries. If provided, this filter will be applied to all queries."`
32-
TimeStart time.Time `json:"time_start" yaml:"time_start" jsonschema:"Optional start time for queries. time_end must be provided if time_start is provided."`
33-
TimeEnd time.Time `json:"time_end" yaml:"time_end" jsonschema:"Optional end time for queries. time_start must be provided if time_end is provided."`
27+
Prompt string `json:"prompt"`
28+
Explore string `json:"explore" yaml:"explore" jsonschema:"Optional explore dashboard name. If provided, the exploration will be limited to this dashboard."`
29+
Dimensions []string `json:"dimensions" yaml:"dimensions" jsonschema:"Optional list of dimensions for queries. If provided, the queries will be limited to these dimensions."`
30+
Measures []string `json:"measures" yaml:"measures" jsonschema:"Optional list of measures for queries. If provided, the queries will be limited to these measures."`
31+
32+
Canvas string `json:"canvas" yaml:"canvas" jsonschema:"Optional canvas name. If provided, the exploration will be limited to this canvas."`
33+
CanvasComponent string `json:"canvas_component" yaml:"canvas_component" jsonschema:"Optional canvas component name. If provided, the exploration will be limited to this canvas component."`
34+
WherePerMetricsView map[string]*metricsview.Expression `json:"where_per_metrics_view" yaml:"where_per_metrics_view" jsonschema:"Optional filter for queries per metrics view. If provided, this filter will be applied to queries for each metrics view."`
35+
36+
Where *metricsview.Expression `json:"where" yaml:"where" jsonschema:"Optional filter for queries. If provided, this filter will be applied to all queries."`
37+
TimeStart time.Time `json:"time_start" yaml:"time_start" jsonschema:"Optional start time for queries. time_end must be provided if time_start is provided."`
38+
TimeEnd time.Time `json:"time_end" yaml:"time_end" jsonschema:"Optional end time for queries. time_start must be provided if time_end is provided."`
3439
}
3540

3641
func (a *AnalystAgentArgs) ToLLM() *aiv1.ContentBlock {
@@ -97,31 +102,53 @@ func (t *AnalystAgent) Handler(ctx context.Context, args *AnalystAgentArgs) (*An
97102

98103
// If a specific dashboard is being explored, we pre-invoke some relevant tool calls for that dashboard.
99104
// TODO: This uses `first`, but that may not be safe if the user has navigated to another dashboard. We probably need some more sophisticated de-duplication here.
100-
var metricsViewName string
101-
if first && args.Explore != "" {
102-
_, metricsView, err := t.getValidExploreAndMetricsView(ctx, args.Explore)
103-
if err != nil {
104-
return nil, err
105-
}
106-
metricsViewName = metricsView.Meta.Name.Name
105+
var metricsViewNames []string
106+
if first {
107+
if args.Explore != "" {
108+
_, metricsView, err := t.getValidExploreAndMetricsView(ctx, args.Explore)
109+
if err != nil {
110+
return nil, err
111+
}
112+
metricsViewNames = append(metricsViewNames, metricsView.Meta.Name.Name)
113+
} else if args.Canvas != "" {
114+
// Pre-invoke the get_canvas tool to get the canvas definition.
115+
_, err := s.CallTool(ctx, RoleAssistant, GetCanvasName, nil, &GetCanvasArgs{
116+
Canvas: args.Canvas,
117+
})
118+
if err != nil {
119+
return nil, err
120+
}
107121

108-
_, err = s.CallTool(ctx, RoleAssistant, QueryMetricsViewSummaryName, nil, &QueryMetricsViewSummaryArgs{
109-
MetricsView: metricsViewName,
110-
})
111-
if err != nil {
112-
return nil, err
122+
_, metricsViews, err := t.getValidCanvasAndMetricsViews(ctx, args.Canvas)
123+
if err != nil {
124+
return nil, err
125+
}
126+
127+
for _, res := range metricsViews {
128+
metricsViewNames = append(metricsViewNames, res.Meta.Name.Name)
129+
}
113130
}
114131

115-
_, err = s.CallTool(ctx, RoleAssistant, GetMetricsViewName, nil, &GetMetricsViewArgs{
116-
MetricsView: metricsViewName,
117-
})
118-
if err != nil {
119-
return nil, err
132+
// Pre-invoke the query_metrics_view tool for each metrics view tied to the explore/canvas.
133+
for _, mvName := range metricsViewNames {
134+
_, err := s.CallTool(ctx, RoleAssistant, QueryMetricsViewSummaryName, nil, &QueryMetricsViewSummaryArgs{
135+
MetricsView: mvName,
136+
})
137+
if err != nil {
138+
return nil, err
139+
}
140+
141+
_, err = s.CallTool(ctx, RoleAssistant, GetMetricsViewName, nil, &GetMetricsViewArgs{
142+
MetricsView: mvName,
143+
})
144+
if err != nil {
145+
return nil, err
146+
}
120147
}
121148
}
122149

123150
// If no specific dashboard is being explored, we pre-invoke the list_metrics_views tool.
124-
if first && args.Explore == "" {
151+
if first && len(metricsViewNames) == 0 {
125152
_, err := s.CallTool(ctx, RoleAssistant, ListMetricsViewsName, nil, &ListMetricsViewsArgs{})
126153
if err != nil {
127154
return nil, err
@@ -131,12 +158,12 @@ func (t *AnalystAgent) Handler(ctx context.Context, args *AnalystAgentArgs) (*An
131158
// Determine tools that can be used
132159
tools := []string{}
133160
if args.Explore == "" {
134-
tools = append(tools, ListMetricsViewsName, GetMetricsViewName)
161+
tools = append(tools, ListMetricsViewsName, GetMetricsViewName, GetCanvasName)
135162
}
136163
tools = append(tools, QueryMetricsViewSummaryName, QueryMetricsViewName, CreateChartName)
137164

138165
// Build completion messages
139-
systemPrompt, err := t.systemPrompt(ctx, metricsViewName, args)
166+
systemPrompt, err := t.systemPrompt(ctx, metricsViewNames, args)
140167
if err != nil {
141168
return nil, err
142169
}
@@ -170,22 +197,30 @@ func (t *AnalystAgent) Handler(ctx context.Context, args *AnalystAgentArgs) (*An
170197
return &AnalystAgentResult{Response: response}, nil
171198
}
172199

173-
func (t *AnalystAgent) systemPrompt(ctx context.Context, metricsViewName string, args *AnalystAgentArgs) (string, error) {
200+
func (t *AnalystAgent) systemPrompt(ctx context.Context, metricsViewNames []string, args *AnalystAgentArgs) (string, error) {
174201
// Prepare template data.
175202
// NOTE: All the template properties are optional and may be empty.
176203
session := GetSession(ctx)
177204
ff, err := t.Runtime.FeatureFlags(ctx, session.InstanceID(), session.Claims())
178205
if err != nil {
179206
return "", fmt.Errorf("failed to get feature flags: %w", err)
180207
}
208+
209+
metricsViewsQuoted := make([]string, len(metricsViewNames))
210+
for i, mv := range metricsViewNames {
211+
metricsViewsQuoted[i] = fmt.Sprintf("`%s`", mv)
212+
}
213+
181214
data := map[string]any{
182-
"ai_instructions": session.ProjectInstructions(),
183-
"metrics_view": metricsViewName,
184-
"explore": args.Explore,
185-
"dimensions": strings.Join(args.Dimensions, ", "),
186-
"measures": strings.Join(args.Measures, ", "),
187-
"feature_flags": ff,
188-
"now": time.Now(),
215+
"ai_instructions": session.ProjectInstructions(),
216+
"metrics_views": strings.Join(metricsViewsQuoted, ", "),
217+
"explore": args.Explore,
218+
"canvas": args.Canvas,
219+
"canvas_component": args.CanvasComponent,
220+
"dimensions": strings.Join(args.Dimensions, ", "),
221+
"measures": strings.Join(args.Measures, ", "),
222+
"feature_flags": ff,
223+
"now": time.Now(),
189224
}
190225

191226
if !args.TimeStart.IsZero() && !args.TimeEnd.IsZero() {
@@ -199,6 +234,18 @@ func (t *AnalystAgent) systemPrompt(ctx context.Context, metricsViewName string,
199234
return "", err
200235
}
201236
}
237+
238+
if args.WherePerMetricsView != nil {
239+
wherePerMetricsView := map[string]string{}
240+
for metricsViewName, whereExpr := range args.WherePerMetricsView {
241+
wherePerMetricsView[metricsViewName], err = metricsview.ExpressionToSQL(whereExpr)
242+
if err != nil {
243+
return "", err
244+
}
245+
}
246+
data["where_per_metrics_view"] = wherePerMetricsView
247+
}
248+
202249
data["forked"] = session.Forked()
203250

204251
// Generate the system prompt
@@ -218,7 +265,7 @@ Today's date is {{ .now.Format "Monday, January 2, 2006" }} ({{ .now.Format "200
218265
<process>
219266
**Phase 1: discovery (setup)**
220267
{{ if .explore }}
221-
Your goal is to analyze the contents of the dashboard "{{ .explore }}", which is powered by the metrics view "{{ .metrics_view }}".
268+
Your goal is to analyze the contents of the dashboard "{{ .explore }}", which is powered by the metrics view(s) {{ .metrics_views }}.
222269
The user is actively viewing this dashboard, and it's what you they refer to if they use expressions like "this dashboard", "the current view", etc.
223270
The metrics view's definition and time range of available data has been provided in your tool calls.
224271
@@ -231,6 +278,22 @@ Here is an overview of the settings the user has currently applied to the dashbo
231278
You should:
232279
1. Carefully study the metrics view definition to understand the measures and dimensions available for analysis.
233280
2. Remember the time range of available data and use it to inform and filter your queries.
281+
{{ else if .canvas }}
282+
Your goal is to analyze the contents of the canvas "{{ .canvas }}", which is powered by the metrics view(s) {{ .metrics_views }}.
283+
The user is actively viewing this dashboard, and it's what you they refer to if they use expressions like "this dashboard", "the current view", etc.
284+
The metrics views and canvas definitions have been provided in your tool calls.
285+
286+
Here is an overview of the settings the user has currently applied to the dashboard (Merge component's dimension_filters with "and"):
287+
{{ if (and .time_start .time_end) }}Use time range: start={{.time_start}}, end={{.time_end}}{{ end }}
288+
{{ if .where_per_metrics_view }}{{range $mv, $filter := .where_per_metrics_view}}Use where filters for metrics view "{{ $mv }}": "{{ $filter }}"
289+
{{end}}{{ end }}
290+
291+
You should:
292+
1. Carefully study the canvas and metrics view definition to understand the measures and dimensions available for analysis.
293+
2. Remember the time range of available data and use it to inform and filter your queries.
294+
{{ if .canvas_component }}
295+
The user is looking at "{{ .canvas_component }}".
296+
{{ end }}
234297
{{ else }}
235298
Follow these steps in order:
236299
1. **Discover**: Use "list_metrics_views" to identify available datasets
@@ -391,3 +454,27 @@ func (t *AnalystAgent) getValidExploreAndMetricsView(ctx context.Context, explor
391454

392455
return explore, metricsView, nil
393456
}
457+
458+
func (t *AnalystAgent) getValidCanvasAndMetricsViews(ctx context.Context, canvasName string) (*runtimev1.Resource, map[string]*runtimev1.Resource, error) {
459+
session := GetSession(ctx)
460+
461+
resolvedCanvas, err := t.Runtime.ResolveCanvas(ctx, session.InstanceID(), canvasName, session.Claims())
462+
if err != nil {
463+
return nil, nil, err
464+
}
465+
466+
if resolvedCanvas == nil || resolvedCanvas.Canvas == nil {
467+
return nil, nil, fmt.Errorf("canvas %q not found", canvasName)
468+
}
469+
470+
metricsViews := map[string]*runtimev1.Resource{}
471+
for mv, res := range resolvedCanvas.ReferencedMetricsViews {
472+
metricsView := res.GetMetricsView()
473+
if metricsView == nil || metricsView.State.ValidSpec == nil {
474+
continue
475+
}
476+
metricsViews[mv] = res
477+
}
478+
479+
return resolvedCanvas.Canvas, metricsViews, nil
480+
}

0 commit comments

Comments
 (0)