Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions apps/operator-backend/src/brain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,26 @@ async function maybeRunTopLevelMepRouteRedline(req: ChatRequest, resolver = mayb
return isExplicitReadOnlyRedlineAnalysisRequest(req) ? null : resolver(req);
}

async function maybeRunTopLevelSemanticAecWorkflow(req: ChatRequest, resolver = maybeRunSemanticAecWorkflow): Promise<ChatResponse | null> {
return isExplicitReadOnlyRedlineAnalysisRequest(req) ? null : resolver(req);
}

export async function __testOnlyMaybeRunTopLevelMepRouteRedline(req: ChatRequest, resolver: typeof maybeRunDeterministicMepRouteRedline): Promise<ChatResponse | null> { return maybeRunTopLevelMepRouteRedline(req, resolver); }
export async function __testOnlyMaybeRunTopLevelSemanticAecWorkflow(req: ChatRequest, resolver: typeof maybeRunSemanticAecWorkflow): Promise<ChatResponse | null> { return maybeRunTopLevelSemanticAecWorkflow(req, resolver); }
export async function __testOnlyMaybeRunSemanticAecWorkflow(req: ChatRequest, interpreter: AecTaskIntentInterpreter): Promise<ChatResponse | null> { return maybeRunSemanticAecWorkflow(req, interpreter); }

export function __testOnlyFinalizeDecision(req: ChatRequest, decision: ChatResponse): ChatResponse {
return finalizeDecision(req, decision);
}

