openpondai/agents/important-dates-roundup
OpenTool app
typescript
import type { ImportantDatesRoundupConfig } from "./config";
type GatewayImportantDate = {
id: string;
series: string;
category: string;
referencePeriod: string;
releaseAt: string;
source: string;
sourceUrl: string;
status: string;
updatedAt: string;
};
type ImportantDatesGatewayResponse = {
events?: GatewayImportantDate[];
refreshedAt?: string | null;
};
export type ImportantDateEvent = {
id: string;
series: string;
category: string;
referencePeriod: string;
releaseAt: string;
source: string;
sourceUrl: string;
status: string;
updatedAt: string;
};
export type ImportantDatesSummary = {
title: string;
overallSummary: string;
topEvents: Array<ImportantDateEvent & { reason: string }>;
};
export type ImportantDatesRoundupResult = {
ok: true;
title: string;
generatedAt: string;
refreshedAt: string | null;
daysAhead: number;
limit: number;
category?: string;
series?: string[];
summary: ImportantDatesSummary;
events: ImportantDateEvent[];
};
function resolveGatewayBase() {
const baseUrl = process.env.OPENPOND_GATEWAY_URL;
if (!baseUrl) {
throw new Error("OPENPOND_GATEWAY_URL is required for Important Dates Roundup.");
}
return baseUrl.replace(/\/$/, "");
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function normalizeImportantDate(value: unknown): ImportantDateEvent | null {
if (!isRecord(value)) return null;
const id = typeof value.id === "string" ? value.id : "";
const series = typeof value.series === "string" ? value.series : "";
const category = typeof value.category === "string" ? value.category : "";
const referencePeriod =
typeof value.referencePeriod === "string" ? value.referencePeriod : "";
const releaseAt =
typeof value.releaseAt === "string" ? value.releaseAt : "";
const source = typeof value.source === "string" ? value.source : "";
const sourceUrl = typeof value.sourceUrl === "string" ? value.sourceUrl : "";
const status = typeof value.status === "string" ? value.status : "";
const updatedAt = typeof value.updatedAt === "string" ? value.updatedAt : "";
if (!id || !series || !releaseAt) {
return null;
}
return {
id,
series,
category,
referencePeriod,
releaseAt,
source,
sourceUrl,
status,
updatedAt,
};
}
function formatReleaseAt(value: string): string {
const timestamp = Date.parse(value);
if (!Number.isFinite(timestamp)) return value;
return new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
timeZoneName: "short",
}).format(new Date(timestamp));
}
function dedupeEvents(events: ImportantDateEvent[]): ImportantDateEvent[] {
const seen = new Set<string>();
const deduped: ImportantDateEvent[] = [];
for (const event of events) {
const key = `${event.id}:${event.releaseAt}`;
if (seen.has(key)) continue;
seen.add(key);
deduped.push(event);
}
return deduped;
}
function fallbackReason(event: ImportantDateEvent): string {
if (event.referencePeriod) {
return `Scheduled ${event.series} release for ${event.referencePeriod}.`;
}
return `Scheduled ${event.series} release.`;
}
function buildFallbackSummary(params: {
config: ImportantDatesRoundupConfig;
events: ImportantDateEvent[];
}): ImportantDatesSummary {
const topEvents = params.events.slice(0, params.config.summaryMaxEvents);
if (topEvents.length === 0) {
return {
title: params.config.title,
overallSummary: `No upcoming dates were found in the next ${params.config.daysAhead} days.`,
topEvents: [],
};
}
const label = topEvents
.slice(0, 3)
.map((event) =>
event.referencePeriod
? `${event.series} (${event.referencePeriod}) on ${formatReleaseAt(event.releaseAt)}`
: `${event.series} on ${formatReleaseAt(event.releaseAt)}`,
)
.join("; ");
return {
title: params.config.title,
overallSummary: `Upcoming releases over the next ${params.config.daysAhead} days: ${label}.`,
topEvents: topEvents.map((event) => ({
...event,
reason: fallbackReason(event),
})),
};
}
async function fetchImportantDates(
config: ImportantDatesRoundupConfig,
): Promise<{ events: ImportantDateEvent[]; refreshedAt: string | null }> {
const gatewayBase = resolveGatewayBase();
const requestLimit = Math.min(Math.max(config.limit * 3, config.limit), 50);
const search = new URLSearchParams({
daysAhead: String(config.daysAhead),
limit: String(requestLimit),
});
if (config.category) {
search.set("category", config.category);
}
const response = await fetch(
`${gatewayBase}/v1/macro/important-dates?${search.toString()}`,
{
method: "GET",
},
);
if (!response.ok) {
throw new Error(`important-dates request failed (${response.status})`);
}
const payload = (await response.json().catch(() => null)) as
| ImportantDatesGatewayResponse
| null;
const seriesFilters = new Set(
(config.series ?? []).map((series) => series.trim().toLowerCase()),
);
const events = dedupeEvents(
(Array.isArray(payload?.events) ? payload.events : [])
.map((event) => normalizeImportantDate(event))
.filter((event): event is ImportantDateEvent => event !== null)
.filter((event) => {
if (seriesFilters.size === 0) return true;
return seriesFilters.has(event.series.trim().toLowerCase());
}),
)
.sort((left, right) => {
const leftTs = Date.parse(left.releaseAt);
const rightTs = Date.parse(right.releaseAt);
return leftTs - rightTs;
})
.slice(0, config.limit);
return {
events,
refreshedAt:
typeof payload?.refreshedAt === "string" ? payload.refreshedAt : null,
};
}
async function summarizeImportantDates(params: {
config: ImportantDatesRoundupConfig;
events: ImportantDateEvent[];
}): Promise<ImportantDatesSummary> {
return buildFallbackSummary(params);
}
export async function runImportantDatesRoundup(
config: ImportantDatesRoundupConfig,
): Promise<ImportantDatesRoundupResult> {
const generatedAt = new Date().toISOString();
const { events, refreshedAt } = await fetchImportantDates(config);
const summary = await summarizeImportantDates({ config, events });
return {
ok: true,
title: config.title,
generatedAt,
refreshedAt,
daysAhead: config.daysAhead,
limit: config.limit,
category: config.category,
series: config.series,
summary,
events,
};
}