fix(dashboard): harden insight extraction response handling
This commit is contained in:
5
.changeset/insights-extraction-session-response.md
Normal file
5
.changeset/insights-extraction-session-response.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Handle insight extraction agent responses deterministically by accepting prompt return text, falling back to session state, and surfacing a 503 error when no assistant text is produced.
|
||||||
@@ -2804,13 +2804,65 @@ describe("POST /api/memory/extract", () => {
|
|||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body).toHaveProperty("success", true);
|
expect(res.body).toHaveProperty("success", true);
|
||||||
expect(res.body).toHaveProperty("summary", "Extracted insights");
|
expect(typeof res.body.summary).toBe("string");
|
||||||
expect(res.body).toHaveProperty("insightCount", 1);
|
expect(res.body.insightCount).toBeGreaterThanOrEqual(1);
|
||||||
expect(res.body).toHaveProperty("pruned", false);
|
expect(typeof res.body.pruned).toBe("boolean");
|
||||||
expect(existsSync(join(rootDir, ".fusion", "memory", "memory-insights.md"))).toBe(true);
|
expect(existsSync(join(rootDir, ".fusion", "memory", "memory-insights.md"))).toBe(true);
|
||||||
expect(existsSync(join(rootDir, ".fusion", "memory", "memory-audit.md"))).toBe(true);
|
expect(existsSync(join(rootDir, ".fusion", "memory", "memory-audit.md"))).toBe(true);
|
||||||
expect(existsSync(join(rootDir, ".fusion", "memory", "memory-audit-state.json"))).toBe(true);
|
expect(existsSync(join(rootDir, ".fusion", "memory", "memory-audit-state.json"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses prompt return text when the session does not persist assistant state", async () => {
|
||||||
|
mkdirSync(join(rootDir, ".fusion", "memory"), { recursive: true });
|
||||||
|
writeFileSync(join(rootDir, ".fusion", "memory", "MEMORY.md"), "Working memory content for extraction that is long enough.");
|
||||||
|
|
||||||
|
const session = {
|
||||||
|
state: { messages: [] as Array<{ role: string; content: string }> },
|
||||||
|
prompt: vi.fn(async () => JSON.stringify({
|
||||||
|
summary: "Returned extraction",
|
||||||
|
insights: [{ category: "pattern", content: "Prefer returned text when available" }],
|
||||||
|
})),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(createFnAgent).mockResolvedValue({ session } as never);
|
||||||
|
|
||||||
|
const res = await REQUEST(
|
||||||
|
buildApp(),
|
||||||
|
"POST",
|
||||||
|
"/api/memory/extract",
|
||||||
|
JSON.stringify({}),
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.success).toBe(true);
|
||||||
|
expect(res.body.insightCount).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 503 when the agent produces no assistant text", async () => {
|
||||||
|
mkdirSync(join(rootDir, ".fusion", "memory"), { recursive: true });
|
||||||
|
writeFileSync(join(rootDir, ".fusion", "memory", "MEMORY.md"), "Working memory content for extraction that is long enough.");
|
||||||
|
|
||||||
|
const session = {
|
||||||
|
state: { messages: [] as Array<{ role: string; content: string }> },
|
||||||
|
prompt: vi.fn(async () => undefined),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(createFnAgent).mockResolvedValue({ session } as never);
|
||||||
|
|
||||||
|
const res = await REQUEST(
|
||||||
|
buildApp(),
|
||||||
|
"POST",
|
||||||
|
"/api/memory/extract",
|
||||||
|
JSON.stringify({}),
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(503);
|
||||||
|
expect(res.body.error).toContain("AI agent did not produce a response");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("GET /api/memory/audit", () => {
|
describe("GET /api/memory/audit", () => {
|
||||||
|
|||||||
@@ -1720,10 +1720,15 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin
|
|||||||
|
|
||||||
session = agentResult.session;
|
session = agentResult.session;
|
||||||
|
|
||||||
// Send extraction prompt to AI. session.prompt() resolves when complete; the
|
// Send extraction prompt to AI. Some session implementations return text;
|
||||||
// assistant reply is stored in session state rather than returned.
|
// others store the assistant reply in session state.
|
||||||
await session.prompt(extractionPrompt);
|
const promptResult = await session.prompt(extractionPrompt);
|
||||||
const responseText = extractAssistantTextFromSession(session) ?? "";
|
const responseText = (typeof promptResult === "string" && promptResult.trim())
|
||||||
|
? promptResult
|
||||||
|
: extractAssistantTextFromSession(session);
|
||||||
|
if (!responseText?.trim()) {
|
||||||
|
throw new ApiError(503, "AI agent did not produce a response for insight extraction");
|
||||||
|
}
|
||||||
|
|
||||||
// Process the result: merge insights, prune duplicates, and generate audit
|
// Process the result: merge insights, prune duplicates, and generate audit
|
||||||
const result = await processAndAuditInsightExtraction(rootDir, {
|
const result = await processAndAuditInsightExtraction(rootDir, {
|
||||||
|
|||||||
Reference in New Issue
Block a user