export async function decide(req: ChatRequest): Promise<ChatResponse> {
export type BrainDecisionDependencies = {
mepRouteRedline?: typeof maybeRunDeterministicMepRouteRedline;
semanticAecWorkflow?: typeof maybeRunSemanticAecWorkflow;
openAiBrain?: typeof decideOpenAi;
openAiStreamingBrain?: typeof decideOpenAiStreaming;
};

export async function decide(req: ChatRequest, dependencies: BrainDecisionDependencies = {}): Promise<ChatResponse> {
const roomReceptacleDecision = maybeRunDeterministicRoomReceptacleAnalog(req);
if (roomReceptacleDecision) {
return finalizeDecision(req, roomReceptacleDecision);
Expand All @@ -119,12 +131,12 @@ export async function decide(req: ChatRequest): Promise<ChatResponse> {
return finalizeDecision(req, enlargedPlanDecision);
}

const mepRouteRedlineDecision = await maybeRunTopLevelMepRouteRedline(req);
const mepRouteRedlineDecision = await maybeRunTopLevelMepRouteRedline(req, dependencies.mepRouteRedline);
if (mepRouteRedlineDecision) {
return finalizeDecision(req, mepRouteRedlineDecision);
}

const semanticAecDecision = await maybeRunSemanticAecWorkflow(req);
const semanticAecDecision = await maybeRunTopLevelSemanticAecWorkflow(req, dependencies.semanticAecWorkflow);
if (semanticAecDecision) {
return finalizeDecision(req, semanticAecDecision);
}
Expand All @@ -134,15 +146,15 @@ export async function decide(req: ChatRequest): Promise<ChatResponse> {

let decision: ChatResponse;
if (forced === "rule") decision = await decideRule(req);
else if (forced === "openai") decision = await decideOpenAi(req);
else if (forced === "openai") decision = await (dependencies.openAiBrain ?? decideOpenAi)(req);
else if (forced === "codex") decision = await decideCodex(req);
else if (hasOpenAiKey) decision = await decideOpenAi(req);
else decision = await decideRule(req);

return finalizeDecision(req, decision);
}

export async function decideStreaming(req: ChatRequest, cb: StreamCallbacks): Promise<ChatResponse> {
export async function decideStreaming(req: ChatRequest, cb: StreamCallbacks, dependencies: BrainDecisionDependencies = {}): Promise<ChatResponse> {
const roomReceptacleDecision = maybeRunDeterministicRoomReceptacleAnalog(req);
if (roomReceptacleDecision) {
const text = roomReceptacleDecision.assistant_message || "";
Expand Down Expand Up @@ -197,7 +209,7 @@ export async function decideStreaming(req: ChatRequest, cb: StreamCallbacks): Pr
return finalizeDecision(req, enlargedPlanDecision);
}

const mepRouteRedlineDecision = await maybeRunTopLevelMepRouteRedline(req);
const mepRouteRedlineDecision = await maybeRunTopLevelMepRouteRedline(req, dependencies.mepRouteRedline);
if (mepRouteRedlineDecision) {
const text = mepRouteRedlineDecision.assistant_message || "";
const chunkSize = 60;
Expand All @@ -210,7 +222,7 @@ export async function decideStreaming(req: ChatRequest, cb: StreamCallbacks): Pr
return finalizeDecision(req, mepRouteRedlineDecision);
}

const semanticAecDecision = await maybeRunSemanticAecWorkflow(req);
const semanticAecDecision = await maybeRunTopLevelSemanticAecWorkflow(req, dependencies.semanticAecWorkflow);
if (semanticAecDecision) {
const text = semanticAecDecision.assistant_message || "";
cb.onDelta?.(text);
Expand All @@ -222,10 +234,10 @@ export async function decideStreaming(req: ChatRequest, cb: StreamCallbacks): Pr
const hasOpenAiKey = !!resolveOpenAiApiKey();
if (forced === "codex") return decideCodexStreaming(req, cb);
if (forced === "openai" || (forced !== "rule" && hasOpenAiKey)) {
return finalizeDecision(req, await decideOpenAiStreaming(req, cb));
return finalizeDecision(req, await (dependencies.openAiStreamingBrain ?? decideOpenAiStreaming)(req, cb));
}

const decision = await decide(req);
const decision = await decide(req, dependencies);

const text = decision.assistant_message || "";
const chunkSize = 60;
Expand Down
18 changes: 16 additions & 2 deletions apps/operator-backend/test/redline_live_gap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { __testOnlyBuildInitialRedlinePreflightAction, __testOnlyIsFastElectricalPlacementRedline, __testOnlyRunInitialRedlineDecisionLane, __testOnlyShouldPrioritizeInitialRedlinePreflight } from "../src/brains/openai_brain.js";
import { __testOnlyMaybeRunTopLevelMepRouteRedline } from "../src/brain.js";
import { __testOnlyMaybeRunTopLevelMepRouteRedline, decide, decideStreaming } from "../src/brain.js";
import { adaptRedlineAnalyzeToAecIntentEvidence } from "../src/redline/redline_analyze_evidence.js";
import { AEC_INTENT_EVIDENCE_MAX_STRING_CHARS } from "../src/aec_intent_evidence.js";
import { executeWorkbenchActions } from "../src/workbench/workbench_runner.js";
Expand Down Expand Up @@ -58,7 +58,21 @@ test("exact failed-live request uses the production read-only decision lane once
test("authoritative step 1968 safety list keeps both dispatches on one analysis-only lane", async () => {
const req = { version: "operator.backend.v1", session_id: "ff088c6a-359c-4d15-97ff-7894e1d3415c", message_id: "f053cd9f-2b13-4389-b455-434f65f0f000", user_text: step1968Prompt, user_attachments: step1968Attachment } as any;
const action = __testOnlyBuildInitialRedlinePreflightAction({ userText: step1968Prompt, userAttachments: step1968Attachment }); assert.equal(action?.type, "analyze_redline"); if (action?.type === "analyze_redline") assert.equal(action.file_path, step1968Attachment[0].relative_path);
for (const dispatch of ["non-streaming", "streaming"]) { let resolverCalls = 0; const response = await __testOnlyMaybeRunTopLevelMepRouteRedline(req, async () => { resolverCalls += 1; return { version: "operator.backend.v1", assistant_message: "resolver", actions: [] }; }); assert.equal(response, null, dispatch); assert.equal(resolverCalls, 0, dispatch); }
const previousBrain = process.env.OPERATOR_BRAIN; process.env.OPERATOR_BRAIN = "openai";
try {
let mepResolverCalls = 0, semanticResolverCalls = 0, nonStreamingAnalysisCalls = 0, streamingAnalysisCalls = 0;
const dependencies = {
mepRouteRedline: async () => { mepResolverCalls += 1; return { version: "operator.backend.v1", assistant_message: "mep resolver", actions: [] }; },
semanticAecWorkflow: async () => { semanticResolverCalls += 1; return { version: "operator.backend.v1", assistant_message: "semantic resolver", actions: [] }; },
openAiBrain: async () => { nonStreamingAnalysisCalls += 1; return { version: "operator.backend.v1", assistant_message: "non-streaming analysis lane", actions: [] }; },
openAiStreamingBrain: async () => { streamingAnalysisCalls += 1; return { version: "operator.backend.v1", assistant_message: "streaming analysis lane", actions: [] }; }
} as any;
const nonStreaming = await decide(req, dependencies);
const streaming = await decideStreaming(req, {}, dependencies);
assert.equal(mepResolverCalls, 0); assert.equal(semanticResolverCalls, 0);
assert.equal(nonStreamingAnalysisCalls, 1); assert.equal(streamingAnalysisCalls, 1);
assert.equal(nonStreaming.assistant_message, "non-streaming analysis lane"); assert.equal(streaming.assistant_message, "streaming analysis lane");
} finally { if (previousBrain === undefined) delete process.env.OPERATOR_BRAIN; else process.env.OPERATOR_BRAIN = previousBrain; }
const order: string[] = []; const lane = await __testOnlyRunInitialRedlineDecisionLane({ userText: step1968Prompt, userAttachments: step1968Attachment, runInitialPreflight: async (entry) => { order.push(entry.type); }, runFastPreflight: async () => { order.push("fast"); return null; }, summarize: async () => { order.push("summary"); return { assistant_message: "Evidence summary", actions: [{ action_id: "unexpected", method: "POST", path: "/revit/delete", body_json: "{}" }] } as any; } });
assert.deepEqual(order, ["analyze_redline", "summary"]); assert.equal(lane.fastPreflight, null); assert.deepEqual(lane.response, { version: "operator.backend.v1", assistant_message: "Evidence summary", actions: [] });
});
Expand Down
Loading