Files
alice-backend/server/tools.mjs
T

55 lines
5.2 KiB
JavaScript

// OpenRouter executes web search itself; Spotify calls are handled by the device.
const spotifyTypes = ['track', 'artist', 'album', 'playlist'];
const actions = {
pause: {}, resume: {}, next: {}, previous: {}, status: {}, queue: {},
volume: { volume: { type: 'integer', minimum: 0, maximum: 100 } },
seek: { positionMs: { type: 'integer', minimum: 0, maximum: 86400000 } },
shuffle: { enabled: { type: 'boolean' } },
repeat: { mode: { enum: ['off', 'track', 'context'] } },
enqueue: { query: { type: 'string', maxLength: 500 } },
};
export const tools = [
{ type: 'openrouter:web_search' },
{ type: 'function', function: { name: 'pc_audio', description: 'Start, stop or check the Windows PC audio mirror on the speaker. Use for explicit requests about sound from the PC, independently of Spotify.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['start', 'stop', 'status'] } }, required: ['action'], additionalProperties: false } } },
{ type: 'function', function: { name: 'spotify_play', description: 'Play a track, artist, album or playlist on Spotify. Pass type when the user names a kind of content. Normalize known transliterations.', parameters: { type: 'object', properties: { query: { type: 'string', maxLength: 500 }, uri: { type: 'string', maxLength: 500 }, type: { type: 'string', enum: spotifyTypes } }, additionalProperties: false } } },
{ type: 'function', function: { name: 'spotify_control', description: 'Control Spotify on the station. Use status for what is playing, queue to read upcoming tracks, enqueue to add a track by name, seek with an absolute position in milliseconds, and repeat with track, context (album or playlist), or off.', parameters: { type: 'object', properties: { action: { type: 'string', enum: Object.keys(actions) }, ...Object.assign({}, ...Object.values(actions)) }, required: ['action'], additionalProperties: false } } },
{ type: 'function', function: { name: 'spotify_playlist', description: 'List own playlists, read tracks, create a private playlist, or add/remove a track. Use playlistUri for an existing playlist, trackUri or trackQuery for a track, and name when creating.', parameters: { type: 'object', properties: {
action: { enum: ['list', 'read', 'create', 'add', 'remove'] }, name: { type: 'string', maxLength: 100 },
playlistUri: { type: 'string' }, trackUri: { type: 'string' }, trackQuery: { type: 'string', maxLength: 500 },
}, required: ['action'], additionalProperties: false } } },
{ type: 'function', function: { name: 'station_volume', description: 'Set the overall speaker volume for both speech and music. Use this for a general volume request.', parameters: { type: 'object', properties: { level: { type: 'integer', minimum: 0, maximum: 100 }, delta: { type: 'integer', enum: [-10, 10] } }, additionalProperties: false } } },
];
function validValue(value, schema) {
if (schema.enum) return schema.enum.includes(value);
if (schema.type === 'integer') return Number.isInteger(value) && value >= schema.minimum && value <= schema.maximum;
if (schema.type === 'boolean') return typeof value === 'boolean';
return typeof value === 'string' && !!value.trim() && value.length <= schema.maxLength;
}
export function validateTool(name, args) {
if (!args || typeof args !== 'object' || Array.isArray(args)) throw new Error('invalid_arguments');
const keys = Object.keys(args);
if (name === 'spotify_play') {
if (keys.some(k => !['query', 'uri', 'type'].includes(k)) || (!args.query && !args.uri) ||
keys.some(k => k === 'type' ? !spotifyTypes.includes(args[k]) : !validValue(args[k], { maxLength: 500 }))) throw new Error('invalid_arguments');
} else if (name === 'spotify_control') {
if (!Object.hasOwn(actions, args.action)) throw new Error('invalid_arguments');
const fields = Object.entries(actions[args.action]);
if (keys.length !== fields.length + 1 || fields.some(([key, schema]) => !validValue(args[key], schema))) throw new Error('invalid_arguments');
} else if (name === 'spotify_playlist') {
const required = { list: [], read: ['playlistUri'], create: ['name'], add: ['playlistUri'], remove: ['playlistUri'] }[args.action];
if (!required || keys.some(k => !['action', 'name', 'playlistUri', 'trackUri', 'trackQuery'].includes(k)) ||
required.some(k => !args[k]) || keys.some(k => typeof args[k] !== 'string' || args[k].length > 500)) throw new Error('invalid_arguments');
if (args.playlistUri && !/^spotify:playlist:[A-Za-z0-9]{22}$/.test(args.playlistUri)) throw new Error('invalid_arguments');
if (args.trackUri && !/^spotify:track:[A-Za-z0-9]{22}$/.test(args.trackUri)) throw new Error('invalid_arguments');
if (['add', 'remove'].includes(args.action) && !args.trackUri && !args.trackQuery) throw new Error('invalid_arguments');
} else if (name === 'pc_audio') {
if (keys.length !== 1 || !['start', 'stop', 'status'].includes(args.action)) throw new Error('invalid_arguments');
} else if (name === 'station_volume') {
if (keys.length !== 1 || ('delta' in args ? ![-10, 10].includes(args.delta) : !Number.isInteger(args.level) || args.level < 0 || args.level > 100)) throw new Error('invalid_arguments');
} else throw new Error('unknown_tool');
return args;
}