mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix(voice): offer typed chat the same Overpass tools as voice
Main now offers osm_query, and names it in the prompt, only when Overpass is configured. Typed chat follows the same switch on both providers: its prompt, the tools it offers and the calls it accepts.
This commit is contained in:
1 parent
fe78b3ebc9
commit
39165bb199
4 files changed
+59
-13
No files matched your search
@@ -15,8 +15,9 @@ export const TEXT_WORDING = Object.freeze([
|
|||||||
|
|
||||||
// Shared-manual adaptation follows Darren Dignam's GEV COMMAND approach (#976).
|
// Shared-manual adaptation follows Darren Dignam's GEV COMMAND approach (#976).
|
||||||
// Keep our Radio handoff and annotation outcome policies from the shared manual.
|
// Keep our Radio handoff and annotation outcome policies from the shared manual.
|
||||||
export function textInstructions(annotationGuidance) {
|
// `options.overpass` matches the tools offered, as for voice.
|
||||||
return realtimeDirectives(annotationGuidance)
|
export function textInstructions(annotationGuidance, options) {
|
||||||
|
return realtimeDirectives(annotationGuidance, options)
|
||||||
.map((line, index) => {
|
.map((line, index) => {
|
||||||
if (index === 0)
|
if (index === 0)
|
||||||
return "You are GEV Chat, a concise text assistant for a Cesium geospatial app called God's Eye View.";
|
return "You are GEV Chat, a concise text assistant for a Cesium geospatial app called God's Eye View.";
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createTextChatHandler } from '../common/textChat.js';
|
import { createTextChatHandler } from '../common/textChat.js';
|
||||||
import { invalidCalls } from '../common/textTools.js';
|
import { invalidCalls } from '../common/textTools.js';
|
||||||
import { textInstructions } from '../common/textInstructions.js';
|
import { textInstructions } from '../common/textInstructions.js';
|
||||||
import { GEV_REALTIME_TOOLS } from '../openai/tools.js';
|
import { GEV_REALTIME_TOOLS, offeredTools } from '../openai/tools.js';
|
||||||
|
import { resolveOverpassUpstreams } from '../overpass/constants.js';
|
||||||
import { normalizeGeminiModel } from './config.js';
|
import { normalizeGeminiModel } from './config.js';
|
||||||
|
|
||||||
// The Gemini token route's throttle default (GEV_RATELIMIT_GEMINI_PER_MIN).
|
// The Gemini token route's throttle default (GEV_RATELIMIT_GEMINI_PER_MIN).
|
||||||
@@ -80,6 +81,9 @@ export function validChatContents(contents, names) {
|
|||||||
export function createGeminiChatHandler({
|
export function createGeminiChatHandler({
|
||||||
annotationGuidance,
|
annotationGuidance,
|
||||||
tools = GEV_REALTIME_TOOLS,
|
tools = GEV_REALTIME_TOOLS,
|
||||||
|
// osm_query is offered, and the prompt names it, only with Overpass, as
|
||||||
|
// for voice.
|
||||||
|
overpassConfigured = () => resolveOverpassUpstreams().length > 0,
|
||||||
resolveChatModel = () =>
|
resolveChatModel = () =>
|
||||||
process.env.GEMINI_CHAT_MODEL || 'gemini-3.5-flash-lite',
|
process.env.GEMINI_CHAT_MODEL || 'gemini-3.5-flash-lite',
|
||||||
resolveApiKey = () => process.env.GEMINI_API_KEY,
|
resolveApiKey = () => process.env.GEMINI_API_KEY,
|
||||||
@@ -102,17 +106,20 @@ export function createGeminiChatHandler({
|
|||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
if (!pairedContents(input.contents)) return null;
|
if (!pairedContents(input.contents)) return null;
|
||||||
|
const overpass = overpassConfigured();
|
||||||
return {
|
return {
|
||||||
endpoint: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
|
endpoint: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
|
||||||
headers: (key) => ({ 'x-goog-api-key': key }),
|
headers: (key) => ({ 'x-goog-api-key': key }),
|
||||||
body: {
|
body: {
|
||||||
contents: input.contents,
|
contents: input.contents,
|
||||||
systemInstruction: {
|
systemInstruction: {
|
||||||
parts: [{ text: textInstructions(annotationGuidance) }],
|
parts: [
|
||||||
|
{ text: textInstructions(annotationGuidance, { overpass }) },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
tools: [
|
tools: [
|
||||||
{
|
{
|
||||||
functionDeclarations: tools.map(
|
functionDeclarations: offeredTools(tools, { overpass }).map(
|
||||||
({ name, description, parameters }) => ({
|
({ name, description, parameters }) => ({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -144,7 +151,10 @@ export function createGeminiChatHandler({
|
|||||||
return {
|
return {
|
||||||
content,
|
content,
|
||||||
finishReason: candidate.finishReason,
|
finishReason: candidate.finishReason,
|
||||||
invalidCalls: invalidCalls(calls, tools),
|
invalidCalls: invalidCalls(
|
||||||
|
calls,
|
||||||
|
offeredTools(tools, { overpass: overpassConfigured() }),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { preferCodexOAuth } from './codex-auth.js';
|
|||||||
import { OPENAI_DEFAULT_PER_MIN } from './rate-limit.js';
|
import { OPENAI_DEFAULT_PER_MIN } from './rate-limit.js';
|
||||||
import { invalidCalls } from '../common/textTools.js';
|
import { invalidCalls } from '../common/textTools.js';
|
||||||
import { textInstructions } from '../common/textInstructions.js';
|
import { textInstructions } from '../common/textInstructions.js';
|
||||||
import { GEV_REALTIME_TOOLS } from './tools.js';
|
import { GEV_REALTIME_TOOLS, offeredTools } from './tools.js';
|
||||||
|
import { resolveOverpassUpstreams } from '../overpass/constants.js';
|
||||||
|
|
||||||
const object = (value) =>
|
const object = (value) =>
|
||||||
value && typeof value === 'object' && !Array.isArray(value);
|
value && typeof value === 'object' && !Array.isArray(value);
|
||||||
@@ -66,6 +67,9 @@ function validMessages(messages) {
|
|||||||
export function createOpenAiChatHandler({
|
export function createOpenAiChatHandler({
|
||||||
annotationGuidance,
|
annotationGuidance,
|
||||||
tools = GEV_REALTIME_TOOLS,
|
tools = GEV_REALTIME_TOOLS,
|
||||||
|
// osm_query is offered, and the prompt names it, only with Overpass, as
|
||||||
|
// for voice.
|
||||||
|
overpassConfigured = () => resolveOverpassUpstreams().length > 0,
|
||||||
resolveApiKey = () => process.env.OPENAI_API_KEY,
|
resolveApiKey = () => process.env.OPENAI_API_KEY,
|
||||||
resolveChatModel = () => process.env.OPENAI_CHAT_MODEL || 'gpt-4.1-mini',
|
resolveChatModel = () => process.env.OPENAI_CHAT_MODEL || 'gpt-4.1-mini',
|
||||||
resolveProviderRateLimit = () => process.env.GEV_RATELIMIT_OPENAI_PER_MIN,
|
resolveProviderRateLimit = () => process.env.GEV_RATELIMIT_OPENAI_PER_MIN,
|
||||||
@@ -102,6 +106,7 @@ export function createOpenAiChatHandler({
|
|||||||
!validMessages(input.messages)
|
!validMessages(input.messages)
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
const overpass = overpassConfigured();
|
||||||
return {
|
return {
|
||||||
endpoint: 'https://api.openai.com/v1/chat/completions',
|
endpoint: 'https://api.openai.com/v1/chat/completions',
|
||||||
headers: (key) => ({ Authorization: `Bearer ${key}` }),
|
headers: (key) => ({ Authorization: `Bearer ${key}` }),
|
||||||
@@ -112,13 +117,18 @@ export function createOpenAiChatHandler({
|
|||||||
store: false,
|
store: false,
|
||||||
max_completion_tokens: 4096,
|
max_completion_tokens: 4096,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: textInstructions(annotationGuidance) },
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: textInstructions(annotationGuidance, { overpass }),
|
||||||
|
},
|
||||||
...input.messages,
|
...input.messages,
|
||||||
],
|
],
|
||||||
tools: tools.map(({ name, description, parameters }) => ({
|
tools: offeredTools(tools, { overpass }).map(
|
||||||
type: 'function',
|
({ name, description, parameters }) => ({
|
||||||
function: { name, description, parameters, strict: false },
|
type: 'function',
|
||||||
})),
|
function: { name, description, parameters, strict: false },
|
||||||
|
}),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -149,7 +159,10 @@ export function createOpenAiChatHandler({
|
|||||||
return {
|
return {
|
||||||
content,
|
content,
|
||||||
finishReason: choice.finish_reason,
|
finishReason: choice.finish_reason,
|
||||||
invalidCalls: invalidCalls(calls, tools),
|
invalidCalls: invalidCalls(
|
||||||
|
calls,
|
||||||
|
offeredTools(tools, { overpass: overpassConfigured() }),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import http from 'node:http';
|
|||||||
import { once } from 'node:events';
|
import { once } from 'node:events';
|
||||||
import { createGeminiChatHandler } from '../server/providers/gemini/chat.js';
|
import { createGeminiChatHandler } from '../server/providers/gemini/chat.js';
|
||||||
import { createOpenAiChatHandler } from '../server/providers/openai/chat.js';
|
import { createOpenAiChatHandler } from '../server/providers/openai/chat.js';
|
||||||
|
import { GEV_REALTIME_TOOLS } from '../server/providers/openai/tools.js';
|
||||||
import {
|
import {
|
||||||
realtimeDirectives,
|
realtimeDirectives,
|
||||||
realtimeInstructions,
|
realtimeInstructions,
|
||||||
@@ -514,6 +515,27 @@ for (const [name, provider] of Object.entries(providers)) {
|
|||||||
assert.equal((await (await unread.send()).json()).code, 'API_KEY_REJECTED');
|
assert.equal((await (await unread.send()).json()).code, 'API_KEY_REJECTED');
|
||||||
assert.equal(cancelled, true);
|
assert.equal(cancelled, true);
|
||||||
});
|
});
|
||||||
|
test(`${name} offers osm_query, and names it, only with Overpass, as voice does`, async (t) => {
|
||||||
|
for (const overpass of [false, true]) {
|
||||||
|
const f = await fixture(t, provider, undefined, {
|
||||||
|
tools: GEV_REALTIME_TOOLS,
|
||||||
|
overpassConfigured: () => overpass,
|
||||||
|
});
|
||||||
|
assert.equal((await f.send()).status, 200);
|
||||||
|
const { body } = f.calls[0];
|
||||||
|
const offered =
|
||||||
|
name === 'openai'
|
||||||
|
? body.tools.map((tool) => tool.function.name)
|
||||||
|
: body.tools[0].functionDeclarations.map((tool) => tool.name);
|
||||||
|
const prompt =
|
||||||
|
name === 'openai'
|
||||||
|
? body.messages[0].content
|
||||||
|
: body.systemInstruction.parts[0].text;
|
||||||
|
assert.equal(offered.includes('osm_query'), overpass);
|
||||||
|
assert.equal(prompt.includes('osm_query'), overpass);
|
||||||
|
assert.ok(offered.includes('resolve_area'));
|
||||||
|
}
|
||||||
|
});
|
||||||
test(`${name} throttled replies say they are rate limited`, async (t) => {
|
test(`${name} throttled replies say they are rate limited`, async (t) => {
|
||||||
const local = await fixture(t, provider, undefined, {
|
const local = await fixture(t, provider, undefined, {
|
||||||
resolveRateLimit: () => '1',
|
resolveRateLimit: () => '1',
|
||||||
|
|||||||
Reference in new issue
Block a user