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

+1 -1
View File
@@ -17,7 +17,7 @@ stations, transit vehicles, road traffic flow, weather, weather map images
(radar, satellite, lightning), wind, the most recent satellite image of an
area, submarine cables, datacenters and dams, regional briefs, tropical cyclones, fire
perimeters, terrain height, military installations and map features, plus a
combined situation brief, the app's heads-up display caption, and a link that
combined situation brief, military awareness around a point, the app's heads-up display caption, and a link that
opens the app over an area. Tools reuse the layers' portable source factories, take a
shared `area` argument (place name, bounding box, or point and radius) and cap
lists at 25 rows by default. The app does not import the tools, so its runtime
+2 -1
View File
@@ -53,7 +53,7 @@ relative `/api/...` paths through an injected `fetchImpl`, so the same tool
code runs wherever an application routes those paths.
The `weather`, `regional`, `terrain`, `summary` and `features` services are the
application request services from `gods-eye-view/application/requests`.
`situation_brief` runs each section whose services are supplied and marks the
`situation_brief` and `military_awareness` run each section whose services are supplied and mark the
others unavailable. `app` is `{ baseUrl }`, the address links open. The `bikeshare` service is `{ systems, getStations }`: the system registry and
the GBFS source. `createGeocodePlaceService` resolves place names through `/api/geocode`;
`createPlaceSearchService` searches `/api/google/*` and reports when no search key
@@ -140,5 +140,6 @@ public feeds the sources already use.
| `find_military_installations` | `installations` | OpenStreetMap military sites in an area of at most 10° per side |
| `get_map_features` | `features` | Administrative areas, named places or monuments at a location (needs Overpass) |
| `situation_brief` | `weather` | Weather, earthquakes, fires, aircraft, ships and cyclones for an area, by section |
| `military_awareness` | `military` | Military and other aircraft, ships and military installations within 250 km of a point, by section |
| `get_hud_caption` | `weather`, `summary` | The app's heads-up display caption for an area |
| `open_in_gods_eye_view` | `app` | A share link looking straight down on an area, with chosen layers on |
+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/,
);
});