Skip to content
Merged
19 changes: 19 additions & 0 deletions apps/petrinaut-website/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,25 @@ The Brunch deployment must allow the website origin through its
microphone permission. Denying permission leaves the existing text composer
available and does not submit anything to Brunch.

When the preview cannot continue, the status panel distinguishes microphone
permission, microphone device, interrupted request, network, timeout, invalid
response, and unavailable/disabled failures. Permission and device failures
identify what to fix; network, timeout, and interrupted requests offer a
reconnect; invalid responses include a diagnostic reference for an operator;
and unavailable voice leaves the text composer as the fallback. Speech failures
always leave the canonical response visible to read.

Realtime connection and Speech requests carry a random `x-request-id` through
the browser and server route, and the existing Brunch transport sends the same
header on each chat request so Brunch's privacy-safe request inspection can
correlate that boundary. Browser and server diagnostics report operation,
stage, outcome, duration, request ID, andβ€”where applicableβ€”status or sanitized
error code. Voice responses also expose privacy-safe `Server-Timing` metrics.
These diagnostics never record audio, SDP, transcript or prompt contents,
canonical speech text, credentials, or provider response bodies. This
controlled-preview evidence does not enable production: production remains
unconditionally disabled by the server policy.

## Testing the API against the built output

A plain `yarn build && yarn vite preview` only serves the static `dist/` assets - `/api/chat` will 404 because the dev plugin is not loaded by `vite preview`. Use one of the options below to exercise the production code path locally.
Expand Down
2 changes: 2 additions & 0 deletions apps/petrinaut-website/api/voice/realtime-call.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createOpenAIRealtimeCallHandler } from "../../src/server/voice/openai-realtime-call";
import { reportVoiceDiagnostic } from "../../src/voice-diagnostics";

declare const process: {
env: Record<string, string | undefined>;
Expand All @@ -8,5 +9,6 @@ export default {
fetch: createOpenAIRealtimeCallHandler({
environment: process.env,
fetch: globalThis.fetch.bind(globalThis),
reportDiagnostic: reportVoiceDiagnostic,
}),
};
2 changes: 2 additions & 0 deletions apps/petrinaut-website/api/voice/speech.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createOpenAISpeechHandler } from "../../src/server/voice/openai-speech";
import { reportVoiceDiagnostic } from "../../src/voice-diagnostics";

declare const process: {
env: Record<string, string | undefined>;
Expand All @@ -8,5 +9,6 @@ export default {
fetch: createOpenAISpeechHandler({
environment: process.env,
fetch: globalThis.fetch.bind(globalThis),
reportDiagnostic: reportVoiceDiagnostic,
}),
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,20 @@ import { describe, expect, test, vi } from "vitest";
import { VoiceInterviewControl } from "../voice-interview/voice-interview-control";
import { getBrunchVoiceComposerControl } from "./local-storage-demo-app";

const defaultTransportOptions = vi.hoisted(() => ({
current: null as unknown,
}));

vi.mock("./brunch-principal", () => ({
getOrCreateBrunchPrincipal: () => "test-principal",
}));

vi.mock("@hashintel/petrinaut/ui", () => ({
DefaultChatTransport: class {},
DefaultChatTransport: class {
public constructor(options: unknown) {
defaultTransportOptions.current = options;
}
},
Petrinaut: () => null,
WalkthroughProvider: ({ children }: { children: ReactNode }) => children,
definePetrinautAiInteractiveTool: (definition: unknown) => definition,
Expand Down Expand Up @@ -42,4 +54,14 @@ describe("local storage demo Brunch voice integration", () => {
}
expect(control.type).toBe(VoiceInterviewControl);
});

test("correlates the existing Brunch transport request", () => {
const options = defaultTransportOptions.current as {
readonly headers: () => Record<string, string>;
};

expect(options.headers()["x-request-id"]).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
WalkthroughProvider,
} from "@hashintel/petrinaut/ui";

import { VOICE_REQUEST_ID_HEADER } from "../../../voice-diagnostics";
import { useSentryFeedbackAction } from "../sentry-feedback-button";
import { VoiceInterviewControl } from "../voice-interview/voice-interview-control";
import { brunchAskInteractiveTool } from "./brunch-ask-interactive-tool";
Expand Down Expand Up @@ -116,6 +117,7 @@ const stockChatTransport = new DefaultChatTransport({
api: brunchPreviewConfig.chatEndpoint,
headers: () => ({
[BRUNCH_PRINCIPAL_HEADER]: brunchPrincipal,
[VOICE_REQUEST_ID_HEADER]: crypto.randomUUID(),
}),
});

Expand Down
Loading
Loading