- Fix ComputeRoomBudget: no-AC rooms check if open-window ventilation can offset gains instead of defaulting to Overloaded. Net-cooling rooms are now Comfortable; ventilation-solvable rooms are Marginal. - Add "comfort" cool mode for hours where outdoor is >5°C below indoor and budget is not overloaded (winter/cold scenarios). - Reorder determineCoolMode: sealed now before overloaded, fixing humid+cold+no-AC giving "overloaded" instead of "sealed". - Update LLM prompts: document comfort coolMode, add cold-weather guidance for summary and actions generation. - Add dashboard forecast refresh button: fetches fresh forecast + warnings, then re-runs compute and LLM pipelines. - Extract forecast fetch into shared fetchForecastForProfile() in db.js, deduplicating logic between setup.js and dashboard.js. - Add indoor humidity support, pressure display, and cool mode sealed integration test.
168 lines
7.5 KiB
Go
168 lines
7.5 KiB
Go
package llm
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
const summarizeSystemPrompt = `You are a heat preparedness assistant. You receive computed heat model data for a specific day.
|
||
Generate exactly 3 concise bullet points summarizing the key drivers and risks.
|
||
Rules:
|
||
- Reference ONLY the data provided below. Do not invent or assume additional information.
|
||
- Use preparedness language (comfort, planning). Never give medical advice or diagnoses.
|
||
- Each bullet: max 20 words, plain language, actionable insight.
|
||
- If peak temperature is below 22°C and risk level is "low", state simply that no heat risk is expected today. Focus on comfort rather than warnings.
|
||
- If budget status is "marginal" but peak temperature is below 25°C, this indicates minor internal heat gains easily managed by opening windows — not a cooling concern.
|
||
- Format: "- [bullet text]" (markdown list)`
|
||
|
||
const rewriteActionSystemPrompt = `You are a heat preparedness assistant. Rewrite the given technical action into a clear, friendly, plain-language instruction.
|
||
Rules:
|
||
- One sentence, max 25 words.
|
||
- Reference the provided temperature and time context.
|
||
- Use preparedness language. Never give medical advice.
|
||
- Return only the rewritten sentence, nothing else.`
|
||
|
||
const generateActionsSystemPrompt = `You are a heat preparedness assistant. Generate context-aware cooling actions for a specific day.
|
||
Rules:
|
||
- Return ONLY a JSON array of action objects. No markdown, no explanation, no wrapping text.
|
||
- Each object schema: {"name": string, "description": string, "category": string, "effort": string, "impact": string, "hours": [int]}
|
||
- Valid categories: shading, ventilation, internal_gains, ac_strategy, hydration, care
|
||
- Valid effort values: none, low, medium, high
|
||
- Valid impact values: low, medium, high
|
||
- Be specific: reference temperatures, time windows, indoor/outdoor differentials, solar gain timing, and room orientations.
|
||
- Use the coolMode data to recommend ventilation vs AC per time window.
|
||
- Valid coolMode values: "comfort" (outdoor well below indoor — no active cooling needed, building envelope handles it), "ventilate" (open windows to cool), "sealed" (too humid to ventilate, keep closed), "ac" (use AC), "overloaded" (AC cannot keep up with heat gains).
|
||
- Do NOT recommend opening windows or ventilation during "comfort" hours — the room is already comfortable. Only generate actions for hours where active intervention is needed.
|
||
- On transitional days where morning/evening hours show "comfort" but midday shows "ventilate" or "ac", focus actions on the warmer hours only.
|
||
- If peak outdoor temperature is below 22°C and risk level is "low", return an empty array []. No cooling actions are needed on mild or cold days.
|
||
- Generate 5-12 actions covering the most impactful strategies for the day.
|
||
- Use preparedness language. Never give medical advice or diagnoses.`
|
||
|
||
const heatPlanSystemPrompt = `You are a heat preparedness assistant. Generate a 1-page plain-language heat plan document.
|
||
Rules:
|
||
- Reference ONLY the data provided below. Do not invent information.
|
||
- Use preparedness language (comfort, planning). Never give medical advice or diagnoses.
|
||
- Structure: Brief overview (2-3 sentences), then hour-by-hour key actions, then care reminders.
|
||
- Use simple language anyone can understand.
|
||
- Keep total length under 500 words.
|
||
- Format as markdown with headers.`
|
||
|
||
// BuildSummaryPrompt constructs the user message for Summarize.
|
||
func BuildSummaryPrompt(input SummaryInput) string {
|
||
var b strings.Builder
|
||
if input.Language != "" {
|
||
fmt.Fprintf(&b, "Respond in: %s\n\n", input.Language)
|
||
}
|
||
fmt.Fprintf(&b, "Date: %s\n", input.Date)
|
||
fmt.Fprintf(&b, "Peak temperature: %.1f°C\n", input.PeakTempC)
|
||
fmt.Fprintf(&b, "Minimum night temperature: %.1f°C\n", input.MinNightTempC)
|
||
fmt.Fprintf(&b, "Overall risk level: %s\n", input.RiskLevel)
|
||
fmt.Fprintf(&b, "AC headroom: %.0f BTU/h\n", input.ACHeadroomBTUH)
|
||
fmt.Fprintf(&b, "Budget status: %s\n", input.BudgetStatus)
|
||
|
||
if len(input.TopHeatSources) > 0 {
|
||
b.WriteString("Top heat sources:\n")
|
||
for _, s := range input.TopHeatSources {
|
||
fmt.Fprintf(&b, " - %s: %.0fW\n", s.Name, s.Watts)
|
||
}
|
||
}
|
||
if len(input.ActiveWarnings) > 0 {
|
||
b.WriteString("Active warnings:\n")
|
||
for _, w := range input.ActiveWarnings {
|
||
fmt.Fprintf(&b, " - %s\n", w)
|
||
}
|
||
}
|
||
if len(input.RiskWindows) > 0 {
|
||
b.WriteString("Risk windows:\n")
|
||
for _, rw := range input.RiskWindows {
|
||
fmt.Fprintf(&b, " - %02d:00–%02d:00, peak %.1f°C, level: %s\n", rw.StartHour, rw.EndHour, rw.PeakTempC, rw.Level)
|
||
}
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// BuildRewriteActionPrompt constructs the user message for RewriteAction.
|
||
func BuildRewriteActionPrompt(input ActionInput) string {
|
||
return fmt.Sprintf("Action: %s\nDescription: %s\nCurrent temperature: %.1f°C\nHour: %02d:00",
|
||
input.ActionName, input.Description, input.TempC, input.Hour)
|
||
}
|
||
|
||
// BuildHeatPlanPrompt constructs the user message for GenerateHeatPlan.
|
||
func BuildHeatPlanPrompt(input HeatPlanInput) string {
|
||
var b strings.Builder
|
||
b.WriteString(BuildSummaryPrompt(input.Summary))
|
||
b.WriteString("\nTimeline:\n")
|
||
for _, s := range input.Timeline {
|
||
actions := "none"
|
||
if len(s.Actions) > 0 {
|
||
actions = strings.Join(s.Actions, ", ")
|
||
}
|
||
fmt.Fprintf(&b, " %02d:00 | %.1f°C | risk: %s | budget: %s | actions: %s\n",
|
||
s.Hour, s.TempC, s.RiskLevel, s.BudgetStatus, actions)
|
||
}
|
||
if len(input.CareChecklist) > 0 {
|
||
b.WriteString("\nCare checklist:\n")
|
||
for _, c := range input.CareChecklist {
|
||
fmt.Fprintf(&b, " - %s\n", c)
|
||
}
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// BuildActionsPrompt constructs the user message for GenerateActions.
|
||
func BuildActionsPrompt(input ActionsInput) string {
|
||
var b strings.Builder
|
||
if input.Language != "" {
|
||
fmt.Fprintf(&b, "Respond in: %s\n\n", input.Language)
|
||
}
|
||
fmt.Fprintf(&b, "Date: %s\n", input.Date)
|
||
fmt.Fprintf(&b, "Indoor target: %.1f°C\n", input.IndoorTempC)
|
||
fmt.Fprintf(&b, "Peak outdoor temp: %.1f°C\n", input.PeakTempC)
|
||
fmt.Fprintf(&b, "Min night temp: %.1f°C\n", input.MinNightTempC)
|
||
fmt.Fprintf(&b, "Poor night cooling: %v\n", input.PoorNightCool)
|
||
fmt.Fprintf(&b, "Risk level: %s\n", input.RiskLevel)
|
||
|
||
if len(input.RiskWindows) > 0 {
|
||
b.WriteString("\nRisk windows:\n")
|
||
for _, rw := range input.RiskWindows {
|
||
fmt.Fprintf(&b, " %02d:00–%02d:00, peak %.1f°C, level: %s\n", rw.StartHour, rw.EndHour, rw.PeakTempC, rw.Level)
|
||
}
|
||
}
|
||
|
||
if len(input.Timeline) > 0 {
|
||
b.WriteString("\nHourly data:\n")
|
||
b.WriteString(" HH:00 | outdoor°C | Δ indoor | humidity% | budget | coolMode | gainsW\n")
|
||
for _, s := range input.Timeline {
|
||
delta := s.TempC - input.IndoorTempC
|
||
fmt.Fprintf(&b, " %02d:00 | %5.1f°C | %+5.1f°C | %5.1f%% | %-10s| %-10s | %.0fW\n",
|
||
s.Hour, s.TempC, delta, s.HumidityPct, s.BudgetStatus, s.CoolMode, s.GainsW)
|
||
}
|
||
}
|
||
|
||
if len(input.Rooms) > 0 {
|
||
b.WriteString("\nRooms:\n")
|
||
for _, r := range input.Rooms {
|
||
acStr := "no"
|
||
if r.HasAC {
|
||
acStr = "yes"
|
||
}
|
||
fmt.Fprintf(&b, " - %s (orientation: %s, shading: %s, AC: %s)\n",
|
||
r.Name, r.Orientation, r.ShadingType, acStr)
|
||
}
|
||
}
|
||
|
||
return b.String()
|
||
}
|
||
|
||
// SummarizeSystemPrompt returns the system prompt for Summarize.
|
||
func SummarizeSystemPrompt() string { return summarizeSystemPrompt }
|
||
|
||
// RewriteActionSystemPrompt returns the system prompt for RewriteAction.
|
||
func RewriteActionSystemPrompt() string { return rewriteActionSystemPrompt }
|
||
|
||
// HeatPlanSystemPrompt returns the system prompt for GenerateHeatPlan.
|
||
func HeatPlanSystemPrompt() string { return heatPlanSystemPrompt }
|
||
|
||
// GenerateActionsSystemPrompt returns the system prompt for GenerateActions.
|
||
func GenerateActionsSystemPrompt() string { return generateActionsSystemPrompt }
|