diff --git a/docs/docs/reference/project-files/alerts.md b/docs/docs/reference/project-files/alerts.md index 5f95fe9e29ef..87f7febb21b6 100644 --- a/docs/docs/reference/project-files/alerts.md +++ b/docs/docs/reference/project-files/alerts.md @@ -119,37 +119,31 @@ _[oneOf]_ - Data source for the alert _(required)_ - **`prompt`** - _[string]_ - Custom prompt to guide the AI analysis. If not provided, a default analysis prompt is used. - - **`time_range`** - _[object]_ - Time range for the analysis period + - **`time_range`** - _[object]_ - Time range for the analysis period. Use either a Rill time expression or fixed start and end timestamps. - - **`iso_duration`** - _[string]_ - ISO 8601 duration (e.g., P7D for 7 days, P1M for 1 month) - - - **`iso_offset`** - _[string]_ - ISO 8601 offset from current time (e.g., P1D to start from yesterday) + - **`expression`** - _[string]_ - Rill time expression (e.g., '7D as of latest/D', '1M as of latest/M') - **`start`** - _[string]_ - Start timestamp in ISO 8601 format - **`end`** - _[string]_ - End timestamp in ISO 8601 format - - **`expression`** - _[string]_ - Rill time expression (e.g., 'last 7 days', 'this month') - - - **`comparison_time_range`** - _[object]_ - Optional comparison time range for period-over-period analysis - - - **`iso_duration`** - _[string]_ - ISO 8601 duration for comparison period + - **`comparison_time_range`** - _[object]_ - Optional comparison time range for period-over-period analysis. Use either a Rill time expression or fixed start and end timestamps. - - **`iso_offset`** - _[string]_ - ISO 8601 offset for comparison period (e.g., P7D to compare with previous week) + - **`expression`** - _[string]_ - Rill time expression for the comparison period (e.g., '7D as of latest/D offset -7D') - **`start`** - _[string]_ - Start timestamp in ISO 8601 format - **`end`** - _[string]_ - End timestamp in ISO 8601 format - - **`expression`** - _[string]_ - Rill time expression for comparison period + - **`time_zone`** - _[string]_ - IANA time zone used to evaluate the time range expressions (e.g., 'America/New_York'). Defaults to UTC. - - **`context`** - _[object]_ - Context to constrain the AI analysis + - **`explore`** - _[string]_ - Name of the explore dashboard to analyze. If provided, the analysis is limited to the metrics view of this dashboard. Combined with `watermark: inherit` on the report, time range expressions are resolved against the latest data in the metrics view instead of the report's trigger time. - - **`explore`** - _[string]_ - Name of the explore dashboard to analyze + - **`dimensions`** - _[array of string]_ - List of dimensions to include in the analysis - - **`dimensions`** - _[array of string]_ - List of dimensions to include in analysis + - **`measures`** - _[array of string]_ - List of measures to include in the analysis - - **`measures`** - _[array of string]_ - List of measures to include in analysis + - **`where`** - _[object]_ - Optional filter expression to apply to the analysis, in the same format as metrics view query filters ### `for` diff --git a/docs/docs/reference/project-files/models.md b/docs/docs/reference/project-files/models.md index d7f3b73a74e2..3e3358aec2d5 100644 --- a/docs/docs/reference/project-files/models.md +++ b/docs/docs/reference/project-files/models.md @@ -182,37 +182,31 @@ _[oneOf]_ - Refers to the explicitly defined state of your model, cannot be used - **`prompt`** - _[string]_ - Custom prompt to guide the AI analysis. If not provided, a default analysis prompt is used. - - **`time_range`** - _[object]_ - Time range for the analysis period + - **`time_range`** - _[object]_ - Time range for the analysis period. Use either a Rill time expression or fixed start and end timestamps. - - **`iso_duration`** - _[string]_ - ISO 8601 duration (e.g., P7D for 7 days, P1M for 1 month) - - - **`iso_offset`** - _[string]_ - ISO 8601 offset from current time (e.g., P1D to start from yesterday) + - **`expression`** - _[string]_ - Rill time expression (e.g., '7D as of latest/D', '1M as of latest/M') - **`start`** - _[string]_ - Start timestamp in ISO 8601 format - **`end`** - _[string]_ - End timestamp in ISO 8601 format - - **`expression`** - _[string]_ - Rill time expression (e.g., 'last 7 days', 'this month') - - - **`comparison_time_range`** - _[object]_ - Optional comparison time range for period-over-period analysis - - - **`iso_duration`** - _[string]_ - ISO 8601 duration for comparison period + - **`comparison_time_range`** - _[object]_ - Optional comparison time range for period-over-period analysis. Use either a Rill time expression or fixed start and end timestamps. - - **`iso_offset`** - _[string]_ - ISO 8601 offset for comparison period (e.g., P7D to compare with previous week) + - **`expression`** - _[string]_ - Rill time expression for the comparison period (e.g., '7D as of latest/D offset -7D') - **`start`** - _[string]_ - Start timestamp in ISO 8601 format - **`end`** - _[string]_ - End timestamp in ISO 8601 format - - **`expression`** - _[string]_ - Rill time expression for comparison period + - **`time_zone`** - _[string]_ - IANA time zone used to evaluate the time range expressions (e.g., 'America/New_York'). Defaults to UTC. - - **`context`** - _[object]_ - Context to constrain the AI analysis + - **`explore`** - _[string]_ - Name of the explore dashboard to analyze. If provided, the analysis is limited to the metrics view of this dashboard. Combined with `watermark: inherit` on the report, time range expressions are resolved against the latest data in the metrics view instead of the report's trigger time. - - **`explore`** - _[string]_ - Name of the explore dashboard to analyze + - **`dimensions`** - _[array of string]_ - List of dimensions to include in the analysis - - **`dimensions`** - _[array of string]_ - List of dimensions to include in analysis + - **`measures`** - _[array of string]_ - List of measures to include in the analysis - - **`measures`** - _[array of string]_ - List of measures to include in analysis + - **`where`** - _[object]_ - Optional filter expression to apply to the analysis, in the same format as metrics view query filters ```yaml state: @@ -280,37 +274,31 @@ _[oneOf]_ - Refers to how your data is partitioned; cannot be used with state. ( - **`prompt`** - _[string]_ - Custom prompt to guide the AI analysis. If not provided, a default analysis prompt is used. - - **`time_range`** - _[object]_ - Time range for the analysis period + - **`time_range`** - _[object]_ - Time range for the analysis period. Use either a Rill time expression or fixed start and end timestamps. - - **`iso_duration`** - _[string]_ - ISO 8601 duration (e.g., P7D for 7 days, P1M for 1 month) - - - **`iso_offset`** - _[string]_ - ISO 8601 offset from current time (e.g., P1D to start from yesterday) + - **`expression`** - _[string]_ - Rill time expression (e.g., '7D as of latest/D', '1M as of latest/M') - **`start`** - _[string]_ - Start timestamp in ISO 8601 format - **`end`** - _[string]_ - End timestamp in ISO 8601 format - - **`expression`** - _[string]_ - Rill time expression (e.g., 'last 7 days', 'this month') - - - **`comparison_time_range`** - _[object]_ - Optional comparison time range for period-over-period analysis - - - **`iso_duration`** - _[string]_ - ISO 8601 duration for comparison period + - **`comparison_time_range`** - _[object]_ - Optional comparison time range for period-over-period analysis. Use either a Rill time expression or fixed start and end timestamps. - - **`iso_offset`** - _[string]_ - ISO 8601 offset for comparison period (e.g., P7D to compare with previous week) + - **`expression`** - _[string]_ - Rill time expression for the comparison period (e.g., '7D as of latest/D offset -7D') - **`start`** - _[string]_ - Start timestamp in ISO 8601 format - **`end`** - _[string]_ - End timestamp in ISO 8601 format - - **`expression`** - _[string]_ - Rill time expression for comparison period + - **`time_zone`** - _[string]_ - IANA time zone used to evaluate the time range expressions (e.g., 'America/New_York'). Defaults to UTC. - - **`context`** - _[object]_ - Context to constrain the AI analysis + - **`explore`** - _[string]_ - Name of the explore dashboard to analyze. If provided, the analysis is limited to the metrics view of this dashboard. Combined with `watermark: inherit` on the report, time range expressions are resolved against the latest data in the metrics view instead of the report's trigger time. - - **`explore`** - _[string]_ - Name of the explore dashboard to analyze + - **`dimensions`** - _[array of string]_ - List of dimensions to include in the analysis - - **`dimensions`** - _[array of string]_ - List of dimensions to include in analysis + - **`measures`** - _[array of string]_ - List of measures to include in the analysis - - **`measures`** - _[array of string]_ - List of measures to include in analysis + - **`where`** - _[object]_ - Optional filter expression to apply to the analysis, in the same format as metrics view query filters ```yaml partitions: diff --git a/docs/docs/reference/project-files/reports.md b/docs/docs/reference/project-files/reports.md index 06b5fc2df153..4359e3deb329 100644 --- a/docs/docs/reference/project-files/reports.md +++ b/docs/docs/reference/project-files/reports.md @@ -121,37 +121,31 @@ Supports ai resolvers only as of now. - **`prompt`** - _[string]_ - Custom prompt to guide the AI analysis. If not provided, a default analysis prompt is used. - - **`time_range`** - _[object]_ - Time range for the analysis period + - **`time_range`** - _[object]_ - Time range for the analysis period. Use either a Rill time expression or fixed start and end timestamps. - - **`iso_duration`** - _[string]_ - ISO 8601 duration (e.g., P7D for 7 days, P1M for 1 month) - - - **`iso_offset`** - _[string]_ - ISO 8601 offset from current time (e.g., P1D to start from yesterday) + - **`expression`** - _[string]_ - Rill time expression (e.g., '7D as of latest/D', '1M as of latest/M') - **`start`** - _[string]_ - Start timestamp in ISO 8601 format - **`end`** - _[string]_ - End timestamp in ISO 8601 format - - **`expression`** - _[string]_ - Rill time expression (e.g., 'last 7 days', 'this month') - - - **`comparison_time_range`** - _[object]_ - Optional comparison time range for period-over-period analysis - - - **`iso_duration`** - _[string]_ - ISO 8601 duration for comparison period + - **`comparison_time_range`** - _[object]_ - Optional comparison time range for period-over-period analysis. Use either a Rill time expression or fixed start and end timestamps. - - **`iso_offset`** - _[string]_ - ISO 8601 offset for comparison period (e.g., P7D to compare with previous week) + - **`expression`** - _[string]_ - Rill time expression for the comparison period (e.g., '7D as of latest/D offset -7D') - **`start`** - _[string]_ - Start timestamp in ISO 8601 format - **`end`** - _[string]_ - End timestamp in ISO 8601 format - - **`expression`** - _[string]_ - Rill time expression for comparison period + - **`time_zone`** - _[string]_ - IANA time zone used to evaluate the time range expressions (e.g., 'America/New_York'). Defaults to UTC. - - **`context`** - _[object]_ - Context to constrain the AI analysis + - **`explore`** - _[string]_ - Name of the explore dashboard to analyze. If provided, the analysis is limited to the metrics view of this dashboard. Combined with `watermark: inherit` on the report, time range expressions are resolved against the latest data in the metrics view instead of the report's trigger time. - - **`explore`** - _[string]_ - Name of the explore dashboard to analyze + - **`dimensions`** - _[array of string]_ - List of dimensions to include in the analysis - - **`dimensions`** - _[array of string]_ - List of dimensions to include in analysis + - **`measures`** - _[array of string]_ - List of measures to include in the analysis - - **`measures`** - _[array of string]_ - List of measures to include in analysis + - **`where`** - _[object]_ - Optional filter expression to apply to the analysis, in the same format as metrics view query filters ### `query` @@ -257,8 +251,7 @@ data: expression: "1D as of latest/D" comparison_time_range: expression: "1D as of latest/D offset -1D" - context: - explore: my_explore + explore: my_explore notify: email: recipients: diff --git a/runtime/parser/parse_partial_data.go b/runtime/parser/parse_partial_data.go index 484ec1e3a8ba..b0c6f5afc196 100644 --- a/runtime/parser/parse_partial_data.go +++ b/runtime/parser/parse_partial_data.go @@ -116,6 +116,11 @@ func (p *Parser) parseDataYAML(paths []string, raw *DataYAML, contextualConnecto count++ resolver = "ai" resolverProps = raw.AI + // The agent can access any metrics view, so the explore is the only dependency known up front. + // Adding it as a ref makes reports wait for it to reconcile and lets watermark: inherit resolve time ranges against its data. + if explore, ok := raw.AI["explore"].(string); ok && explore != "" { + refs = append(refs, ResourceName{Kind: ResourceKindExplore, Name: explore}) + } } // Handle union resolver diff --git a/runtime/parser/parser_test.go b/runtime/parser/parser_test.go index d8b28163a898..01154d26fca2 100644 --- a/runtime/parser/parser_test.go +++ b/runtime/parser/parser_test.go @@ -1524,6 +1524,94 @@ annotations: requireResourcesAndErrors(t, p, resources, nil) } +func TestReportAIExploreRef(t *testing.T) { + ctx := context.Background() + repo := makeRepo(t, map[string]string{ + `rill.yaml`: ``, + `reports/r1.yaml`: ` +type: report +display_name: AI Report + +refresh: + cron: 0 8 * * * + +watermark: inherit + +data: + ai: + prompt: Analyze key metrics + time_range: + expression: 1D as of latest/D + explore: e1 + +notify: + email: + recipients: + - user_1@example.com +`, + // Without an explore, the report has no refs. + `reports/r2.yaml`: ` +type: report +display_name: AI Report + +refresh: + cron: 0 8 * * * + +data: + ai: + prompt: Analyze key metrics + +notify: + email: + recipients: + - user_1@example.com +`, + }) + + resources := []*Resource{ + { + Name: ResourceName{Kind: ResourceKindReport, Name: "r1"}, + Paths: []string{"/reports/r1.yaml"}, + Refs: []ResourceName{{Kind: ResourceKindExplore, Name: "e1"}}, + ReportSpec: &runtimev1.ReportSpec{ + DisplayName: "AI Report", + RefreshSchedule: &runtimev1.Schedule{Cron: "0 8 * * *"}, + Resolver: "ai", + ResolverProperties: must(structpb.NewStruct(map[string]any{ + "prompt": "Analyze key metrics", + "time_range": map[string]any{"expression": "1D as of latest/D"}, + "explore": "e1", + })), + Notifiers: []*runtimev1.Notifier{{ + Connector: "email", + Properties: must(structpb.NewStruct(map[string]any{"recipients": []any{"user_1@example.com"}})), + }}, + WatermarkInherit: true, + }, + }, + { + Name: ResourceName{Kind: ResourceKindReport, Name: "r2"}, + Paths: []string{"/reports/r2.yaml"}, + ReportSpec: &runtimev1.ReportSpec{ + DisplayName: "AI Report", + RefreshSchedule: &runtimev1.Schedule{Cron: "0 8 * * *"}, + Resolver: "ai", + ResolverProperties: must(structpb.NewStruct(map[string]any{ + "prompt": "Analyze key metrics", + })), + Notifiers: []*runtimev1.Notifier{{ + Connector: "email", + Properties: must(structpb.NewStruct(map[string]any{"recipients": []any{"user_1@example.com"}})), + }}, + }, + }, + } + + p, err := Parse(ctx, repo, "", "", "duckdb", true) + require.NoError(t, err) + requireResourcesAndErrors(t, p, resources, nil) +} + func TestReportPdfValidation(t *testing.T) { ctx := context.Background() repo := makeRepo(t, map[string]string{ diff --git a/runtime/parser/schema/project.schema.yaml b/runtime/parser/schema/project.schema.yaml index eaac919df70e..43a970965520 100644 --- a/runtime/parser/schema/project.schema.yaml +++ b/runtime/parser/schema/project.schema.yaml @@ -2881,8 +2881,7 @@ definitions: expression: "1D as of latest/D" comparison_time_range: expression: "1D as of latest/D offset -1D" - context: - explore: my_explore + explore: my_explore notify: email: recipients: @@ -3764,71 +3763,59 @@ definitions: description: Custom prompt to guide the AI analysis. If not provided, a default analysis prompt is used. time_range: type: object - description: Time range for the analysis period + description: Time range for the analysis period. Use either a Rill time expression or fixed start and end timestamps. properties: - iso_duration: - type: string - description: ISO 8601 duration (e.g., P7D for 7 days, P1M for 1 month) - iso_offset: + expression: type: string - description: ISO 8601 offset from current time (e.g., P1D to start from yesterday) + description: Rill time expression (e.g., '7D as of latest/D', '1M as of latest/M') start: type: string description: Start timestamp in ISO 8601 format end: type: string description: End timestamp in ISO 8601 format - expression: - type: string - description: Rill time expression (e.g., 'last 7 days', 'this month') comparison_time_range: type: object - description: Optional comparison time range for period-over-period analysis + description: Optional comparison time range for period-over-period analysis. Use either a Rill time expression or fixed start and end timestamps. properties: - iso_duration: - type: string - description: ISO 8601 duration for comparison period - iso_offset: + expression: type: string - description: ISO 8601 offset for comparison period (e.g., P7D to compare with previous week) + description: Rill time expression for the comparison period (e.g., '7D as of latest/D offset -7D') start: type: string description: Start timestamp in ISO 8601 format end: type: string description: End timestamp in ISO 8601 format - expression: - type: string - description: Rill time expression for comparison period - context: + time_zone: + type: string + description: IANA time zone used to evaluate the time range expressions (e.g., 'America/New_York'). Defaults to UTC. + explore: + type: string + description: "Name of the explore dashboard to analyze. If provided, the analysis is limited to the metrics view of this dashboard. Combined with `watermark: inherit` on the report, time range expressions are resolved against the latest data in the metrics view instead of the report's trigger time." + dimensions: + type: array + items: + type: string + description: List of dimensions to include in the analysis + measures: + type: array + items: + type: string + description: List of measures to include in the analysis + where: type: object - description: Context to constrain the AI analysis - properties: - explore: - type: string - description: Name of the explore dashboard to analyze - dimensions: - type: array - items: - type: string - description: List of dimensions to include in analysis - measures: - type: array - items: - type: string - description: List of measures to include in analysis + description: Optional filter expression to apply to the analysis, in the same format as metrics view query filters required: - ai examples: - ai: prompt: "Analyze revenue trends and identify top performing regions" time_range: - iso_duration: P7D + expression: "7D as of latest/D" comparison_time_range: - iso_duration: P7D - iso_offset: P7D - context: - explore: sales_explore + expression: "7D as of latest/D offset -7D" + explore: sales_explore explore_time_range_properties: oneOf: diff --git a/runtime/reconcilers/report.go b/runtime/reconcilers/report.go index 59e896685993..9fbaa3bf67d3 100644 --- a/runtime/reconcilers/report.go +++ b/runtime/reconcilers/report.go @@ -887,6 +887,20 @@ func formatExportFormat(f runtimev1.ExportFormat) string { func (r *ReportReconciler) computeInheritedWatermark(ctx context.Context, refs []*runtimev1.ResourceName) (time.Time, bool, error) { var t time.Time for _, ref := range refs { + // Explores (referenced by AI reports) inherit the watermark of their metrics view. + // Resolving the metrics view here instead of in the watermark query keeps the query's cache keyed on the metrics view's data. + if ref.Kind == runtime.ResourceKindExplore { + res, err := r.C.Get(ctx, ref, false) + if err != nil { + return t, false, fmt.Errorf("failed to get explore %q: %w", ref.Name, err) + } + spec := res.GetExplore().State.ValidSpec + if spec == nil { + return t, false, fmt.Errorf("explore %q is not valid", ref.Name) + } + ref = &runtimev1.ResourceName{Kind: runtime.ResourceKindMetricsView, Name: spec.MetricsView} + } + q := &queries.ResourceWatermark{ ResourceKind: ref.Kind, ResourceName: ref.Name, diff --git a/runtime/reconcilers/report_test.go b/runtime/reconcilers/report_test.go index 876e50778adb..3ddb1f57c930 100644 --- a/runtime/reconcilers/report_test.go +++ b/runtime/reconcilers/report_test.go @@ -2,6 +2,7 @@ package reconcilers_test import ( "testing" + "time" runtimev1 "github.com/rilldata/rill/proto/gen/rill/runtime/v1" "github.com/rilldata/rill/runtime" @@ -9,6 +10,84 @@ import ( "github.com/stretchr/testify/require" ) +func TestReportAIExploreWatermarkInherit(t *testing.T) { + rt, id := testruntime.NewInstance(t) + testruntime.PutFiles(t, rt, id, map[string]string{ + "/models/bar.sql": ` +SELECT '2024-01-01T00:00:00Z'::TIMESTAMP as __time, 'Denmark' as country +`, + "/metrics/mv1.yaml": ` +version: 1 +type: metrics_view +model: bar +timeseries: __time +dimensions: +- column: country +measures: +- expression: count(*) +`, + "/explores/e1.yaml": ` +type: explore +metrics_view: mv1 +`, + // AI report that inherits its watermark through the explore + "/reports/r1.yaml": ` +type: report +display_name: AI Report +refresh: + cron: 0 8 * * * +watermark: inherit +data: + ai: + prompt: Analyze key metrics + explore: e1 +notify: + email: + recipients: + - somebody@example.com +`, + // Same report without watermark: inherit, which should use the trigger time + "/reports/r2.yaml": ` +type: report +display_name: AI Report +refresh: + cron: 0 8 * * * +data: + ai: + prompt: Analyze key metrics + explore: e1 +notify: + email: + recipients: + - somebody@example.com +`, + }) + testruntime.ReconcileParserAndWait(t, rt, id) + testruntime.RequireReconcileState(t, rt, id, 6, 0, 0) + + r1 := testruntime.GetResource(t, rt, id, runtime.ResourceKindReport, "r1") + require.Len(t, r1.Meta.Refs, 1) + require.Equal(t, runtime.ResourceKindExplore, r1.Meta.Refs[0].Kind) + require.Equal(t, "e1", r1.Meta.Refs[0].Name) + + // Trigger both reports. + // The execution's report time is recorded before the report is sent, so it is available regardless of whether the AI session could be created. + triggerTime := time.Now() + testruntime.RefreshAndWait(t, rt, id, &runtimev1.ResourceName{Kind: runtime.ResourceKindReport, Name: "r1"}) + testruntime.RefreshAndWait(t, rt, id, &runtimev1.ResourceName{Kind: runtime.ResourceKindReport, Name: "r2"}) + + // r1 should have resolved the watermark of the explore's metrics view. + // The watermark is max(__time) plus one second, since it's used as an exclusive upper bound (see ResourceWatermark). + r1 = testruntime.GetResource(t, rt, id, runtime.ResourceKindReport, "r1") + require.Len(t, r1.GetReport().State.ExecutionHistory, 1) + require.Equal(t, time.Date(2024, 1, 1, 0, 0, 1, 0, time.UTC), r1.GetReport().State.ExecutionHistory[0].ReportTime.AsTime()) + + // r2 should have used the trigger time + r2 := testruntime.GetResource(t, rt, id, runtime.ResourceKindReport, "r2") + require.Len(t, r2.GetReport().State.ExecutionHistory, 1) + require.WithinDuration(t, triggerTime, r2.GetReport().State.ExecutionHistory[0].ReportTime.AsTime(), time.Minute) +} + func TestReportCanvasResolveTransitiveAccess(t *testing.T) { rt, id := testruntime.NewInstanceWithOptions(t, testruntime.InstanceOptions{ Files: map[string]string{"rill.yaml": ""},