fix(chat): tolerate 500 on empty inbox edges; fix(agent): clear LLM-unavailable msg
- chatApi.listThreads: catch on the edges query so a fresh empty inbox doesn't bubble a 500 to the UI. New users see 'No conversations yet' instead of the error banner. - agentTools.llmFallback: when the LLM gateway returns 503/500, show a clear 'plain-language replies temporarily unavailable' message naming the reason, listing the deterministic capabilities that still work. No more silent null returns.
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
// End-to-end Studio flow against LIVE.
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
|
||||||
|
const URL = "https://canvas.flow-master.ai/";
|
||||||
|
const calls = [];
|
||||||
|
const consoleErrs = [];
|
||||||
|
|
||||||
|
const b = await chromium.launch({ headless: true });
|
||||||
|
const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } });
|
||||||
|
const p = await ctx.newPage();
|
||||||
|
p.on("response", (r) => {
|
||||||
|
const u = r.url();
|
||||||
|
if (u.includes("/api/")) calls.push({ status: r.status(), method: r.request().method(), url: u.replace("https://canvas.flow-master.ai", "") });
|
||||||
|
});
|
||||||
|
p.on("console", (m) => { if (m.type() === "error") consoleErrs.push(m.text().slice(0, 200)); });
|
||||||
|
await ctx.addInitScript(() => localStorage.setItem("fm.canvas.tour.seen", String(Date.now())));
|
||||||
|
|
||||||
|
const log = (s) => console.log(`[${(Date.now() - t0) / 1000}s] ${s}`);
|
||||||
|
const t0 = Date.now();
|
||||||
|
|
||||||
|
log("login");
|
||||||
|
await p.goto(URL, { waitUntil: "domcontentloaded" });
|
||||||
|
await p.waitForTimeout(2500);
|
||||||
|
await p.locator(".persona-chip").filter({ hasText: /Mariana/ }).click();
|
||||||
|
await p.waitForSelector(".topbar", { timeout: 30000 });
|
||||||
|
log("logged in, on " + (await p.evaluate(() => document.querySelector("[class*='shell-']")?.className)));
|
||||||
|
|
||||||
|
log("nav to studio");
|
||||||
|
const studioTab = p.locator(".tab").filter({ hasText: /Studio/i }).first();
|
||||||
|
await studioTab.click({ timeout: 10000 });
|
||||||
|
await p.waitForTimeout(2500);
|
||||||
|
log("studio scene: " + (await p.evaluate(() => document.querySelector(".scene")?.innerText?.slice(0, 150))));
|
||||||
|
|
||||||
|
log("fill intake");
|
||||||
|
const desc = p.locator("textarea").first();
|
||||||
|
await desc.fill("E2E test: when a store manager requests a laptop, run procurement approval.");
|
||||||
|
await p.waitForTimeout(500);
|
||||||
|
|
||||||
|
log("click Start drafting");
|
||||||
|
await p.locator("button.btn-primary").filter({ hasText: /draft/i }).first().click({ timeout: 8000 });
|
||||||
|
await p.waitForTimeout(7000);
|
||||||
|
|
||||||
|
log("step now: " + (await p.evaluate(() => {
|
||||||
|
const t = document.querySelector(".scene")?.innerText || "";
|
||||||
|
return t.split("\n").slice(0, 3).join(" | ");
|
||||||
|
})));
|
||||||
|
|
||||||
|
log("click Confirm structure");
|
||||||
|
const confirmBtn = p.locator("button.btn-primary").filter({ hasText: /Confirm structure/i }).first();
|
||||||
|
if (await confirmBtn.count()) {
|
||||||
|
await confirmBtn.click({ timeout: 8000 });
|
||||||
|
log("waiting for toast or step change…");
|
||||||
|
await p.waitForTimeout(10000);
|
||||||
|
const toasts = await p.locator(".toaster, .toast, .toast-msg").allTextContents();
|
||||||
|
log("toasts: " + JSON.stringify(toasts.slice(-3)));
|
||||||
|
const step = await p.evaluate(() => {
|
||||||
|
const t = document.querySelector(".scene")?.innerText || "";
|
||||||
|
return t.split("\n").slice(0, 3).join(" | ");
|
||||||
|
});
|
||||||
|
log("now: " + step);
|
||||||
|
}
|
||||||
|
|
||||||
|
await p.screenshot({ path: "/tmp/canvas-evidence/studio-e2e.png" });
|
||||||
|
|
||||||
|
await b.close();
|
||||||
|
console.log("\n=== Network ===");
|
||||||
|
const byStatus = {};
|
||||||
|
for (const c of calls) byStatus[c.status] = (byStatus[c.status] || 0) + 1;
|
||||||
|
console.log("by status:", JSON.stringify(byStatus));
|
||||||
|
const fails = calls.filter(c => c.status >= 400);
|
||||||
|
console.log("\nFails:");
|
||||||
|
for (const f of fails) console.log(` ${f.status} ${f.method} ${f.url.slice(0, 90)}`);
|
||||||
|
const applyBatchCalls = calls.filter(c => c.url.includes("apply-batch"));
|
||||||
|
console.log("\napply-batch calls:");
|
||||||
|
for (const c of applyBatchCalls) console.log(` ${c.status} ${c.method} ${c.url}`);
|
||||||
|
console.log("\nConsole errors:");
|
||||||
|
for (const e of consoleErrs.slice(-10)) console.log(" " + e);
|
||||||
+12
-1
@@ -194,7 +194,18 @@ async function llmFallback(trimmed: string, ctx: ToolContext, hints: Memory[]):
|
|||||||
const user: LlmMessage = { role: "user", content: trimmed };
|
const user: LlmMessage = { role: "user", content: trimmed };
|
||||||
const reply = await llmClient.chat({ messages: [system, user], max_tokens: 320 });
|
const reply = await llmClient.chat({ messages: [system, user], max_tokens: 320 });
|
||||||
if (reply.ok) return { ok: true, display: reply.content };
|
if (reply.ok) return { ok: true, display: reply.content };
|
||||||
return null;
|
if (!reply.configured) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
display:
|
||||||
|
`Plain-language replies are temporarily unavailable (${reply.reason}). ` +
|
||||||
|
`I can still: navigate, list processes, start a process, message a teammate, remember / recall notes.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
display: `LLM provider error: ${reply.error}. I can still navigate, list/start processes, message a teammate, or remember notes.`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function routeAgentInput(text: string, ctx: ToolContext): Promise<ToolResult> {
|
export async function routeAgentInput(text: string, ctx: ToolContext): Promise<ToolResult> {
|
||||||
|
|||||||
+7
-1
@@ -111,7 +111,13 @@ export const chatApi = {
|
|||||||
if (!inbox) {
|
if (!inbox) {
|
||||||
throw new Error("Chat inbox setup failed (EA2 backend not reachable). Try refreshing.");
|
throw new Error("Chat inbox setup failed (EA2 backend not reachable). Try refreshing.");
|
||||||
}
|
}
|
||||||
const edges = await jsonFetch(`/api/ea2/edges/defines?from=flow/${inboxKey}&limit=200`, { signal });
|
let edges: { items?: unknown[] } = { items: [] };
|
||||||
|
try {
|
||||||
|
edges = await jsonFetch(`/api/ea2/edges/defines?from=flow/${inboxKey}&limit=200`, { signal });
|
||||||
|
} catch {
|
||||||
|
// No edges yet for a fresh inbox; treat as empty thread list.
|
||||||
|
edges = { items: [] };
|
||||||
|
}
|
||||||
const threadKeys = ((edges?.items || []) as any[])
|
const threadKeys = ((edges?.items || []) as any[])
|
||||||
.filter((e) => e?.role === "presentation")
|
.filter((e) => e?.role === "presentation")
|
||||||
.map((e) => (e._to || "").split("/").pop())
|
.map((e) => (e._to || "").split("/").pop())
|
||||||
|
|||||||
Reference in New Issue
Block a user