refactor(reasoning)!: remove Node-RED engine and integrations
BREAKING CHANGE: NODE_RED reasoning is no longer supported. Unsupported mode values fall back to PROXY. PROXY and OPENAI remain available.
This commit is contained in:
@@ -115,7 +115,7 @@ by Git. The launcher serves source through Vite; it does not use `dist/` or
|
||||
|
||||
Tests cover session initialization, response sequencing, cumulative transcripts,
|
||||
interruptions, queue polling, admission deadlines, cancellation, microphone-permission cancellation, tool
|
||||
results, external reasoning and `LOG_ONLY`, late-response cleanup, the idle
|
||||
results, external reasoning, late-response cleanup, the idle
|
||||
timeout, settings, RAG, profiles, and sanitized Markdown/export behavior.
|
||||
Launcher tests start temporary local children to check readiness, environment
|
||||
forwarding, failures, and shutdown of complete process groups.
|
||||
@@ -144,8 +144,7 @@ quick actions remain absent, matching their disabled state in the old UI.
|
||||
Native speech stays at the current project's 16 kHz PCM rate. The backend
|
||||
continues to own VAD, transcription, model inference, synthesis, upstream
|
||||
credentials, and ApiPilot execution. Browser `SpeechSynthesis` is used only
|
||||
when the configured external reasoning service returns an answer instead of
|
||||
`log_only`.
|
||||
when the configured external reasoning service returns an answer.
|
||||
|
||||
Session startup waits for audio initialization and `session.created`, then sends
|
||||
`session.update` before microphone audio or the initial greeting. The speech
|
||||
|
||||
@@ -79,7 +79,7 @@ export async function getTools(signal: AbortSignal, refresh = false) {
|
||||
return result.data.tools;
|
||||
}
|
||||
|
||||
export const reasoningSchema = z.object({ answer: z.string().default(""), log_only: z.boolean().default(false) });
|
||||
export const reasoningSchema = z.object({ answer: z.string().default("") });
|
||||
export const searchSchema = z.object({
|
||||
answer: z.string().nullish(),
|
||||
results: z.array(z.object({ title: z.string(), snippet: z.string().default(""), url: z.string() })).default([]),
|
||||
|
||||
@@ -9,7 +9,7 @@ export const configSchema = z.object({
|
||||
allowDirect: z.boolean().default(true),
|
||||
s2sUrl: z.string().default(""),
|
||||
primaryPrompt: z.string().default(""),
|
||||
reasoningType: z.enum(["PROXY", "OPENAI", "NODE_RED"]).catch("PROXY"),
|
||||
reasoningType: z.enum(["PROXY", "OPENAI"]).catch("PROXY"),
|
||||
injectProfile: z.boolean().default(false),
|
||||
debugPrompt: z.boolean().default(false),
|
||||
startSessionMode: z.string().default(""),
|
||||
|
||||
@@ -63,7 +63,7 @@ beforeEach(() => {
|
||||
}
|
||||
const pathname = new URL(path, "http://localhost").pathname;
|
||||
const responses: Record<string, unknown> = {
|
||||
"/api/config": { reasoningType: "NODE_RED", injectProfile: false, s2sUrl: "ws://localhost/v1/realtime" },
|
||||
"/api/config": { reasoningType: "OPENAI", injectProfile: false, s2sUrl: "ws://localhost/v1/realtime" },
|
||||
"/api/user/phone": { result: { name: "Ana" } },
|
||||
"/api/user/reports": { count: 0 },
|
||||
};
|
||||
@@ -98,17 +98,28 @@ describe("voice application lifecycle", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps LOG_ONLY reasoning on the native voice path and preserves separate profile context", async () => {
|
||||
it("renders an external answer and preserves separate profile context", async () => {
|
||||
const client = await start();
|
||||
client.emit("transcript", { id: "u1", role: "user", text: "Hola", partial: false });
|
||||
pendingReasoning?.(new Response(JSON.stringify({ log_only: true, answer: "" })));
|
||||
pendingReasoning?.(new Response(JSON.stringify({ answer: " Hola, Ana " })));
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
const request = fetchMock.mock.calls.find(([path]) => path === "/api/reasoning");
|
||||
expect(request?.[1]?.body).toContain('"phoneNumber":"1234"');
|
||||
expect(request?.[1]?.body).toContain("Caller profile resolved");
|
||||
expect(client.options.instructions).not.toContain("Caller profile resolved");
|
||||
expect(client.renderExternalAnswer).toHaveBeenCalledWith("Hola, Ana");
|
||||
expect(controller.getSnapshot().messages.map((message) => message.text)).toEqual(["Hola", "Hola, Ana"]);
|
||||
});
|
||||
it.each(["PROXY", "UNSUPPORTED"])("keeps native reasoning for mode %s", async (reasoningType) => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ reasoningType, s2sUrl: "ws://localhost/v1/realtime" })),
|
||||
);
|
||||
const client = await start();
|
||||
client.emit("transcript", { id: "u1", role: "user", text: "Hola", partial: false });
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(fetchMock.mock.calls.some(([path]) => path === "/api/reasoning")).toBe(false);
|
||||
expect(client.renderExternalAnswer).not.toHaveBeenCalled();
|
||||
expect(controller.getSnapshot().messages).toHaveLength(1);
|
||||
expect(controller.getSnapshot().messages.map((message) => message.text)).toEqual(["Hola"]);
|
||||
});
|
||||
it("discards external reasoning that finishes after the session was stopped", async () => {
|
||||
const client = await start();
|
||||
|
||||
@@ -379,7 +379,7 @@ export class VoiceController {
|
||||
},
|
||||
this.requests.signal,
|
||||
);
|
||||
if (this.client !== client || result.log_only) {
|
||||
if (this.client !== client) {
|
||||
return;
|
||||
}
|
||||
const answer = result.answer.trim();
|
||||
|
||||
Reference in New Issue
Block a user