Add daily generated text assets
This commit is contained in:
38
internal/reporttemplate/prompts/daily.generated_text.md
Normal file
38
internal/reporttemplate/prompts/daily.generated_text.md
Normal file
@@ -0,0 +1,38 @@
|
||||
TASK: You are writing structured prose slots for a dated daily weather report.
|
||||
|
||||
The calling application will render the final Markdown report. Your job is not to write the full report. Return only a JSON object matching the configured schema.
|
||||
|
||||
Use only the supplied `data_package`. Do not invent weather details, times, hazards, probabilities, or impacts that are not supported by the data.
|
||||
|
||||
The report focuses on the selected local civil day in `report.valid_period` for the configured location.
|
||||
|
||||
Return these fields:
|
||||
|
||||
- `summary`: required. 1-2 sentences summarizing the main weather story for the selected day.
|
||||
- `forecast_discussion`: required. 1 or more short paragraphs explaining the setup, timing, trend, or uncertainty most relevant to the selected day.
|
||||
- `precipitation_timing`: optional. Include only when the deterministic `precip_timing` module contains precipitation windows.
|
||||
- `confidence`: optional. Include only if uncertainty, timing spread, or conflicting signals materially affect how the reader should interpret the forecast.
|
||||
|
||||
Return JSON only.
|
||||
|
||||
# summary
|
||||
|
||||
Lead with the most practical local outcome for the selected day. If an active warning is relevant during the report period, lead with the hazard.
|
||||
|
||||
Mention the expected temperature character, precipitation risk, wind, visibility, heat, cold, or other hazards only when supported by the data package.
|
||||
|
||||
# forecast_discussion
|
||||
|
||||
Use deterministic module facts and narrative products to explain the most useful details for the selected day.
|
||||
|
||||
Useful context may include:
|
||||
|
||||
- timing of condition changes by daypart or hour
|
||||
- boundaries, forcing, moisture, instability, or storm mode when supported
|
||||
- active alerts or SPC outlooks that apply to the location
|
||||
- planning concerns surfaced by `daily_planning`
|
||||
- confidence or uncertainty
|
||||
|
||||
# precipitation_timing
|
||||
|
||||
Use 1-2 sentences to add practical precipitation context only when the data package contains deterministic precipitation windows. Include expected timing, type, intensity, duration, and uncertainty only when those details are supported.
|
||||
@@ -12,12 +12,14 @@ import (
|
||||
var assets embed.FS
|
||||
|
||||
var templates = map[string]string{
|
||||
"daily": "templates/daily.md.tmpl",
|
||||
"hourly": "templates/hourly.md.tmpl",
|
||||
"today": "templates/today.md.tmpl",
|
||||
"tomorrow": "templates/tomorrow.md.tmpl",
|
||||
}
|
||||
|
||||
var schemas = map[string]string{
|
||||
"daily": "schemas/daily.generated_text.schema.json",
|
||||
"hourly": "schemas/hourly.generated_text.schema.json",
|
||||
"today": "schemas/today.generated_text.schema.json",
|
||||
"tomorrow": "schemas/tomorrow.generated_text.schema.json",
|
||||
|
||||
@@ -2,6 +2,7 @@ package reporttemplate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -30,6 +31,18 @@ func TestTomorrowTemplateLookup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDailyTemplateLookup(t *testing.T) {
|
||||
source, err := Template("daily")
|
||||
if err != nil {
|
||||
t.Fatalf("Template() error = %v", err)
|
||||
}
|
||||
for _, want := range []string{"# {{ .Report.Title }}", "**Forecast date:**", "## Daypart Forecast", "## Precipitation Timing", "## Forecast Discussion"} {
|
||||
if !strings.Contains(source, want) {
|
||||
t.Fatalf("template missing %q:\n%s", want, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTodayTemplateLookup(t *testing.T) {
|
||||
source, err := Template("today")
|
||||
if err != nil {
|
||||
@@ -84,6 +97,37 @@ func TestTomorrowSchemaLookup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDailySchemaLookup(t *testing.T) {
|
||||
data, err := Schema("daily")
|
||||
if err != nil {
|
||||
t.Fatalf("Schema() error = %v", err)
|
||||
}
|
||||
schema := assertSchema(t, data, "summary,forecast_discussion")
|
||||
property, ok := schema.Properties["forecast_discussion"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("schema property forecast_discussion missing or invalid")
|
||||
}
|
||||
if property["type"] != "array" {
|
||||
t.Fatalf("forecast_discussion type = %v, want array", property["type"])
|
||||
}
|
||||
if property["minItems"] != float64(1) {
|
||||
t.Fatalf("forecast_discussion minItems = %v, want 1", property["minItems"])
|
||||
}
|
||||
items, ok := property["items"].(map[string]any)
|
||||
if !ok || items["type"] != "string" {
|
||||
t.Fatalf("forecast_discussion items = %#v, want string items", property["items"])
|
||||
}
|
||||
for _, field := range []string{"summary", "precipitation_timing", "confidence"} {
|
||||
property, ok := schema.Properties[field].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("schema property %q missing or invalid", field)
|
||||
}
|
||||
if property["type"] != "string" {
|
||||
t.Fatalf("schema property %q type = %v, want string", field, property["type"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTodaySchemaLookup(t *testing.T) {
|
||||
data, err := Schema("today")
|
||||
if err != nil {
|
||||
@@ -351,6 +395,85 @@ func TestRenderTomorrow(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRenderDaily(t *testing.T) {
|
||||
rendered, err := Render("daily", testDailyRenderContext{
|
||||
Report: testDailyReportContext{
|
||||
Title: "Monday's Weather",
|
||||
ForecastDateLabel: "Monday, June 15, 2026",
|
||||
GeneratedAtLabel: "Sunday, June 14, 2026 at 9:14 AM",
|
||||
},
|
||||
GeneratedText: testDailyGeneratedText{
|
||||
Summary: "The selected day starts dry before showers return later in the day.",
|
||||
ForecastDiscussion: []string{
|
||||
"Clouds increase after sunrise.",
|
||||
"Rain chances peak during the afternoon.",
|
||||
},
|
||||
PrecipitationTiming: "A few showers may linger into early evening.",
|
||||
},
|
||||
Modules: testDailyModules{
|
||||
Dayparts: []testDailyDaypart{
|
||||
{
|
||||
Key: "morning",
|
||||
Summary: testDaypartSummary{
|
||||
DisplayName: "Morning",
|
||||
TemperatureTrend: "rising",
|
||||
TemperatureStartPhraseF: "upper 50s",
|
||||
TemperatureEndPhraseF: "upper 60s",
|
||||
DominantConditionDisplay: "Sunny",
|
||||
DominantConditionLower: "sunny",
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: "afternoon",
|
||||
Summary: testDaypartSummary{
|
||||
DisplayName: "Afternoon",
|
||||
TemperatureTrend: "steady",
|
||||
TemperatureSteadyPhraseF: "upper 70s",
|
||||
DominantConditionDisplay: "Showers",
|
||||
DominantConditionLower: "showers",
|
||||
MaxPopPercent: intPtr(70),
|
||||
MaxPopTimeLabel: "3:00 PM",
|
||||
MentionPrecipitation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
PrecipTiming: &testPrecipTiming{
|
||||
PrecipitationWindows: []testPrecipWindow{
|
||||
{PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Render() error = %v", err)
|
||||
}
|
||||
text := string(rendered)
|
||||
for _, want := range []string{
|
||||
"# Monday's Weather",
|
||||
"**Forecast date:** Monday, June 15, 2026",
|
||||
"**Updated:** Sunday, June 14, 2026 at 9:14 AM",
|
||||
"The selected day starts dry before showers return later in the day.",
|
||||
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.",
|
||||
"- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%.",
|
||||
"- **3:00 PM** to **6:00 PM**: Precipitation is expected during this period. The peak precipitation chance is 70% at 3:00 PM.",
|
||||
"A few showers may linger into early evening.",
|
||||
"Clouds increase after sunrise.",
|
||||
"Rain chances peak during the afternoon.",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
assertOrderedText(t, text, []string{
|
||||
"# Monday's Weather",
|
||||
"## Daypart Forecast",
|
||||
"- **Morning:**",
|
||||
"- **Afternoon:**",
|
||||
"## Precipitation Timing",
|
||||
"## Forecast Discussion",
|
||||
})
|
||||
}
|
||||
|
||||
func TestRenderToday(t *testing.T) {
|
||||
rendered, err := Render("today", testTodayRenderContext{
|
||||
Report: testTodayReportContext{
|
||||
@@ -532,17 +655,29 @@ func TestRenderHourlyOmitsConditionalSectionsForClearWeather(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUnknownAssetsReturnActionableErrors(t *testing.T) {
|
||||
if _, err := Template("daily"); err == nil || !strings.Contains(err.Error(), `unknown report template "daily"`) {
|
||||
if _, err := Template("missing"); err == nil || !strings.Contains(err.Error(), `unknown report template "missing"`) {
|
||||
t.Fatalf("Template() error = %v, want unknown template", err)
|
||||
}
|
||||
if _, err := Schema("daily"); err == nil || !strings.Contains(err.Error(), `unknown generated text schema "daily"`) {
|
||||
if _, err := Schema("missing"); err == nil || !strings.Contains(err.Error(), `unknown generated text schema "missing"`) {
|
||||
t.Fatalf("Schema() error = %v, want unknown schema", err)
|
||||
}
|
||||
if _, err := Render("daily", testRenderContext{}); err == nil || !strings.Contains(err.Error(), `unknown report template "daily"`) {
|
||||
if _, err := Render("missing", testRenderContext{}); err == nil || !strings.Contains(err.Error(), `unknown report template "missing"`) {
|
||||
t.Fatalf("Render() error = %v, want unknown template", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDailyPromptAssetExists(t *testing.T) {
|
||||
data, err := os.ReadFile("prompts/daily.generated_text.md")
|
||||
if err != nil {
|
||||
t.Fatalf("read Daily prompt asset: %v", err)
|
||||
}
|
||||
for _, want := range []string{"TASK:", "`summary`", "`forecast_discussion`", "`daily_planning`"} {
|
||||
if !strings.Contains(string(data), want) {
|
||||
t.Fatalf("Daily prompt asset missing %q:\n%s", want, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderFailsForMissingContextFields(t *testing.T) {
|
||||
_, err := Render("hourly", map[string]any{"Report": map[string]any{"Title": "Hourly Report"}})
|
||||
if err == nil {
|
||||
@@ -571,6 +706,12 @@ type testTodayRenderContext struct {
|
||||
Modules testTodayModules
|
||||
}
|
||||
|
||||
type testDailyRenderContext struct {
|
||||
Report testDailyReportContext
|
||||
GeneratedText testDailyGeneratedText
|
||||
Modules testDailyModules
|
||||
}
|
||||
|
||||
type testReportContext struct {
|
||||
Title string
|
||||
LocationName string
|
||||
@@ -597,6 +738,12 @@ type testTodayReportContext struct {
|
||||
GeneratedAtLabel string
|
||||
}
|
||||
|
||||
type testDailyReportContext struct {
|
||||
Title string
|
||||
ForecastDateLabel string
|
||||
GeneratedAtLabel string
|
||||
}
|
||||
|
||||
type testTomorrowGeneratedText struct {
|
||||
Summary string
|
||||
ForecastDiscussion []string
|
||||
@@ -604,6 +751,13 @@ type testTomorrowGeneratedText struct {
|
||||
Confidence string
|
||||
}
|
||||
|
||||
type testDailyGeneratedText struct {
|
||||
Summary string
|
||||
ForecastDiscussion []string
|
||||
PrecipitationTiming string
|
||||
Confidence string
|
||||
}
|
||||
|
||||
type testModules struct {
|
||||
CurrentConditions *testCurrentConditions
|
||||
HourlyForecast *testHourlyForecast
|
||||
@@ -627,6 +781,11 @@ type testTodayModules struct {
|
||||
TodayPlanning *testTodayPlanning
|
||||
}
|
||||
|
||||
type testDailyModules struct {
|
||||
Dayparts []testDailyDaypart
|
||||
PrecipTiming *testPrecipTiming
|
||||
}
|
||||
|
||||
type testTodayPlanning struct {
|
||||
MorningReadiness []string
|
||||
CommuteSchoolWorkdayConcerns []string
|
||||
@@ -639,6 +798,11 @@ type testTomorrowDaypart struct {
|
||||
Summary testDaypartSummary
|
||||
}
|
||||
|
||||
type testDailyDaypart struct {
|
||||
Key string
|
||||
Summary testDaypartSummary
|
||||
}
|
||||
|
||||
type testDaypartSummary struct {
|
||||
DisplayName string
|
||||
TempRangeF string
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "weatherreporter.daily.generated_text.schema.json",
|
||||
"title": "Daily GeneratedText",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"summary",
|
||||
"forecast_discussion"
|
||||
],
|
||||
"properties": {
|
||||
"summary": {
|
||||
"type": "string"
|
||||
},
|
||||
"forecast_discussion": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"precipitation_timing": {
|
||||
"type": "string"
|
||||
},
|
||||
"confidence": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
24
internal/reporttemplate/templates/daily.md.tmpl
Normal file
24
internal/reporttemplate/templates/daily.md.tmpl
Normal file
@@ -0,0 +1,24 @@
|
||||
# {{ .Report.Title }}
|
||||
|
||||
**Forecast date:** {{ .Report.ForecastDateLabel }}
|
||||
**Updated:** {{ .Report.GeneratedAtLabel }}
|
||||
|
||||
{{ .GeneratedText.Summary }}
|
||||
|
||||
## Daypart Forecast
|
||||
{{ with .Modules.Dayparts }}{{ range . }}{{ $daypart := . }}- **{{ if .Summary.DisplayName }}{{ .Summary.DisplayName }}{{ else }}{{ .Key }}{{ end }}:** {{ with .Summary.DominantConditionDisplay }}{{ . }}{{ else }}{{ with .Summary.DominantCondition }}{{ . }}{{ else }}Forecast details are limited{{ end }}{{ end }}{{ if eq .Summary.TemperatureTrend "rising" }}{{ with .Summary.TemperatureStartPhraseF }}, with temperatures rising from the {{ . }}{{ with $daypart.Summary.TemperatureEndPhraseF }} to the {{ . }}{{ end }}{{ end }}{{ else if eq .Summary.TemperatureTrend "falling" }}{{ with .Summary.TemperatureStartPhraseF }}, with temperatures falling from the {{ . }}{{ with $daypart.Summary.TemperatureEndPhraseF }} to the {{ . }}{{ end }}{{ end }}{{ else if eq .Summary.TemperatureTrend "peaking" }}{{ with .Summary.TemperaturePeakPhraseF }}, with temperatures peaking in the {{ . }}{{ end }}{{ else }}{{ with .Summary.TemperatureSteadyPhraseF }}, with temperatures in the {{ . }}{{ else }}{{ with .Summary.TemperaturePhraseF }}, with temperatures in the {{ . }}{{ end }}{{ end }}{{ end }}.{{ if .Summary.MentionPrecipitation }}{{ with .Summary.MaxPopPercent }} Chance of precipitation is {{ . }}%.{{ end }}{{ end }}
|
||||
{{ end }}{{ else }}- No daypart forecast details are available.
|
||||
{{ end }}
|
||||
|
||||
{{ with .Modules.PrecipTiming }}{{ with .PrecipitationWindows }}
|
||||
## Precipitation Timing
|
||||
{{ range . }}{{ $window := . }}
|
||||
- **{{ if .PeriodBeginsHourLabel }}{{ .PeriodBeginsHourLabel }}{{ else }}{{ .PeriodBegins }}{{ end }}**{{ with .PeriodEndsHourLabel }} to **{{ . }}**{{ else }}{{ with .PeriodEnds }} to **{{ . }}**{{ end }}{{ end }}: Precipitation is expected during this period.{{ with .MaxPopPercent }} The peak precipitation chance is {{ . }}%{{ with $window.MaxPopHourLabel }} at {{ . }}{{ else }}{{ with $window.MaxPopTime }} at {{ . }}{{ end }}{{ end }}.{{ end }}
|
||||
{{ end }}{{ with $.GeneratedText.PrecipitationTiming }}
|
||||
{{ . }}
|
||||
{{ end }}
|
||||
{{ end }}{{ end }}
|
||||
## Forecast Discussion
|
||||
{{ range .GeneratedText.ForecastDiscussion }}
|
||||
{{ . }}
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user