mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge main into feat/voice-core
Keeps the voice-core prompt rules and adds main's world-query and vessels-vs-visuals rules; the tool catalog wraps the deixis-aware runner. Also fixes two things the branch had before the merge: - Live voice could not start: OpenAI rejects track_entity's top-level anyOf. Model-facing tools now omit top-level combinators; the runner and benchmark still require a query or a referent. - The Contacts-centred analyst answer dropped the viewport coverage note.
This commit is contained in:
commit
e7cb9b02b5
191 files changed
+20005
-1450
No files matched your search
@@ -901,12 +901,20 @@ export function createActionTools(descriptions = {}) {
|
||||
if (!schemas.some((schema) => schema.name === name))
|
||||
throw new TypeError('Unknown action description: ' + name);
|
||||
}
|
||||
return schemas.map((schema) => ({
|
||||
type: 'function',
|
||||
...describe(schema, descriptions[schema.name] || {}),
|
||||
}));
|
||||
return schemas.map((schema) => {
|
||||
const tool = {
|
||||
type: 'function',
|
||||
...describe(schema, descriptions[schema.name] || {}),
|
||||
};
|
||||
// Function-calling APIs reject anyOf/oneOf/allOf at the top level of
|
||||
// parameters; the runner and benchmark validate those rules themselves.
|
||||
for (const key of TOP_LEVEL_COMBINATORS) delete tool.parameters?.[key];
|
||||
return tool;
|
||||
});
|
||||
}
|
||||
|
||||
const TOP_LEVEL_COMBINATORS = ['anyOf', 'oneOf', 'allOf'];
|
||||
|
||||
function describe(schema, metadata, allowDescription = true) {
|
||||
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
|
||||
throw new TypeError('Descriptions must be an object');
|
||||
|
||||
@@ -3,7 +3,10 @@ import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { GEV_ACTION_SCHEMAS, createActionTools } from './actionSchemas.js';
|
||||
import { GEV_REALTIME_TOOLS } from '../../server/providers/openai/tools.js';
|
||||
import { validateArgs } from '../../scripts/voice-bench/grade.mjs';
|
||||
import {
|
||||
validateArgs,
|
||||
validateCall,
|
||||
} from '../../scripts/voice-bench/grade.mjs';
|
||||
|
||||
const stable = (value) =>
|
||||
Array.isArray(value)
|
||||
@@ -30,8 +33,9 @@ test('the complete Realtime tool payload pins the manifest-generated layer relea
|
||||
digest,
|
||||
// Re-derived for the voice layer manifest (generated layer enums and
|
||||
// aliases), point-and-ask (pointer sentinels, referent args) and the
|
||||
// consolidated tool wording (each policy stated once).
|
||||
'a824c2ed0ceb553eb3041350856db05dbfc5abad8a3fa17d29dc378bf7a732d2',
|
||||
// consolidated tool wording (each policy stated once); model-facing tools
|
||||
// omit top-level anyOf (OpenAI rejects it).
|
||||
'66c955a41e36c6e7d2d255a950993128528288be13946fe4f7dacb05ad2ba291',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -139,7 +143,7 @@ test('all legacy action arguments are byte-identical after removing the delibera
|
||||
});
|
||||
|
||||
test('track_entity accepts a referent alone but rejects an empty target', () => {
|
||||
const schema = GEV_REALTIME_TOOLS.find(
|
||||
const schema = GEV_ACTION_SCHEMAS.find(
|
||||
(tool) => tool.name === 'track_entity',
|
||||
).parameters;
|
||||
assert.deepEqual(validateArgs(schema, { referent: 2 }), []);
|
||||
@@ -151,3 +155,28 @@ test('track_entity accepts a referent alone but rejects an empty target', () =>
|
||||
assert.ok(validateArgs(schema, { referent: 0 }).length > 0);
|
||||
assert.ok(validateArgs(schema, { referent: 6 }).length > 0);
|
||||
});
|
||||
|
||||
test('tools sent to the model never carry a top-level schema combinator', () => {
|
||||
for (const tool of createActionTools()) {
|
||||
assert.equal(tool.parameters.type, 'object', tool.name);
|
||||
for (const key of ['anyOf', 'oneOf', 'allOf', 'enum', 'not'])
|
||||
assert.equal(tool.parameters[key], undefined, `${tool.name}.${key}`);
|
||||
}
|
||||
// The runner still knows track_entity needs a query or a referent.
|
||||
const track = GEV_ACTION_SCHEMAS.find((s) => s.name === 'track_entity');
|
||||
assert.ok(track.parameters.anyOf);
|
||||
});
|
||||
|
||||
test('the benchmark still rejects an empty track_entity although the model-facing tool omits anyOf', () => {
|
||||
assert.ok(
|
||||
validateCall(GEV_REALTIME_TOOLS, { name: 'track_entity', args: {} })
|
||||
.length > 0,
|
||||
);
|
||||
assert.deepEqual(
|
||||
validateCall(GEV_REALTIME_TOOLS, {
|
||||
name: 'track_entity',
|
||||
args: { referent: 2 },
|
||||
}),
|
||||
[],
|
||||
);
|
||||
});
|
||||
@@ -691,7 +691,7 @@ export function createGevActionRunner({
|
||||
const style = normalizeStyle(args.style);
|
||||
if (!style)
|
||||
throw new Error(`Unknown visual style: ${args.style || 'missing'}`);
|
||||
styleManager.setStyle(style);
|
||||
styleManager.setStyle(style, { userInitiated: true });
|
||||
return { ok: true, action: 'set_visual_style', style };
|
||||
}
|
||||
|
||||
@@ -4870,6 +4870,13 @@ async function runAnalystQuery(
|
||||
// arbitrary points — only "how many aircraft around <this contact>" is
|
||||
// unified, because that is the question the panel is already answering.
|
||||
if (entityWindow) {
|
||||
// The detached Contacts snapshot keeps its own coverage; carry the same
|
||||
// viewport note so both answer shapes say what they measured.
|
||||
if (viewportScoped && entityWindow.coverage)
|
||||
entityWindow.coverage = {
|
||||
...entityWindow.coverage,
|
||||
note: result.coverage?.note,
|
||||
};
|
||||
return entityWindow;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { toFunctionOutput } from '../tools/functions.js';
|
||||
import { GEV_ACTION_SCHEMAS } from './actionSchemas.js';
|
||||
import { createGevActionRunner } from './gevActions.js';
|
||||
import { createVoiceCommands } from './commands.js';
|
||||
import { cameraPoseKey, createPointerTracker } from './pointerContext.js';
|
||||
@@ -5,6 +7,25 @@ import { createPointerPicker } from './pointerPick.js';
|
||||
import { createReferentRegistry } from './referents.js';
|
||||
export * from './realtimeController.js';
|
||||
|
||||
const ACTION_NAMES = new Set(GEV_ACTION_SCHEMAS.map((schema) => schema.name));
|
||||
|
||||
/**
|
||||
* Run app actions through `runner` and every other tool the catalog has
|
||||
* through the catalog. `loadCatalog` resolves the catalog when first needed.
|
||||
*/
|
||||
export function withToolCatalog(runner, loadCatalog) {
|
||||
if (typeof loadCatalog !== 'function') return runner;
|
||||
return async function runGevTool(name, args, options = {}) {
|
||||
if (ACTION_NAMES.has(name)) return runner(name, args, options);
|
||||
const catalog = await loadCatalog();
|
||||
if (!catalog?.get(name)) return runner(name, args, options);
|
||||
const result = await catalog.call(name, args ?? {}, {
|
||||
signal: options.signal,
|
||||
});
|
||||
return toFunctionOutput(name, result);
|
||||
};
|
||||
}
|
||||
|
||||
/** Compose the standalone action runner with the voice controls. */
|
||||
export function initGevVoiceCommands(options) {
|
||||
const canvas = options.viewer?.scene?.canvas || null;
|
||||
@@ -29,7 +50,10 @@ export function initGevVoiceCommands(options) {
|
||||
...options,
|
||||
pointer,
|
||||
referents,
|
||||
runner: createGevActionRunner({ ...options, deixis }),
|
||||
runner: withToolCatalog(
|
||||
createGevActionRunner({ ...options, deixis }),
|
||||
options.toolCatalog,
|
||||
),
|
||||
});
|
||||
deixis.imageFrame = () => commands.retainedImageFrame?.() || null;
|
||||
deixis.onPointerUsed = () => commands.announcePointer?.();
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
readStoredVoiceLimits,
|
||||
writeStoredVoiceTier,
|
||||
writeStoredVoiceLimits,
|
||||
withToolCatalog,
|
||||
} from './gevRealtime.js';
|
||||
import { createVoiceCostTracker } from './voiceCost.js';
|
||||
|
||||
@@ -3725,3 +3726,43 @@ test('a claimed Space hold barges in on the assistant; a short tap never does',
|
||||
const testPlaceSearch = () => createStandalonePlaceSearch({ resolveApiKey: () => globalThis.window?.__GOOGLE_MAPS_API_KEY__ });
|
||||
function createGevActionRunner(options) { return createActionRunner({ placeSearch: testPlaceSearch(), ...options }); }
|
||||
function controlRadio(viewer, manager, args, options) { return runControlRadio(viewer, manager, args, { placeSearch: testPlaceSearch(), ...options }); }
|
||||
|
||||
test('voice runs app actions itself and other tools through the catalog', async () => {
|
||||
const actions = [];
|
||||
const runner = async (name, args) => {
|
||||
actions.push([name, args]);
|
||||
return { ok: true, action: name };
|
||||
};
|
||||
const calls = [];
|
||||
let loads = 0;
|
||||
const catalog = {
|
||||
get: (name) => (name === 'get_wind' ? { name } : undefined),
|
||||
async call(name, args, options) {
|
||||
calls.push([name, args, options.signal]);
|
||||
return { summary: 'Calm.', data: { calm: true } };
|
||||
},
|
||||
};
|
||||
const run = withToolCatalog(runner, async () => {
|
||||
loads += 1;
|
||||
return catalog;
|
||||
});
|
||||
const signal = new AbortController().signal;
|
||||
assert.deepEqual(await run('zoom_to_globe', {}), {
|
||||
ok: true,
|
||||
action: 'zoom_to_globe',
|
||||
});
|
||||
assert.equal(loads, 0);
|
||||
assert.deepEqual(
|
||||
await run('get_wind', { location: { place: 'Oslo' } }, { signal }),
|
||||
{ ok: true, tool: 'get_wind', summary: 'Calm.', data: { calm: true } },
|
||||
);
|
||||
assert.deepEqual(calls, [
|
||||
['get_wind', { location: { place: 'Oslo' } }, signal],
|
||||
]);
|
||||
await run('not_a_tool', {});
|
||||
assert.deepEqual(
|
||||
actions.map(([name]) => name),
|
||||
['zoom_to_globe', 'not_a_tool'],
|
||||
);
|
||||
assert.equal(withToolCatalog(runner, undefined), runner);
|
||||
});
|
||||
Reference in new issue
Block a user