mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
feat: add a military awareness tool
This commit is contained in:
1 parent
28cfd39d1a
commit
a747b8a4e0
5 files changed
+197
-19
No files matched your search
+6
-1
@@ -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
@@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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/,
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user