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

+6 -1
View File
@@ -11,7 +11,11 @@ import {
} from './queries/aviation.js';
import { getWeatherMap, getWind } from './queries/atmosphere.js';
import { findSubmarineCables } from './queries/cables.js';
import { getHudCaption, situationBrief } from './queries/brief.js';
import {
getHudCaption,
militaryAwareness,
situationBrief,
} from './queries/brief.js';
import {
findMilitaryInstallations,
getCyclones,
@@ -106,6 +110,7 @@ export const coreTools = Object.freeze([
findMilitaryInstallations,
getMapFeatures,
situationBrief,
militaryAwareness,
getHudCaption,
openInGodsEyeView,
]);
+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,
},
};
},
});
+78
View File
@@ -453,3 +453,81 @@ test('the HUD caption summarizes the brief through the summary service', async (
(error) => error instanceof ToolError && error.code === 'unavailable',
);
});
test('military awareness gathers contacts around a point and marks failures', async () => {
const contact = (id, latitude, longitude) => ({
id,
reference: id,
latitude,
longitude,
callsign: 'RCH123',
baroAltitudeM: 9000,
onGround: false,
positionTimeMs: Date.UTC(2026, 0, 1),
});
const military = {
getSnapshot: async () => ({
records: [contact('ae1234', 32.8, -117.1), contact('ae9999', 40, -100)],
complete: true,
source: 'Military feed',
freshness: 'current',
}),
};
const aircraft = {
getSnapshot: async () => {
throw new Error('upstream down');
},
};
const installations = {
getMappedSites: async () => ({
source: 'OpenStreetMap',
records: [
{
id: 'osm:1',
kind: 'installation',
class: 'base',
name: 'Naval Base Point Loma',
latitude: 32.6941,
longitude: -117.2494,
},
],
}),
};
const catalog = catalogWith({ military, aircraft, installations });
const result = await catalog.call('military_awareness', {
location: { lat: 32.7, lon: -117.2 },
radius_km: 100,
});
assert.equal(
result.summary,
'Military awareness within 100 km of 32.7000, -117.2000: ' +
'1 military aircraft in 100 km around 32.700, -117.200. ' +
'1 mapped military installation in 100 km around 32.700, -117.200.',
);
assert.deepEqual(Object.keys(result.data.sections), [
'military_aircraft',
'aircraft',
'installations',
]);
assert.deepEqual(
result.data.sections.military_aircraft.data.rows.map((row) => row.id),
['ae1234'],
);
assert.deepEqual(result.data.sections.aircraft, {
unavailable: true,
reason: 'unavailable right now',
});
assert.equal(result.data.radius_km, 100);
assert.equal(
(await catalog.call('military_awareness', { location: { lat: 0, lon: 0 } }))
.data.radius_km,
250,
);
await assert.rejects(
catalog.call('military_awareness', {
location: { lat: 0, lon: 0 },
radius_km: 500,
}),
/radius_km/,
);
});