mirror of
https://github.com/kennethnym/aris.git
synced 2026-03-30 06:41:18 +01:00
Compare commits
1 Commits
kn/combine
...
fix/llm-st
| Author | SHA1 | Date | |
|---|---|---|---|
|
bbf425e255
|
@@ -32,7 +32,7 @@ export interface CurrentWeatherFeedItem extends FeedItem<
|
||||
CurrentWeatherData
|
||||
> {}
|
||||
|
||||
export type HourlyWeatherEntry = {
|
||||
export type HourlyWeatherData = {
|
||||
forecastTime: Date
|
||||
conditionCode: ConditionCode
|
||||
daylight: boolean
|
||||
@@ -48,16 +48,12 @@ export type HourlyWeatherEntry = {
|
||||
windSpeed: number
|
||||
}
|
||||
|
||||
export type HourlyWeatherData = {
|
||||
hours: HourlyWeatherEntry[]
|
||||
}
|
||||
|
||||
export interface HourlyWeatherFeedItem extends FeedItem<
|
||||
typeof WeatherFeedItemType.Hourly,
|
||||
HourlyWeatherData
|
||||
> {}
|
||||
|
||||
export type DailyWeatherEntry = {
|
||||
export type DailyWeatherData = {
|
||||
forecastDate: Date
|
||||
conditionCode: ConditionCode
|
||||
maxUvIndex: number
|
||||
@@ -71,10 +67,6 @@ export type DailyWeatherEntry = {
|
||||
temperatureMin: number
|
||||
}
|
||||
|
||||
export type DailyWeatherData = {
|
||||
days: DailyWeatherEntry[]
|
||||
}
|
||||
|
||||
export interface DailyWeatherFeedItem extends FeedItem<
|
||||
typeof WeatherFeedItemType.Daily,
|
||||
DailyWeatherData
|
||||
|
||||
@@ -8,10 +8,8 @@ export {
|
||||
type CurrentWeatherData,
|
||||
type HourlyWeatherFeedItem,
|
||||
type HourlyWeatherData,
|
||||
type HourlyWeatherEntry,
|
||||
type DailyWeatherFeedItem,
|
||||
type DailyWeatherData,
|
||||
type DailyWeatherEntry,
|
||||
type WeatherAlertFeedItem,
|
||||
type WeatherAlertData,
|
||||
} from "./feed-items"
|
||||
|
||||
@@ -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, DailyForecast } from "./weatherkit"
|
||||
import type { WeatherKitClient, WeatherKitResponse } from "./weatherkit"
|
||||
|
||||
import fixture from "../fixtures/san-francisco.json"
|
||||
import { WeatherFeedItemType, type DailyWeatherData, type HourlyWeatherData } from "./feed-items"
|
||||
import { WeatherFeedItemType } from "./feed-items"
|
||||
import { WeatherKey, type Weather } from "./weather-context"
|
||||
import { WeatherSource, Units } from "./weather-source"
|
||||
|
||||
@@ -131,125 +131,8 @@ describe("WeatherSource", () => {
|
||||
const hourlyItems = items.filter((i) => i.type === WeatherFeedItemType.Hourly)
|
||||
const dailyItems = items.filter((i) => i.type === WeatherFeedItemType.Daily)
|
||||
|
||||
expect(hourlyItems.length).toBe(1)
|
||||
expect((hourlyItems[0]!.data as HourlyWeatherData).hours.length).toBe(3)
|
||||
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 () => {
|
||||
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("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")
|
||||
expect(hourlyItems.length).toBe(3)
|
||||
expect(dailyItems.length).toBe(2)
|
||||
})
|
||||
|
||||
test("sets timestamp from context.time", async () => {
|
||||
|
||||
@@ -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 DailyWeatherEntry, type HourlyWeatherEntry, type WeatherFeedItem } from "./feed-items"
|
||||
import { WeatherFeedItemType, type WeatherFeedItem } from "./feed-items"
|
||||
import currentWeatherInsightPrompt from "./prompts/current-weather-insight.txt"
|
||||
import { WeatherKey, type Weather } from "./weather-context"
|
||||
import {
|
||||
@@ -174,15 +174,21 @@ export class WeatherSource implements FeedSource<WeatherFeedItem> {
|
||||
|
||||
if (response.forecastHourly?.hours) {
|
||||
const hours = response.forecastHourly.hours.slice(0, this.hourlyLimit)
|
||||
if (hours.length > 0) {
|
||||
items.push(createHourlyForecastFeedItem(hours, timestamp, this.units, this.id))
|
||||
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 (response.forecastDaily?.days) {
|
||||
const days = response.forecastDaily.days.slice(0, this.dailyLimit)
|
||||
if (days.length > 0) {
|
||||
items.push(createDailyForecastFeedItem(days, timestamp, this.units, this.id))
|
||||
for (let i = 0; i < days.length; i++) {
|
||||
const day = days[i]
|
||||
if (day) {
|
||||
items.push(createDailyWeatherFeedItem(day, i, timestamp, this.units, this.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,18 +323,24 @@ function createCurrentWeatherFeedItem(
|
||||
}
|
||||
}
|
||||
|
||||
function createHourlyForecastFeedItem(
|
||||
hourlyForecasts: HourlyForecast[],
|
||||
function createHourlyWeatherFeedItem(
|
||||
hourly: HourlyForecast,
|
||||
index: number,
|
||||
timestamp: Date,
|
||||
units: Units,
|
||||
sourceId: string,
|
||||
): WeatherFeedItem {
|
||||
const hours: HourlyWeatherEntry[] = []
|
||||
let totalUrgency = 0
|
||||
let worstTimeRelevance: TimeRelevance = TimeRelevance.Ambient
|
||||
const signals: FeedItemSignals = {
|
||||
urgency: adjustUrgencyForCondition(BASE_URGENCY.hourly, hourly.conditionCode),
|
||||
timeRelevance: timeRelevanceForCondition(hourly.conditionCode),
|
||||
}
|
||||
|
||||
for (const hourly of hourlyForecasts) {
|
||||
hours.push({
|
||||
return {
|
||||
id: `weather-hourly-${timestamp.getTime()}-${index}`,
|
||||
sourceId,
|
||||
type: WeatherFeedItemType.Hourly,
|
||||
timestamp,
|
||||
data: {
|
||||
forecastTime: new Date(hourly.forecastStart),
|
||||
conditionCode: hourly.conditionCode,
|
||||
daylight: hourly.daylight,
|
||||
@@ -342,43 +354,29 @@ function createHourlyForecastFeedItem(
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
function createDailyForecastFeedItem(
|
||||
dailyForecasts: DailyForecast[],
|
||||
function createDailyWeatherFeedItem(
|
||||
daily: DailyForecast,
|
||||
index: number,
|
||||
timestamp: Date,
|
||||
units: Units,
|
||||
sourceId: string,
|
||||
): WeatherFeedItem {
|
||||
const days: DailyWeatherEntry[] = []
|
||||
let totalUrgency = 0
|
||||
let worstTimeRelevance: TimeRelevance = TimeRelevance.Ambient
|
||||
const signals: FeedItemSignals = {
|
||||
urgency: adjustUrgencyForCondition(BASE_URGENCY.daily, daily.conditionCode),
|
||||
timeRelevance: timeRelevanceForCondition(daily.conditionCode),
|
||||
}
|
||||
|
||||
for (const daily of dailyForecasts) {
|
||||
days.push({
|
||||
return {
|
||||
id: `weather-daily-${timestamp.getTime()}-${index}`,
|
||||
sourceId,
|
||||
type: WeatherFeedItemType.Daily,
|
||||
timestamp,
|
||||
data: {
|
||||
forecastDate: new Date(daily.forecastStart),
|
||||
conditionCode: daily.conditionCode,
|
||||
maxUvIndex: daily.maxUvIndex,
|
||||
@@ -390,27 +388,7 @@ function createDailyForecastFeedItem(
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user