fix(ai-builder): Read eval pin data when the model explains it after the JSON (no-changelog) (#40682)
Some checks are pending
CI: Master (Build, Test, Lint) / Build & format (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.18.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (26.5.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Verify single-instance deps (npm-install) (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
José Braulio González Valido 2026-10-08 21:57:15 +00:00 committed by GitHub
parent 5b78e8b655
commit 6fc0aeda35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 34 additions and 2 deletions

View File

@ -416,6 +416,24 @@ describe('generateEvalPinData', () => {
expect(result.Slack).toHaveLength(1);
});
it('should read fenced JSON that the model explains afterwards', async () => {
const workflow: SimpleWorkflow = {
name: 'Test',
nodes: [makeNode({ name: 'Slack', type: 'n8n-nodes-base.slack' })],
connections: {},
};
const llmResponse =
'```json\n{"Slack": [{"json": {"ok": true}}]}\n```\n\n**Slack:** the post succeeds, so one item.';
const result = await generateEvalPinData(workflow, {
llm: createMockLLM(llmResponse),
nodeTypes,
});
expect(result.Slack).toHaveLength(1);
});
it('should wrap raw objects in { json: ... } format', async () => {
const workflow: SimpleWorkflow = {
name: 'Test',

View File

@ -14,6 +14,7 @@
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
import { extractJsonCandidate } from '@n8n/ai-utilities/llm-output';
import type { NodeSchemaContext, OutputSchemaLookup, WorkflowJSON } from '@n8n/workflow-sdk';
import {
buildDateAnchors,
@ -195,7 +196,8 @@ export async function generateEvalPinData(
const responseText =
typeof response.content === 'string' ? response.content : JSON.stringify(response.content);
const pinData = parsePinDataResponse(responseText, expectedNodeNames);
// The model sometimes explains its answer after the JSON block.
const pinData = parsePinDataResponse(extractJsonCandidate(responseText), expectedNodeNames);
// Envelope repair for parser-target roots; the shared helper derives the
// envelope key from each root's with-parser `__schema__` variant

View File

@ -61,6 +61,16 @@ describe('generatePinData', () => {
expect(result).toEqual({ 'Get Posted Keys': [] });
});
it('reads the fenced JSON when the model explains it afterwards', async () => {
respondWith(
'```json\n{ "Get Posted Keys": [] }\n```\n\n**Get Posted Keys (`[]`):** every key is already stored, so the read returns no rows.',
);
const result = await generatePinData({ workflow, nodeNames: ['Get Posted Keys'] });
expect(result).toEqual({ 'Get Posted Keys': [] });
});
it('wraps raw items and passes json-wrapped items through', async () => {
respondWith(
JSON.stringify({

View File

@ -11,6 +11,7 @@
* LLM call (`createEvalAgent`) and the `__schema__` lookup wiring.
*/
import { extractJsonCandidate } from '@n8n/ai-utilities/llm-output';
import { createEvalAgent, extractText } from '@n8n/instance-ai';
import type {
WorkflowJSON,
@ -109,7 +110,8 @@ export async function generatePinData(options: GeneratePinDataOptions): Promise<
abortSignal: AbortSignal.timeout(PIN_DATA_LLM_TIMEOUT_MS),
});
const responseText = extractText(result);
// The model sometimes explains its answer after the JSON block.
const responseText = extractJsonCandidate(extractText(result));
const pinData = parsePinDataResponse(responseText, expectedNodeNames);
const missing = expectedNodeNames.filter((name) => !(name in pinData));