diff --git a/packages/aelis-source-weatherkit/src/feed-items.ts b/packages/aelis-source-weatherkit/src/feed-items.ts index 3b4ca10..a785d46 100644 --- a/packages/aelis-source-weatherkit/src/feed-items.ts +++ b/packages/aelis-source-weatherkit/src/feed-items.ts @@ -32,7 +32,7 @@ export interface CurrentWeatherFeedItem extends FeedItem< CurrentWeatherData > {} -export type HourlyWeatherData = { +export type HourlyWeatherEntry = { forecastTime: Date conditionCode: ConditionCode daylight: boolean @@ -48,6 +48,10 @@ export type HourlyWeatherData = { windSpeed: number } +export type HourlyWeatherData = { + hours: HourlyWeatherEntry[] +} + export interface HourlyWeatherFeedItem extends FeedItem< typeof WeatherFeedItemType.Hourly, HourlyWeatherData diff --git a/packages/aelis-source-weatherkit/src/index.ts b/packages/aelis-source-weatherkit/src/index.ts index 192e6bf..c996887 100644 --- a/packages/aelis-source-weatherkit/src/index.ts +++ b/packages/aelis-source-weatherkit/src/index.ts @@ -8,6 +8,7 @@ export { type CurrentWeatherData, type HourlyWeatherFeedItem, type HourlyWeatherData, + type HourlyWeatherEntry, type DailyWeatherFeedItem, type DailyWeatherData, type WeatherAlertFeedItem, diff --git a/packages/aelis-source-weatherkit/src/weather-source.test.ts b/packages/aelis-source-weatherkit/src/weather-source.test.ts index 8d5f763..b808cf5 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 } from "./weatherkit" +import type { WeatherKitClient, WeatherKitResponse, HourlyForecast } from "./weatherkit" import fixture from "../fixtures/san-francisco.json" -import { WeatherFeedItemType } from "./feed-items" +import { WeatherFeedItemType, type HourlyWeatherData } from "./feed-items" import { WeatherKey, type Weather } from "./weather-context" import { WeatherSource, Units } from "./weather-source" @@ -131,10 +131,67 @@ describe("WeatherSource", () => { const hourlyItems = items.filter((i) => i.type === WeatherFeedItemType.Hourly) const dailyItems = items.filter((i) => i.type === WeatherFeedItemType.Daily) - expect(hourlyItems.length).toBe(3) + expect(hourlyItems.length).toBe(1) + expect((hourlyItems[0]!.data as HourlyWeatherData).hours.length).toBe(3) expect(dailyItems.length).toBe(2) }) + test("produces a single hourly item with hours array", async () => { + const source = new WeatherSource({ client: mockClient }) + const context = createMockContext({ lat: 37.7749, lng: -122.4194 }) + + const items = await source.fetchItems(context) + + const hourlyItems = items.filter((i) => i.type === WeatherFeedItemType.Hourly) + expect(hourlyItems.length).toBe(1) + + const hourlyData = hourlyItems[0]!.data as HourlyWeatherData + expect(Array.isArray(hourlyData.hours)).toBe(true) + expect(hourlyData.hours.length).toBeGreaterThan(0) + expect(hourlyData.hours.length).toBeLessThanOrEqual(12) + }) + + test("averages urgency across hours with mixed conditions", async () => { + const mildHour: HourlyForecast = { + forecastStart: "2026-01-17T01:00:00Z", + conditionCode: "Clear", + daylight: true, + humidity: 0.5, + precipitationAmount: 0, + precipitationChance: 0, + precipitationType: "clear", + pressure: 1013, + snowfallIntensity: 0, + temperature: 20, + temperatureApparent: 20, + temperatureDewPoint: 10, + uvIndex: 3, + visibility: 20000, + windDirection: 180, + windGust: 10, + windSpeed: 5, + } + const severeHour: HourlyForecast = { + ...mildHour, + forecastStart: "2026-01-17T02:00:00Z", + conditionCode: "SevereThunderstorm", + } + const mixedResponse: WeatherKitResponse = { + forecastHourly: { hours: [mildHour, severeHour] }, + } + const source = new WeatherSource({ client: createMockClient(mixedResponse) }) + const context = createMockContext({ lat: 37.7749, lng: -122.4194 }) + + const items = await source.fetchItems(context) + const hourlyItem = items.find((i) => i.type === WeatherFeedItemType.Hourly) + + expect(hourlyItem).toBeDefined() + // Mild urgency = 0.3, severe urgency = 0.6, average = 0.45 + expect(hourlyItem!.signals!.urgency).toBeCloseTo(0.45, 5) + // Worst-case: SevereThunderstorm → Imminent + expect(hourlyItem!.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 5fb0fd9..9f9f40f 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 WeatherFeedItem } from "./feed-items" +import { WeatherFeedItemType, type HourlyWeatherEntry, type WeatherFeedItem } from "./feed-items" import currentWeatherInsightPrompt from "./prompts/current-weather-insight.txt" import { WeatherKey, type Weather } from "./weather-context" import { @@ -174,11 +174,8 @@ export class WeatherSource implements FeedSource { if (response.forecastHourly?.hours) { const hours = response.forecastHourly.hours.slice(0, this.hourlyLimit) - for (let i = 0; i < hours.length; i++) { - const hour = hours[i] - if (hour) { - items.push(createHourlyWeatherFeedItem(hour, i, timestamp, this.units, this.id)) - } + if (hours.length > 0) { + items.push(createHourlyForecastFeedItem(hours, timestamp, this.units, this.id)) } } @@ -323,24 +320,18 @@ function createCurrentWeatherFeedItem( } } -function createHourlyWeatherFeedItem( - hourly: HourlyForecast, - index: number, +function createHourlyForecastFeedItem( + hourlyForecasts: HourlyForecast[], timestamp: Date, units: Units, sourceId: string, ): WeatherFeedItem { - const signals: FeedItemSignals = { - urgency: adjustUrgencyForCondition(BASE_URGENCY.hourly, hourly.conditionCode), - timeRelevance: timeRelevanceForCondition(hourly.conditionCode), - } + const hours: HourlyWeatherEntry[] = [] + let totalUrgency = 0 + let worstTimeRelevance: TimeRelevance = TimeRelevance.Ambient - return { - id: `weather-hourly-${timestamp.getTime()}-${index}`, - sourceId, - type: WeatherFeedItemType.Hourly, - timestamp, - data: { + for (const hourly of hourlyForecasts) { + hours.push({ forecastTime: new Date(hourly.forecastStart), conditionCode: hourly.conditionCode, daylight: hourly.daylight, @@ -354,7 +345,27 @@ function createHourlyWeatherFeedItem( windDirection: hourly.windDirection, windGust: convertSpeed(hourly.windGust, units), windSpeed: convertSpeed(hourly.windSpeed, units), - }, + }) + totalUrgency += adjustUrgencyForCondition(BASE_URGENCY.hourly, hourly.conditionCode) + const rel = timeRelevanceForCondition(hourly.conditionCode) + if (rel === TimeRelevance.Imminent) { + worstTimeRelevance = TimeRelevance.Imminent + } else if (rel === TimeRelevance.Upcoming && worstTimeRelevance !== TimeRelevance.Imminent) { + worstTimeRelevance = TimeRelevance.Upcoming + } + } + + const signals: FeedItemSignals = { + urgency: totalUrgency / hours.length, + timeRelevance: worstTimeRelevance, + } + + return { + id: `weather-hourly-${timestamp.getTime()}`, + sourceId, + type: WeatherFeedItemType.Hourly, + timestamp, + data: { hours }, signals, } }