mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-01 15:47:41 +08:00
feat(ai-builder): Support dataset context and conversation history in evaluations (no-changelog) (#27618)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -176,6 +176,7 @@ describe('Runner - LangSmith Mode', () => {
|
||||
// Collectors are passed explicitly from the traceable wrapper to capture token usage and subgraph metrics
|
||||
expect(generateWorkflow).toHaveBeenCalledWith(
|
||||
'Create a workflow',
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
tokenUsage: expect.any(Function),
|
||||
subgraphMetrics: expect.any(Function),
|
||||
|
||||
@@ -9,6 +9,8 @@ import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import pLimit from 'p-limit';
|
||||
|
||||
import { CodeWorkflowBuilder } from '@/code-builder';
|
||||
import type { HistoryContext } from '@/code-builder';
|
||||
import type { ConversationEntry } from '@/code-builder/utils/code-builder-session';
|
||||
import type { CoordinationLogEntry } from '@/types/coordination';
|
||||
import type { StreamChunk, WorkflowUpdateChunk } from '@/types/streaming';
|
||||
import type { SimpleWorkflow } from '@/types/workflow';
|
||||
@@ -46,6 +48,7 @@ import {
|
||||
extractSubgraphMetrics,
|
||||
getChatPayload,
|
||||
} from '../harness/evaluation-helpers';
|
||||
import type { DatasetInputContext } from '../harness/harness-types';
|
||||
import { createLogger } from '../harness/logger';
|
||||
import type { GenerationCollectors, SubgraphMetricsCollector } from '../harness/runner';
|
||||
import { TokenUsageTrackingHandler } from '../harness/token-tracking-handler';
|
||||
@@ -132,8 +135,16 @@ function createWorkflowGenerator(
|
||||
parsedNodeTypes: INodeTypeDescription[],
|
||||
llms: ResolvedStageLLMs,
|
||||
featureFlags?: BuilderFeatureFlags,
|
||||
): (prompt: string, collectors?: GenerationCollectors) => Promise<SimpleWorkflow> {
|
||||
return async (prompt: string, collectors?: GenerationCollectors): Promise<SimpleWorkflow> => {
|
||||
): (
|
||||
prompt: string,
|
||||
datasetInputContext?: DatasetInputContext,
|
||||
collectors?: GenerationCollectors,
|
||||
) => Promise<SimpleWorkflow> {
|
||||
return async (
|
||||
prompt: string,
|
||||
datasetInputContext?: DatasetInputContext,
|
||||
collectors?: GenerationCollectors,
|
||||
): Promise<SimpleWorkflow> => {
|
||||
const runId = generateRunId();
|
||||
|
||||
const agent = createAgent({
|
||||
@@ -153,10 +164,13 @@ function createWorkflowGenerator(
|
||||
message: prompt,
|
||||
workflowId: runId,
|
||||
featureFlags,
|
||||
workflowContext: datasetInputContext?.workflowContext,
|
||||
mode: datasetInputContext?.mode,
|
||||
}),
|
||||
EVAL_USERS.LANGSMITH,
|
||||
undefined, // abortSignal
|
||||
tokenTracker ? [tokenTracker] : undefined, // externalCallbacks
|
||||
datasetInputContext?.historicalMessages,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -230,6 +244,58 @@ function createEvaluators(params: {
|
||||
return evaluators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert raw LangChain messages from dataset into HistoryContext for the code builder.
|
||||
* Pairs up human/AI messages as conversation entries.
|
||||
*/
|
||||
function buildHistoryContextFromMessages(messages: unknown[]): HistoryContext | undefined {
|
||||
if (messages.length === 0) return undefined;
|
||||
|
||||
const entries: ConversationEntry[] = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i];
|
||||
if (!isUnknownRecord(msg)) continue;
|
||||
|
||||
const msgId = msg.id;
|
||||
const isHuman = Array.isArray(msgId) && msgId.includes('HumanMessage');
|
||||
|
||||
if (!isHuman) continue;
|
||||
|
||||
const kwargs = msg.kwargs;
|
||||
if (!isUnknownRecord(kwargs) || typeof kwargs.content !== 'string') continue;
|
||||
|
||||
const userContent = kwargs.content;
|
||||
|
||||
// Look for a following AI message to pair with
|
||||
const nextMsg = i + 1 < messages.length ? messages[i + 1] : undefined;
|
||||
const nextIsAI =
|
||||
isUnknownRecord(nextMsg) && Array.isArray(nextMsg.id) && nextMsg.id.includes('AIMessage');
|
||||
|
||||
if (nextIsAI && isUnknownRecord(nextMsg)) {
|
||||
const nextKwargs = nextMsg.kwargs;
|
||||
const aiContent =
|
||||
isUnknownRecord(nextKwargs) && typeof nextKwargs.content === 'string'
|
||||
? nextKwargs.content
|
||||
: '';
|
||||
entries.push({
|
||||
type: 'assistant-exchange',
|
||||
userQuery: userContent,
|
||||
assistantSummary: aiContent,
|
||||
});
|
||||
i++; // Skip the AI message
|
||||
} else {
|
||||
entries.push({ type: 'build-request', message: userContent });
|
||||
}
|
||||
}
|
||||
|
||||
return entries.length > 0 ? { conversationEntries: entries } : undefined;
|
||||
}
|
||||
|
||||
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CodeWorkflowBuilder generator function.
|
||||
* Uses the CodeWorkflowBuilder which coordinates planning and coding agents to generate
|
||||
@@ -245,9 +311,17 @@ function createCodeWorkflowBuilderGenerator(
|
||||
llms: ResolvedStageLLMs,
|
||||
timeoutMs?: number,
|
||||
nodeDefinitionDirs?: string[],
|
||||
): (prompt: string, collectors?: GenerationCollectors) => Promise<GenerationResult> {
|
||||
): (
|
||||
prompt: string,
|
||||
datasetInputContext?: DatasetInputContext,
|
||||
collectors?: GenerationCollectors,
|
||||
) => Promise<GenerationResult> {
|
||||
// Subgraph metrics are not applicable since CodeWorkflowBuilder doesn't use coordination logs.
|
||||
return async (prompt: string, collectors?: GenerationCollectors): Promise<GenerationResult> => {
|
||||
return async (
|
||||
prompt: string,
|
||||
datasetInputContext?: DatasetInputContext,
|
||||
collectors?: GenerationCollectors,
|
||||
): Promise<GenerationResult> => {
|
||||
const runId = generateRunId();
|
||||
|
||||
// Accumulate token usage across all LLM calls
|
||||
@@ -271,8 +345,15 @@ function createCodeWorkflowBuilderGenerator(
|
||||
message: prompt,
|
||||
workflowId: runId,
|
||||
featureFlags: { codeBuilder: true },
|
||||
workflowContext: datasetInputContext?.workflowContext,
|
||||
mode: datasetInputContext?.mode,
|
||||
});
|
||||
|
||||
// Build history context from dataset messages if available
|
||||
const historyContext = datasetInputContext?.historicalMessages
|
||||
? buildHistoryContextFromMessages(datasetInputContext.historicalMessages)
|
||||
: undefined;
|
||||
|
||||
let workflow: SimpleWorkflow | null = null;
|
||||
let generatedCode: string | undefined;
|
||||
|
||||
@@ -293,6 +374,7 @@ function createCodeWorkflowBuilderGenerator(
|
||||
payload,
|
||||
EVAL_USERS.LANGSMITH,
|
||||
abortController.signal,
|
||||
historyContext,
|
||||
)) {
|
||||
for (const message of output.messages) {
|
||||
if (isWorkflowUpdateChunk(message)) {
|
||||
@@ -484,6 +566,7 @@ export async function runV2Evaluation(): Promise<void> {
|
||||
const logger = createLogger(args.verbose);
|
||||
const lifecycle = createConsoleLifecycle({ verbose: args.verbose, logger });
|
||||
const stageModels = argsToStageModels(args);
|
||||
|
||||
const env = await setupTestEnvironment(stageModels, logger);
|
||||
|
||||
// Validate LangSmith client early if langsmith backend is requested
|
||||
|
||||
@@ -67,18 +67,35 @@ export interface GetChatPayloadOptions {
|
||||
message: string;
|
||||
workflowId: string;
|
||||
featureFlags?: BuilderFeatureFlags;
|
||||
/** Full workflowContext from dataset (overrides default empty context) */
|
||||
workflowContext?: ChatPayload['workflowContext'];
|
||||
/** Builder mode from dataset */
|
||||
mode?: 'build' | 'plan';
|
||||
}
|
||||
|
||||
export function getChatPayload(options: GetChatPayloadOptions): ChatPayload {
|
||||
const { evalType, message, workflowId, featureFlags } = options;
|
||||
const { evalType, message, workflowId, featureFlags, workflowContext, mode } = options;
|
||||
|
||||
// Always use the eval runId as currentWorkflow.id so getState() can find the thread.
|
||||
// When workflowContext is provided from a dataset, override its currentWorkflow.id.
|
||||
const resolvedContext = workflowContext
|
||||
? {
|
||||
...workflowContext,
|
||||
currentWorkflow: {
|
||||
nodes: [],
|
||||
connections: {},
|
||||
...((workflowContext.currentWorkflow as Record<string, unknown>) ?? {}),
|
||||
id: workflowId,
|
||||
},
|
||||
}
|
||||
: { currentWorkflow: { id: workflowId, nodes: [], connections: {} } };
|
||||
|
||||
return {
|
||||
id: `${evalType}-${uuid()}`,
|
||||
featureFlags: featureFlags ?? DEFAULTS.FEATURE_FLAGS,
|
||||
message,
|
||||
workflowContext: {
|
||||
currentWorkflow: { id: workflowId, nodes: [], connections: {} },
|
||||
},
|
||||
workflowContext: resolvedContext,
|
||||
...(mode ? { mode } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import type { Client as LangsmithClient } from 'langsmith/client';
|
||||
import type { IPinData } from 'n8n-workflow';
|
||||
import type pLimit from 'p-limit';
|
||||
@@ -6,9 +7,27 @@ import type { EvalLogger } from './logger';
|
||||
import type { GenerationCollectors } from './runner';
|
||||
import type { IntrospectionEvent } from '../../src/tools/introspect.tool.js';
|
||||
import type { SimpleWorkflow } from '../../src/types/workflow';
|
||||
import type { ChatPayload } from '../../src/workflow-builder-agent';
|
||||
|
||||
export type LlmCallLimiter = ReturnType<typeof pLimit>;
|
||||
|
||||
/**
|
||||
* Full dataset input context from a LangSmith example.
|
||||
* Contains all the context fields the agent needs for realistic generation.
|
||||
*/
|
||||
export interface DatasetInputContext {
|
||||
/** The complete workflowContext from the dataset (executionSchema, executionData, etc.) */
|
||||
workflowContext?: ChatPayload['workflowContext'];
|
||||
/** The existing workflow JSON before this turn */
|
||||
existingWorkflow?: SimpleWorkflow;
|
||||
/** Historical messages from prior conversation turns (deserialized BaseMessage instances) */
|
||||
historicalMessages?: BaseMessage[];
|
||||
/** Builder mode from dataset */
|
||||
mode?: 'build' | 'plan';
|
||||
/** Feature flags from dataset metadata */
|
||||
featureFlags?: ChatPayload['featureFlags'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared context passed to all evaluators.
|
||||
*
|
||||
@@ -43,6 +62,8 @@ export interface EvaluationContext {
|
||||
pinData?: IPinData;
|
||||
/** Per-example annotations (e.g., code_necessary) from CSV or LangSmith dataset */
|
||||
annotations?: Record<string, unknown>;
|
||||
/** Full dataset input context for trace-based evaluation */
|
||||
datasetInputContext?: DatasetInputContext;
|
||||
}
|
||||
|
||||
/** Context attached to an individual test case (prompt is provided separately). */
|
||||
@@ -123,6 +144,7 @@ export interface RunConfigBase {
|
||||
/** Function to generate workflow from prompt. May return GenerationResult with source code. Optional collectors receive metrics. */
|
||||
generateWorkflow: (
|
||||
prompt: string,
|
||||
datasetInputContext?: DatasetInputContext,
|
||||
collectors?: GenerationCollectors,
|
||||
) => Promise<SimpleWorkflow | GenerationResult>;
|
||||
/** Evaluators to run on each generated workflow */
|
||||
@@ -248,7 +270,7 @@ export interface SubgraphExampleOutput {
|
||||
response?: string;
|
||||
/** The workflow produced by the subgraph (for builder/configurator) */
|
||||
workflow?: SimpleWorkflow;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from workflow generation that may include source code.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import { AIMessage, HumanMessage, type BaseMessage } from '@langchain/core/messages';
|
||||
import { evaluate } from 'langsmith/evaluation';
|
||||
import type { Run, Example } from 'langsmith/schemas';
|
||||
import { traceable } from 'langsmith/traceable';
|
||||
@@ -9,6 +9,7 @@ import { runWithOptionalLimiter, withTimeout } from './evaluation-helpers';
|
||||
import { toLangsmithEvaluationResult } from './feedback';
|
||||
import {
|
||||
isGenerationResult,
|
||||
type DatasetInputContext,
|
||||
type Evaluator,
|
||||
type TestCase,
|
||||
type EvaluationContext,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
} from './score-calculator';
|
||||
import type { IntrospectionEvent } from '../../src/tools/introspect.tool.js';
|
||||
import type { SimpleWorkflow } from '../../src/types/workflow';
|
||||
import type { ChatPayload } from '../../src/workflow-builder-agent';
|
||||
import { extractMessageContent } from '../langsmith/types';
|
||||
|
||||
const DEFAULT_PASS_THRESHOLD = 0.7;
|
||||
@@ -201,9 +203,17 @@ function buildContext(args: {
|
||||
referenceWorkflows?: SimpleWorkflow[];
|
||||
generatedCode?: string;
|
||||
pinData?: IPinData;
|
||||
datasetInputContext?: DatasetInputContext;
|
||||
}): EvaluationContext {
|
||||
const { prompt, globalContext, testCaseContext, referenceWorkflows, generatedCode, pinData } =
|
||||
args;
|
||||
const {
|
||||
prompt,
|
||||
globalContext,
|
||||
testCaseContext,
|
||||
referenceWorkflows,
|
||||
generatedCode,
|
||||
pinData,
|
||||
datasetInputContext,
|
||||
} = args;
|
||||
|
||||
return {
|
||||
prompt,
|
||||
@@ -212,6 +222,7 @@ function buildContext(args: {
|
||||
...(referenceWorkflows?.length ? { referenceWorkflows } : {}),
|
||||
...(generatedCode ? { generatedCode } : {}),
|
||||
...(pinData ? { pinData } : {}),
|
||||
...(datasetInputContext ? { datasetInputContext } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -543,6 +554,7 @@ async function runLocalExampleSuccess(args: {
|
||||
testCase: TestCase;
|
||||
generateWorkflow: (
|
||||
prompt: string,
|
||||
datasetInputContext?: DatasetInputContext,
|
||||
collectors?: GenerationCollectors,
|
||||
) => Promise<SimpleWorkflow | GenerationResult>;
|
||||
evaluators: Array<Evaluator<EvaluationContext>>;
|
||||
@@ -571,7 +583,7 @@ async function runLocalExampleSuccess(args: {
|
||||
|
||||
const genResult = await runWithOptionalLimiter(async () => {
|
||||
return await withTimeout({
|
||||
promise: generateWorkflow(testCase.prompt, collectors),
|
||||
promise: generateWorkflow(testCase.prompt, testCase.context?.datasetInputContext, collectors),
|
||||
timeoutMs,
|
||||
label: 'workflow_generation',
|
||||
});
|
||||
@@ -604,6 +616,7 @@ async function runLocalExampleSuccess(args: {
|
||||
referenceWorkflows: testCase.referenceWorkflows,
|
||||
generatedCode,
|
||||
pinData,
|
||||
datasetInputContext: testCase.context?.datasetInputContext,
|
||||
});
|
||||
|
||||
// Run evaluators in parallel
|
||||
@@ -641,6 +654,7 @@ async function runLocalExample(args: {
|
||||
testCase: TestCase;
|
||||
generateWorkflow: (
|
||||
prompt: string,
|
||||
datasetInputContext?: DatasetInputContext,
|
||||
collectors?: GenerationCollectors,
|
||||
) => Promise<SimpleWorkflow | GenerationResult>;
|
||||
evaluators: Array<Evaluator<EvaluationContext>>;
|
||||
@@ -720,6 +734,7 @@ async function runLocalDataset(params: {
|
||||
testCases: TestCase[];
|
||||
generateWorkflow: (
|
||||
prompt: string,
|
||||
datasetInputContext?: DatasetInputContext,
|
||||
collectors?: GenerationCollectors,
|
||||
) => Promise<SimpleWorkflow | GenerationResult>;
|
||||
evaluators: Array<Evaluator<EvaluationContext>>;
|
||||
@@ -954,6 +969,12 @@ interface LangsmithDatasetInput {
|
||||
prompt?: string;
|
||||
messages?: BaseMessage[];
|
||||
evals?: Record<string, unknown>;
|
||||
workflowJSON?: unknown;
|
||||
workflowContext?: unknown;
|
||||
workflowOperations?: unknown[];
|
||||
mode?: string;
|
||||
/** Injected by enrichExamplesWithHistory - historical messages from outputs */
|
||||
_historicalMessages?: unknown[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -975,6 +996,116 @@ function extractPrompt(inputs: LangsmithDatasetInput): string {
|
||||
throw new Error('No prompt found in inputs - expected "prompt" string or "messages" array');
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-process LangSmith examples to extract conversation history from outputs
|
||||
* and inject it into inputs for the target function.
|
||||
*
|
||||
* The dataset format has:
|
||||
* - inputs.messages[0]: The latest user turn
|
||||
* - outputs.messages: The FULL conversation (all prior turns + latest + AI response)
|
||||
*
|
||||
* We find the latest turn in outputs and extract everything before it as historical.
|
||||
*/
|
||||
function enrichExamplesWithHistory(examples: Example[]): Example[] {
|
||||
return examples.map((example) => {
|
||||
const outputMessages = (example.outputs as Record<string, unknown> | undefined)?.messages;
|
||||
if (!Array.isArray(outputMessages) || outputMessages.length <= 1) {
|
||||
return example; // No history to extract
|
||||
}
|
||||
|
||||
const inputMessages = (example.inputs as Record<string, unknown> | undefined)?.messages;
|
||||
if (!Array.isArray(inputMessages) || inputMessages.length === 0) {
|
||||
return example;
|
||||
}
|
||||
|
||||
// Get the content of the latest turn from inputs
|
||||
const inputMsg = inputMessages[0] as Record<string, unknown> | undefined;
|
||||
const inputContent = (inputMsg?.kwargs as Record<string, unknown> | undefined)?.content;
|
||||
if (typeof inputContent !== 'string') return example;
|
||||
|
||||
// Find the index of the latest turn in output messages by matching content
|
||||
const latestTurnIndex = outputMessages.findIndex((msg: unknown) => {
|
||||
if (!isUnknownRecord(msg)) return false;
|
||||
const kwargs = msg.kwargs;
|
||||
if (!isUnknownRecord(kwargs)) return false;
|
||||
return kwargs.content === inputContent;
|
||||
});
|
||||
|
||||
if (latestTurnIndex <= 0) return example; // No history before the latest turn
|
||||
|
||||
// Extract all messages before the latest turn as historical
|
||||
const historicalMessages = outputMessages.slice(0, latestTurnIndex);
|
||||
|
||||
return {
|
||||
...example,
|
||||
inputs: {
|
||||
...example.inputs,
|
||||
_historicalMessages: historicalMessages,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize raw LangChain `lc` serialization format messages into BaseMessage instances.
|
||||
* Dataset messages use the format: {id, lc: 1, type: "constructor", kwargs: {content, ...}}
|
||||
* with the class ID in `id` array (e.g. ["langchain_core", "messages", "HumanMessage"]).
|
||||
*
|
||||
* Only keeps HumanMessage and AIMessage — tool calls/results are stripped
|
||||
* to simplify the context passed to the code builder.
|
||||
*/
|
||||
function deserializeLcMessages(rawMessages: unknown[]): BaseMessage[] {
|
||||
return rawMessages
|
||||
.filter((msg): msg is Record<string, unknown> => {
|
||||
if (!isUnknownRecord(msg)) return false;
|
||||
const id = Array.isArray(msg.id) ? (msg.id as unknown[]) : [];
|
||||
const last: unknown = id[id.length - 1];
|
||||
return last === 'HumanMessage' || last === 'AIMessage';
|
||||
})
|
||||
.map((msg) => {
|
||||
const kwargs = isUnknownRecord(msg.kwargs) ? msg.kwargs : {};
|
||||
const content = typeof kwargs.content === 'string' ? kwargs.content : '';
|
||||
const id = Array.isArray(msg.id) ? (msg.id as unknown[]) : [];
|
||||
|
||||
return id[id.length - 1] === 'HumanMessage'
|
||||
? new HumanMessage(content)
|
||||
: new AIMessage(content);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract DatasetInputContext from LangSmith dataset inputs.
|
||||
* Captures the full agent context (workflowContext, existing workflow, mode)
|
||||
* needed to replay the generation realistically.
|
||||
*/
|
||||
function extractDatasetInputContext(
|
||||
inputs: LangsmithDatasetInput,
|
||||
): DatasetInputContext | undefined {
|
||||
const hasContext =
|
||||
inputs.workflowContext ?? inputs.workflowJSON ?? inputs.mode ?? inputs._historicalMessages;
|
||||
if (!hasContext) return undefined;
|
||||
|
||||
const context: DatasetInputContext = {};
|
||||
|
||||
if (isUnknownRecord(inputs.workflowContext)) {
|
||||
context.workflowContext = inputs.workflowContext as ChatPayload['workflowContext'];
|
||||
}
|
||||
|
||||
if (isSimpleWorkflow(inputs.workflowJSON)) {
|
||||
context.existingWorkflow = inputs.workflowJSON;
|
||||
}
|
||||
|
||||
if (inputs.mode === 'build' || inputs.mode === 'plan') {
|
||||
context.mode = inputs.mode;
|
||||
}
|
||||
|
||||
if (Array.isArray(inputs._historicalMessages) && inputs._historicalMessages.length > 0) {
|
||||
context.historicalMessages = deserializeLcMessages(inputs._historicalMessages);
|
||||
}
|
||||
|
||||
return Object.keys(context).length > 0 ? context : undefined;
|
||||
}
|
||||
|
||||
function createLangsmithFeedbackExtractor(): (
|
||||
rootRun: Run,
|
||||
_example?: Example,
|
||||
@@ -1171,6 +1302,35 @@ function updateStats(
|
||||
else stats.errors++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve LangSmith dataset examples and enrich them with conversation history.
|
||||
* Handles fallback preloading when filters/maxExamples are requested on a string dataset.
|
||||
*/
|
||||
async function resolveAndEnrichLangsmithData(params: {
|
||||
dataset: string;
|
||||
langsmithOptions: LangsmithRunConfig['langsmithOptions'];
|
||||
lsClient: LangsmithRunConfig['langsmithClient'];
|
||||
logger: EvalLogger;
|
||||
}): Promise<string | Example[]> {
|
||||
const { dataset, langsmithOptions, lsClient, logger } = params;
|
||||
let data = await resolveLangsmithData({ dataset, langsmithOptions, lsClient, logger });
|
||||
// Defensive: if maxExamples/filters were requested but we still got a dataset name,
|
||||
// fall back to preloading so we can honor limits instead of streaming everything.
|
||||
if (
|
||||
typeof data === 'string' &&
|
||||
((langsmithOptions.maxExamples ?? 0) > 0 || langsmithOptions.filters !== undefined)
|
||||
) {
|
||||
data = await loadExamplesFromDataset({
|
||||
lsClient,
|
||||
datasetName: data,
|
||||
maxExamples: langsmithOptions.maxExamples,
|
||||
filters: langsmithOptions.filters,
|
||||
});
|
||||
}
|
||||
// Enrich pre-loaded examples with conversation history from outputs
|
||||
return Array.isArray(data) ? enrichExamplesWithHistory(data) : data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run evaluation in LangSmith mode.
|
||||
*/
|
||||
@@ -1215,15 +1375,17 @@ async function runLangsmith(config: LangsmithRunConfig): Promise<RunSummary> {
|
||||
prompt: string;
|
||||
genFn: (
|
||||
prompt: string,
|
||||
datasetInputContext?: DatasetInputContext,
|
||||
collectors?: GenerationCollectors,
|
||||
) => Promise<SimpleWorkflow | GenerationResult>;
|
||||
collectors?: GenerationCollectors;
|
||||
limiter?: LlmCallLimiter;
|
||||
genTimeoutMs?: number;
|
||||
datasetInputContext?: DatasetInputContext;
|
||||
}): Promise<SimpleWorkflow | GenerationResult> => {
|
||||
return await runWithOptionalLimiter(async () => {
|
||||
return await withTimeout({
|
||||
promise: args.genFn(args.prompt, args.collectors),
|
||||
promise: args.genFn(args.prompt, args.datasetInputContext, args.collectors),
|
||||
timeoutMs: args.genTimeoutMs,
|
||||
label: 'workflow_generation',
|
||||
});
|
||||
@@ -1278,6 +1440,7 @@ async function runLangsmith(config: LangsmithRunConfig): Promise<RunSummary> {
|
||||
const index = targetCallCount;
|
||||
// Extract prompt from inputs (supports both direct prompt and messages array)
|
||||
const prompt = extractPrompt(inputs);
|
||||
const datasetInputContext = extractDatasetInputContext(inputs);
|
||||
const { evals: datasetContext, ...rest } = inputs;
|
||||
|
||||
lifecycle?.onExampleStart?.(index, totalExamples, prompt);
|
||||
@@ -1292,6 +1455,7 @@ async function runLangsmith(config: LangsmithRunConfig): Promise<RunSummary> {
|
||||
collectors,
|
||||
limiter: effectiveGlobalContext.llmCallLimiter,
|
||||
genTimeoutMs: timeoutMs,
|
||||
datasetInputContext,
|
||||
});
|
||||
const genDurationMs = Date.now() - genStart;
|
||||
|
||||
@@ -1321,6 +1485,7 @@ async function runLangsmith(config: LangsmithRunConfig): Promise<RunSummary> {
|
||||
testCaseContext: extracted,
|
||||
generatedCode,
|
||||
pinData,
|
||||
datasetInputContext,
|
||||
});
|
||||
|
||||
// Run all evaluators in parallel (wrapped in traceable so it appears
|
||||
@@ -1411,25 +1576,11 @@ async function runLangsmith(config: LangsmithRunConfig): Promise<RunSummary> {
|
||||
|
||||
const feedbackExtractor = createLangsmithFeedbackExtractor();
|
||||
|
||||
// Load examples if maxExamples is set
|
||||
if (typeof dataset !== 'string') {
|
||||
throw new Error('LangSmith mode requires dataset to be a dataset name string');
|
||||
}
|
||||
|
||||
let data = await resolveLangsmithData({ dataset, langsmithOptions, lsClient, logger });
|
||||
// Defensive: if maxExamples/filters were requested but we still got a dataset name,
|
||||
// fall back to preloading so we can honor limits instead of streaming everything.
|
||||
if (
|
||||
typeof data === 'string' &&
|
||||
((langsmithOptions.maxExamples ?? 0) > 0 || langsmithOptions.filters !== undefined)
|
||||
) {
|
||||
data = await loadExamplesFromDataset({
|
||||
lsClient,
|
||||
datasetName: data,
|
||||
maxExamples: langsmithOptions.maxExamples,
|
||||
filters: langsmithOptions.filters,
|
||||
});
|
||||
}
|
||||
const data = await resolveAndEnrichLangsmithData({ dataset, langsmithOptions, lsClient, logger });
|
||||
|
||||
const effectiveData = applyRepetitions(data, langsmithOptions.repetitions);
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ import type { StreamOutput } from '../types/streaming';
|
||||
import type { ChatPayload } from '../workflow-builder-agent';
|
||||
import { CodeBuilderAgent } from './code-builder-agent';
|
||||
import { SessionChatHandler } from './handlers/session-chat-handler';
|
||||
import type { HistoryContext } from './prompts';
|
||||
import type { TokenUsage } from './types';
|
||||
export type { TokenUsage };
|
||||
export type { HistoryContext, TokenUsage };
|
||||
|
||||
/**
|
||||
* Configuration for CodeWorkflowBuilder
|
||||
@@ -122,12 +123,14 @@ export class CodeWorkflowBuilder {
|
||||
* @param payload - Chat payload with message and workflow context
|
||||
* @param userId - User ID for logging
|
||||
* @param abortSignal - Optional abort signal
|
||||
* @param historyContext - Optional conversation history (used in evals when no session handler)
|
||||
* @yields StreamOutput chunks for messages, tool progress, and workflow updates
|
||||
*/
|
||||
async *chat(
|
||||
payload: ChatPayload,
|
||||
userId: string,
|
||||
abortSignal?: AbortSignal,
|
||||
historyContext?: HistoryContext,
|
||||
): AsyncGenerator<StreamOutput, void, unknown> {
|
||||
const workflowId = payload.workflowContext?.currentWorkflow?.id;
|
||||
|
||||
@@ -150,7 +153,12 @@ export class CodeWorkflowBuilder {
|
||||
// No session handler - track generation success and call callback manually
|
||||
let generationSucceeded = false;
|
||||
|
||||
for await (const chunk of this.codeBuilderAgent.chat(payload, userId, abortSignal)) {
|
||||
for await (const chunk of this.codeBuilderAgent.chat(
|
||||
payload,
|
||||
userId,
|
||||
abortSignal,
|
||||
historyContext,
|
||||
)) {
|
||||
// Check if this chunk indicates successful workflow generation
|
||||
if (chunk.messages?.some((msg) => msg.type === 'workflow-updated')) {
|
||||
generationSucceeded = true;
|
||||
|
||||
@@ -12,7 +12,7 @@ export type { CodeBuilderAgentConfig, ParseAndValidateResult, ValidationWarning
|
||||
|
||||
// Code Workflow Builder
|
||||
export { CodeWorkflowBuilder } from './code-workflow-builder';
|
||||
export type { CodeWorkflowBuilderConfig } from './code-workflow-builder';
|
||||
export type { CodeWorkflowBuilderConfig, HistoryContext } from './code-workflow-builder';
|
||||
|
||||
// Session utilities
|
||||
export { generateCodeBuilderThreadId } from './utils/code-builder-session';
|
||||
|
||||
Reference in New Issue
Block a user