From 7a37abca67aa68cba3a432caa2e554a4b39685fa Mon Sep 17 00:00:00 2001 From: kenneth Date: Sun, 29 Mar 2026 14:31:02 +0000 Subject: [PATCH] feat: combine daily weather into single feed item Co-authored-by: Ona --- .../aelis-source-weatherkit/src/feed-items.ts | 6 +- packages/aelis-source-weatherkit/src/index.ts | 1 + .../src/weather-source.test.ts | 66 ++++++++++++++++++- .../src/weather-source.ts | 51 ++++++++------ 4 files changed, 100 insertions(+), 24 deletions(-) diff --git a/packages/aelis-source-weatherkit/src/feed-items.ts b/packages/aelis-source-weatherkit/src/feed-items.ts index a785d46..6af7e55 100644 --- a/packages/aelis-source-weatherkit/src/feed-items.ts +++ b/packages/aelis-source-weatherkit/src/feed-items.ts @@ -57,7 +57,7 @@ export interface HourlyWeatherFeedItem extends FeedItem< HourlyWeatherData > {} -export type DailyWeatherData = { +export type DailyWeatherEntry = { forecastDate: Date conditionCode: ConditionCode maxUvIndex: number @@ -71,6 +71,10 @@ export type DailyWeatherData = { temperatureMin: number } +export type DailyWeatherData = { + days: DailyWeatherEntry[] +} + export interface DailyWeatherFeedItem extends FeedItem< typeof WeatherFeedItemType.Daily, DailyWeatherData diff --git a/packages/aelis-source-weatherkit/src/index.ts b/packages/aelis-source-weatherkit/src/index.ts index c996887..1b878c5 100644 --- a/packages/aelis-source-weatherkit/src/index.ts +++ b/packages/aelis-source-weatherkit/src/index.ts @@ -11,6 +11,7 @@ export { type HourlyWeatherEntry, type DailyWeatherFeedItem, type DailyWeatherData, + type DailyWeatherEntry, type WeatherAlertFeedItem, type WeatherAlertData, } from "./feed-items" diff --git a/packages/aelis-source-weatherkit/src/weather-source.test.ts b/packages/aelis-source-weatherkit/src/weather-source.test.ts index b808cf5..1b08bc9 100644 --- a/packages/aelis-source-weatherkit/src/weather-source.test.ts +++ b/packages/aelis-source-weatherkit/src/weather-source.test.ts @@ -4,10 +4,10 @@ import { Context } from "@aelis/core" import { LocationKey } from "@aelis/source-location" import { describe, expect, test } from "bun:test" -import type { WeatherKitClient, WeatherKitResponse, HourlyForecast } from "./weatherkit" +import type { WeatherKitClient, WeatherKitResponse, HourlyForecast, DailyForecast } from "./weatherkit" import fixture from "../fixtures/san-francisco.json" -import { WeatherFeedItemType, type HourlyWeatherData } from "./feed-items" +import { WeatherFeedItemType, type DailyWeatherData, type HourlyWeatherData } from "./feed-items" import { WeatherKey, type Weather } from "./weather-context" import { WeatherSource, Units } from "./weather-source" @@ -133,7 +133,8 @@ describe("WeatherSource", () => { expect(hourlyItems.length).toBe(1) expect((hourlyItems[0]!.data as HourlyWeatherData).hours.length).toBe(3) - expect(dailyItems.length).toBe(2) + expect(dailyItems.length).toBe(1) + expect((dailyItems[0]!.data as DailyWeatherData).days.length).toBe(2) }) test("produces a single hourly item with hours array", async () => { @@ -192,6 +193,65 @@ describe("WeatherSource", () => { expect(hourlyItem!.signals!.timeRelevance).toBe("imminent") }) + test("produces a single daily item with days array", async () => { + const source = new WeatherSource({ client: mockClient }) + const context = createMockContext({ lat: 37.7749, lng: -122.4194 }) + + const items = await source.fetchItems(context) + + const dailyItems = items.filter((i) => i.type === WeatherFeedItemType.Daily) + expect(dailyItems.length).toBe(1) + + const dailyData = dailyItems[0]!.data as DailyWeatherData + expect(Array.isArray(dailyData.days)).toBe(true) + expect(dailyData.days.length).toBeGreaterThan(0) + expect(dailyData.days.length).toBeLessThanOrEqual(7) + }) + + test("averages urgency across days with mixed conditions", async () => { + const mildDay: DailyForecast = { + forecastStart: "2026-01-17T00:00:00Z", + forecastEnd: "2026-01-18T00:00:00Z", + conditionCode: "Clear", + maxUvIndex: 3, + moonPhase: "firstQuarter", + precipitationAmount: 0, + precipitationChance: 0, + precipitationType: "clear", + snowfallAmount: 0, + sunrise: "2026-01-17T07:00:00Z", + sunriseCivil: "2026-01-17T06:30:00Z", + sunriseNautical: "2026-01-17T06:00:00Z", + sunriseAstronomical: "2026-01-17T05:30:00Z", + sunset: "2026-01-17T17:00:00Z", + sunsetCivil: "2026-01-17T17:30:00Z", + sunsetNautical: "2026-01-17T18:00:00Z", + sunsetAstronomical: "2026-01-17T18:30:00Z", + temperatureMax: 15, + temperatureMin: 5, + } + const severeDay: DailyForecast = { + ...mildDay, + forecastStart: "2026-01-18T00:00:00Z", + forecastEnd: "2026-01-19T00:00:00Z", + conditionCode: "SevereThunderstorm", + } + const mixedResponse: WeatherKitResponse = { + forecastDaily: { days: [mildDay, severeDay] }, + } + const source = new WeatherSource({ client: createMockClient(mixedResponse) }) + const context = createMockContext({ lat: 37.7749, lng: -122.4194 }) + + const items = await source.fetchItems(context) + const dailyItem = items.find((i) => i.type === WeatherFeedItemType.Daily) + + expect(dailyItem).toBeDefined() + // Mild urgency = 0.2, severe urgency = 0.5, average = 0.35 + expect(dailyItem!.signals!.urgency).toBeCloseTo(0.35, 5) + // Worst-case: SevereThunderstorm → Imminent + expect(dailyItem!.signals!.timeRelevance).toBe("imminent") + }) + test("sets timestamp from context.time", async () => { const source = new WeatherSource({ client: mockClient }) const queryTime = new Date("2026-01-17T12:00:00Z") diff --git a/packages/aelis-source-weatherkit/src/weather-source.ts b/packages/aelis-source-weatherkit/src/weather-source.ts index 9f9f40f..742e8e7 100644 --- a/packages/aelis-source-weatherkit/src/weather-source.ts +++ b/packages/aelis-source-weatherkit/src/weather-source.ts @@ -3,7 +3,7 @@ import type { ActionDefinition, ContextEntry, FeedItemSignals, FeedSource } from import { Context, TimeRelevance, UnknownActionError } from "@aelis/core" import { LocationKey } from "@aelis/source-location" -import { WeatherFeedItemType, type HourlyWeatherEntry, type WeatherFeedItem } from "./feed-items" +import { WeatherFeedItemType, type DailyWeatherEntry, type HourlyWeatherEntry, type WeatherFeedItem } from "./feed-items" import currentWeatherInsightPrompt from "./prompts/current-weather-insight.txt" import { WeatherKey, type Weather } from "./weather-context" import { @@ -181,11 +181,8 @@ export class WeatherSource implements FeedSource { if (response.forecastDaily?.days) { const days = response.forecastDaily.days.slice(0, this.dailyLimit) - for (let i = 0; i < days.length; i++) { - const day = days[i] - if (day) { - items.push(createDailyWeatherFeedItem(day, i, timestamp, this.units, this.id)) - } + if (days.length > 0) { + items.push(createDailyForecastFeedItem(days, timestamp, this.units, this.id)) } } @@ -370,24 +367,18 @@ function createHourlyForecastFeedItem( } } -function createDailyWeatherFeedItem( - daily: DailyForecast, - index: number, +function createDailyForecastFeedItem( + dailyForecasts: DailyForecast[], timestamp: Date, units: Units, sourceId: string, ): WeatherFeedItem { - const signals: FeedItemSignals = { - urgency: adjustUrgencyForCondition(BASE_URGENCY.daily, daily.conditionCode), - timeRelevance: timeRelevanceForCondition(daily.conditionCode), - } + const days: DailyWeatherEntry[] = [] + let totalUrgency = 0 + let worstTimeRelevance: TimeRelevance = TimeRelevance.Ambient - return { - id: `weather-daily-${timestamp.getTime()}-${index}`, - sourceId, - type: WeatherFeedItemType.Daily, - timestamp, - data: { + for (const daily of dailyForecasts) { + days.push({ forecastDate: new Date(daily.forecastStart), conditionCode: daily.conditionCode, maxUvIndex: daily.maxUvIndex, @@ -399,7 +390,27 @@ function createDailyWeatherFeedItem( sunset: new Date(daily.sunset), temperatureMax: convertTemperature(daily.temperatureMax, units), temperatureMin: convertTemperature(daily.temperatureMin, units), - }, + }) + totalUrgency += adjustUrgencyForCondition(BASE_URGENCY.daily, daily.conditionCode) + const rel = timeRelevanceForCondition(daily.conditionCode) + if (rel === TimeRelevance.Imminent) { + worstTimeRelevance = TimeRelevance.Imminent + } else if (rel === TimeRelevance.Upcoming && worstTimeRelevance !== TimeRelevance.Imminent) { + worstTimeRelevance = TimeRelevance.Upcoming + } + } + + const signals: FeedItemSignals = { + urgency: totalUrgency / days.length, + timeRelevance: worstTimeRelevance, + } + + return { + id: `weather-daily-${timestamp.getTime()}`, + sourceId, + type: WeatherFeedItemType.Daily, + timestamp, + data: { days }, signals, } }