feat: add a military awareness tool

This commit is contained in:
Sameh Khamis committed 2026-10-01 13:41:30 -07:00
1 parent 28cfd39d1a
commit a747b8a4e0
5 files changed
+197 -19

No files matched your search

+110 -16
View File
@@ -1,13 +1,25 @@
/** Composite queries that combine other tools' answers for one area. */
import { defineTool, ToolError } from '../catalog.js';
import { AREA_SCHEMA, areaCenter, resolveArea } from '../area.js';
import {
AREA_SCHEMA,
POINT_SCHEMA,
areaCenter,
resolveArea,
resolvePoint,
} from '../area.js';
import { aircraftInArea } from './aviation.js';
import { getCyclones, getWeather } from './environment.js';
import {
findMilitaryInstallations,
getCyclones,
getWeather,
} from './environment.js';
import { getActiveFires, getEarthquakes } from './hazards.js';
import { vesselsInArea } from './maritime.js';
const SECTION_LIMIT = 5;
const AWARENESS_LIMIT = 10;
const AWARENESS_RADIUS_KM = 250;
/** Brief sections: the tool each one reuses and the services it needs. */
const SECTIONS = [
@@ -45,14 +57,37 @@ const SECTIONS = [
},
];
/**
* Run every section whose services are supplied. A failing section is
* reported as unavailable instead of failing the brief.
*/
async function buildBrief(args, { services, signal }) {
const resolved = await resolveArea(args.area, { services, signal });
// Sections receive the resolved box so a place name is looked up once.
const area = {
/** Military contacts around a point, as the app's military awareness panel. */
const AWARENESS_SECTIONS = [
{
key: 'military_aircraft',
tool: aircraftInArea,
args: (area) => ({
area: area.argument,
military: true,
limit: AWARENESS_LIMIT,
}),
},
{
key: 'aircraft',
tool: aircraftInArea,
args: (area) => ({ area: area.argument, limit: AWARENESS_LIMIT }),
},
{
key: 'vessels',
tool: vesselsInArea,
args: (area) => ({ area: area.argument, limit: AWARENESS_LIMIT }),
},
{
key: 'installations',
tool: findMilitaryInstallations,
args: (area) => ({ area: area.argument, limit: AWARENESS_LIMIT }),
},
];
/** The resolved area, plus the argument sections receive in its place. */
function sectionArea(resolved) {
return {
...resolved,
argument: resolved.center
? {
@@ -64,8 +99,15 @@ async function buildBrief(args, { services, signal }) {
bbox: [resolved.west, resolved.south, resolved.east, resolved.north],
},
};
const center = areaCenter(resolved);
const sections = SECTIONS.filter(({ tool }) =>
}
/**
* Run every section whose services are supplied. A failing section is
* reported as unavailable instead of failing the whole answer.
*/
async function runSections(all, area, { services, signal }) {
const center = areaCenter(area);
const sections = all.filter(({ tool }) =>
tool.requires.every((key) => services[key] != null),
);
const results = await Promise.allSettled(
@@ -74,22 +116,32 @@ async function buildBrief(args, { services, signal }) {
),
);
signal?.throwIfAborted();
const brief = {};
const answers = {};
const lines = [];
sections.forEach(({ key }, index) => {
const result = results[index];
if (result.status === 'fulfilled') {
brief[key] = { summary: result.value.summary, data: result.value.data };
answers[key] = { summary: result.value.summary, data: result.value.data };
lines.push(result.value.summary);
} else {
const known = result.reason instanceof ToolError;
brief[key] = {
answers[key] = {
unavailable: true,
reason: known ? result.reason.message : 'unavailable right now',
};
}
});
return { area: resolved, center, brief, lines };
return { center, answers, lines };
}
async function buildBrief(args, { services, signal }) {
// Sections receive the resolved box so a place name is looked up once.
const area = sectionArea(await resolveArea(args.area, { services, signal }));
const { center, answers, lines } = await runSections(SECTIONS, area, {
services,
signal,
});
return { area, center, brief: answers, lines };
}
export const situationBrief = defineTool({
@@ -143,3 +195,45 @@ export const getHudCaption = defineTool({
};
},
});
export const militaryAwareness = defineTool({
name: 'military_awareness',
title: 'Military awareness',
description:
'Military contacts around a location, as the military awareness panel ' +
'shows them: military aircraft, other aircraft, ships and mapped ' +
`military installations within ${AWARENESS_RADIUS_KM} km by default, ` +
'each nearest first. Sections that are unavailable are marked as such.',
inputSchema: {
type: 'object',
properties: {
location: POINT_SCHEMA,
radius_km: { type: 'number', minimum: 1, maximum: AWARENESS_RADIUS_KM },
},
required: ['location'],
additionalProperties: false,
},
requires: ['military'],
async run(args, { services, signal }) {
const point = await resolvePoint(args.location, { services, signal });
const radiusKm = args.radius_km ?? AWARENESS_RADIUS_KM;
const area = sectionArea(
await resolveArea(
{ lat: point.lat, lon: point.lon, radius_km: radiusKm },
{ services, signal },
),
);
const { answers, lines } = await runSections(AWARENESS_SECTIONS, area, {
services,
signal,
});
return {
summary: `Military awareness within ${radiusKm} km of ${point.label}: ${lines.join(' ')}`,
data: {
location: point,
radius_km: radiusKm,
sections: answers,
},
};
},
});