Files
gods-eye-view/src/tools/queries/brief.js
T
German Khokhlov 03c168d7c9 refactor(hud): share the warning line and trim the summary helpers
One provenanceLine() builds STATE | text | LAYERS for the HUD and
get_hud_caption. hasWord is one lookaround regex, capAtWord one grapheme
pass. Feed-state helpers are imported from layerSnapshot.js directly, and the
kept-whole sanitizer cases are one table.
2026-11-03 14:45:32 +04:00

324 lines
9.4 KiB
JavaScript

/** Composite queries that combine other tools' answers for one area. */
import { suggestView } from '../views.js';
import {
feedProvenanceEnvelope,
hudSummaryMatchesProvenance,
provenanceLine,
provenanceTag,
} from '../../data/layerSnapshot.js';
import { defineTool, ToolError } from '../catalog.js';
import {
AREA_SCHEMA,
POINT_SCHEMA,
areaCenter,
resolveArea,
resolvePoint,
} from '../area.js';
const SECTION_LIMIT = 5;
const AWARENESS_LIMIT = 10;
const AWARENESS_RADIUS_KM = 250;
/** Brief sections: the tool each one calls and the arguments it passes. */
const SECTIONS = [
{
key: 'weather',
label: 'Weather',
tool: 'get_weather',
args: (area, center) => ({
location: area.argument.place
? { place: area.argument.place }
: { lat: center.lat, lon: center.lon },
}),
},
{
key: 'earthquakes',
layer: 'earthquakes',
label: 'Earthquakes',
tool: 'get_earthquakes',
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
},
{
key: 'fires',
layer: 'local-firms',
label: 'Active fires',
tool: 'get_active_fires',
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
},
{
key: 'aircraft',
layer: 'flights',
label: 'Aircraft',
tool: 'aircraft_in_area',
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
},
{
key: 'vessels',
layer: 'ais-live-vessels',
label: 'Ships',
tool: 'vessels_in_area',
args: (area) => ({ area: area.argument, limit: SECTION_LIMIT }),
},
{
key: 'cyclones',
layer: 'weather-cyclones',
label: 'Tropical cyclones',
tool: 'get_cyclones',
args: (area) => ({ area: area.argument }),
},
];
/** Military contacts around a point, as the app's military awareness panel. */
const AWARENESS_SECTIONS = [
{
key: 'military_aircraft',
tool: 'aircraft_in_area',
args: (area) => ({
area: area.argument,
military: true,
limit: AWARENESS_LIMIT,
}),
},
{
key: 'aircraft',
tool: 'aircraft_in_area',
args: (area) => ({ area: area.argument, limit: AWARENESS_LIMIT }),
},
{
key: 'vessels',
tool: 'vessels_in_area',
args: (area) => ({ area: area.argument, limit: AWARENESS_LIMIT }),
},
{
key: 'installations',
tool: 'find_military_installations',
args: (area) => ({ area: area.argument, limit: AWARENESS_LIMIT }),
},
];
/** A section's feed state for the caption, from what its result reported. */
function sectionFeedState(section) {
if (section.unavailable) return 'unavailable';
const data = section.data || {};
if (data.freshness === 'stale' || data.stale === true) return 'stale';
// Answered, but some of the feeds or satellites behind it did not.
if (data.missing_sources?.length || data.unavailable_feeds?.length)
return 'degraded';
return 'nominal';
}
/**
* The resolved area, plus the argument sections receive. A named place is
* passed by name, so sections resolve it to the same point and label as a
* direct query does (place lookups are cached).
*/
function sectionArea(resolved, original) {
return {
...resolved,
argument: original?.place
? { place: original.place }
: resolved.center
? {
lat: resolved.center.lat,
lon: resolved.center.lon,
radius_km: resolved.center.radiusKm,
}
: {
bbox: [
resolved.west,
resolved.south,
resolved.east,
resolved.north,
],
},
};
}
/**
* Run every section whose tool the catalog offers, through the catalog so
* replaced tools and interceptors apply. A failing section is reported as
* unavailable instead of failing the whole answer.
*/
async function runSections(all, area, { tools, signal }) {
const center = areaCenter(area);
const sections = all.filter(({ tool }) => tools.has(tool));
const results = await Promise.allSettled(
sections.map(({ tool, args: build }) =>
tools.call(tool, build(area, center)),
),
);
signal?.throwIfAborted();
const answers = {};
const lines = [];
sections.forEach(({ key }, index) => {
const result = results[index];
if (result.status === 'fulfilled') {
answers[key] = { summary: result.value.summary, data: result.value.data };
lines.push(result.value.summary);
} else {
const known = result.reason instanceof ToolError;
answers[key] = {
unavailable: true,
reason: known ? result.reason.message : 'unavailable right now',
};
}
});
return { center, answers, lines };
}
async function buildBrief(args, { services, signal, tools }) {
// Sections receive the resolved box so a place name is looked up once.
const area = sectionArea(
await resolveArea(args.area, { services, signal }),
args.area,
);
const { center, answers, lines } = await runSections(SECTIONS, area, {
tools,
signal,
});
return { area, center, brief: answers, lines };
}
export const situationBrief = defineTool({
name: 'situation_brief',
title: 'Situation brief',
description:
'One overview of an area: current weather, recent earthquakes, active ' +
'fires, aircraft overhead, ships and tropical cyclones, each summarized with ' +
'its top items. Sections that are unavailable are marked as such.',
inputSchema: {
type: 'object',
properties: { area: AREA_SCHEMA },
required: ['area'],
additionalProperties: false,
},
requires: ['weather'],
async run(args, context) {
const { area, brief, lines } = await buildBrief(args, context);
return {
summary: `Situation in ${area.label}: ${lines.join(' ')}`,
data: {
view: suggestView(context.services, {
area,
layers: SECTIONS.filter(({ key }) => brief[key])
.map(({ layer }) => layer)
.filter(Boolean),
}),
area: area.label,
sections: brief,
},
};
},
});
export const getHudCaption = defineTool({
name: 'get_hud_caption',
title: 'Heads-up display caption',
description:
"The short heads-up display caption God's Eye View would show for an " +
'area, written by the app from the same overview situation_brief gives.',
inputSchema: {
type: 'object',
properties: { area: AREA_SCHEMA },
required: ['area'],
additionalProperties: false,
},
requires: ['weather', 'summary'],
async run(args, context) {
const { area, center, brief, lines } = await buildBrief(args, context);
// The app's caption context: place labels and each section as a layer
// with the feed state its result reported, failures included.
const layers = SECTIONS.filter(({ key }) => brief[key]).map(
({ key, label }) => ({
id: key,
name: label,
enabled: true,
feedState: sectionFeedState(brief[key]),
}),
);
const feedProvenance = feedProvenanceEnvelope(layers);
const response = await context.services.summary.summarize(
{
placeLabels: [area.label],
location: area.label,
center,
observations: lines,
enabledLayerLabels: layers.map((layer) => layer.name),
enabledLayers: layers.map(({ id, name, feedState }) => ({
id,
name,
feedState,
})),
feedProvenance,
},
{ signal: context.signal },
);
const words = response?.data?.summary;
if (!response?.ok || typeof words !== 'string' || !words)
throw new ToolError('unavailable', 'The caption service did not answer');
// As on the HUD: the app's own state leads and its layers trail, and a
// reply that names another state than the app's keeps only the place.
const caption = provenanceLine(
hudSummaryMatchesProvenance(response.data, feedProvenance)
? words
: area.label,
provenanceTag(feedProvenance),
);
return {
summary: caption,
data: {
area: area.label,
caption,
feedState: feedProvenance.overall,
},
};
},
});
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, tools }) {
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, {
tools,
signal,
});
return {
summary: `Military awareness within ${radiusKm} km of ${point.label}: ${lines.join(' ')}`,
data: {
view: suggestView(services, {
area,
layers: ['military', 'ais-live-vessels', 'military-installations'],
}),
location: point,
radius_km: radiusKm,
sections: answers,
},
};
},
});