@wundr.io/langgraph-orchestrator 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +842 -0
- package/dist/checkpointing.d.ts +265 -0
- package/dist/checkpointing.d.ts.map +1 -0
- package/dist/checkpointing.js +577 -0
- package/dist/checkpointing.js.map +1 -0
- package/dist/edges/conditional-edge.d.ts +230 -0
- package/dist/edges/conditional-edge.d.ts.map +1 -0
- package/dist/edges/conditional-edge.js +439 -0
- package/dist/edges/conditional-edge.js.map +1 -0
- package/dist/edges/loop-edge.d.ts +290 -0
- package/dist/edges/loop-edge.d.ts.map +1 -0
- package/dist/edges/loop-edge.js +503 -0
- package/dist/edges/loop-edge.js.map +1 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +269 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes/decision-node.d.ts +276 -0
- package/dist/nodes/decision-node.d.ts.map +1 -0
- package/dist/nodes/decision-node.js +403 -0
- package/dist/nodes/decision-node.js.map +1 -0
- package/dist/nodes/human-node.d.ts +272 -0
- package/dist/nodes/human-node.d.ts.map +1 -0
- package/dist/nodes/human-node.js +394 -0
- package/dist/nodes/human-node.js.map +1 -0
- package/dist/nodes/llm-node.d.ts +173 -0
- package/dist/nodes/llm-node.d.ts.map +1 -0
- package/dist/nodes/llm-node.js +325 -0
- package/dist/nodes/llm-node.js.map +1 -0
- package/dist/nodes/tool-node.d.ts +151 -0
- package/dist/nodes/tool-node.d.ts.map +1 -0
- package/dist/nodes/tool-node.js +373 -0
- package/dist/nodes/tool-node.js.map +1 -0
- package/dist/prebuilt-graphs/plan-execute-refine.d.ts +149 -0
- package/dist/prebuilt-graphs/plan-execute-refine.d.ts.map +1 -0
- package/dist/prebuilt-graphs/plan-execute-refine.js +600 -0
- package/dist/prebuilt-graphs/plan-execute-refine.js.map +1 -0
- package/dist/state-graph.d.ts +158 -0
- package/dist/state-graph.d.ts.map +1 -0
- package/dist/state-graph.js +756 -0
- package/dist/state-graph.js.map +1 -0
- package/dist/types.d.ts +762 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +73 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
- package/src/checkpointing.ts +702 -0
- package/src/edges/conditional-edge.ts +518 -0
- package/src/edges/loop-edge.ts +623 -0
- package/src/index.ts +416 -0
- package/src/nodes/decision-node.ts +538 -0
- package/src/nodes/human-node.ts +572 -0
- package/src/nodes/llm-node.ts +448 -0
- package/src/nodes/tool-node.ts +525 -0
- package/src/prebuilt-graphs/plan-execute-refine.ts +769 -0
- package/src/state-graph.ts +990 -0
- package/src/types.ts +729 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human Node - Human-in-the-loop interaction node
|
|
3
|
+
* @module @wundr.io/langgraph-orchestrator
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
AgentState,
|
|
11
|
+
NodeDefinition,
|
|
12
|
+
NodeContext,
|
|
13
|
+
NodeResult,
|
|
14
|
+
Message,
|
|
15
|
+
} from '../types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configuration for human node
|
|
19
|
+
*/
|
|
20
|
+
export interface HumanNodeConfig {
|
|
21
|
+
/** Prompt to display to the human */
|
|
22
|
+
readonly prompt?: string | ((state: AgentState) => string);
|
|
23
|
+
/** Input handler to collect human response */
|
|
24
|
+
readonly inputHandler: HumanInputHandler;
|
|
25
|
+
/** Validation for human input */
|
|
26
|
+
readonly validation?: z.ZodSchema;
|
|
27
|
+
/** Timeout for human response in milliseconds */
|
|
28
|
+
readonly timeout?: number;
|
|
29
|
+
/** What to do on timeout */
|
|
30
|
+
readonly onTimeout?: 'error' | 'skip' | 'default';
|
|
31
|
+
/** Default value to use on timeout */
|
|
32
|
+
readonly defaultValue?: unknown;
|
|
33
|
+
/** Pre-defined choices for the human */
|
|
34
|
+
readonly choices?: HumanChoice[];
|
|
35
|
+
/** Whether to require confirmation */
|
|
36
|
+
readonly requireConfirmation?: boolean;
|
|
37
|
+
/** Custom response processor */
|
|
38
|
+
readonly processResponse?: (
|
|
39
|
+
response: HumanResponse,
|
|
40
|
+
state: AgentState
|
|
41
|
+
) => Partial<AgentState['data']>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Handler for collecting human input
|
|
46
|
+
*/
|
|
47
|
+
export interface HumanInputHandler {
|
|
48
|
+
/** Request input from human */
|
|
49
|
+
request(context: HumanInputContext): Promise<HumanResponse>;
|
|
50
|
+
/** Cancel a pending request */
|
|
51
|
+
cancel?(requestId: string): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Context provided to human input handler
|
|
56
|
+
*/
|
|
57
|
+
export interface HumanInputContext {
|
|
58
|
+
/** Unique request ID */
|
|
59
|
+
readonly requestId: string;
|
|
60
|
+
/** Prompt to display */
|
|
61
|
+
readonly prompt: string;
|
|
62
|
+
/** Available choices */
|
|
63
|
+
readonly choices?: HumanChoice[];
|
|
64
|
+
/** Current workflow state (sanitized) */
|
|
65
|
+
readonly state: Partial<AgentState>;
|
|
66
|
+
/** Timeout in milliseconds */
|
|
67
|
+
readonly timeout?: number;
|
|
68
|
+
/** Additional metadata */
|
|
69
|
+
readonly metadata?: Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Human choice option
|
|
74
|
+
*/
|
|
75
|
+
export interface HumanChoice {
|
|
76
|
+
/** Choice value */
|
|
77
|
+
readonly value: string;
|
|
78
|
+
/** Display label */
|
|
79
|
+
readonly label: string;
|
|
80
|
+
/** Description */
|
|
81
|
+
readonly description?: string;
|
|
82
|
+
/** Whether this is the default choice */
|
|
83
|
+
readonly default?: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Response from human input
|
|
88
|
+
*/
|
|
89
|
+
export interface HumanResponse {
|
|
90
|
+
/** The response value */
|
|
91
|
+
readonly value: unknown;
|
|
92
|
+
/** Response type */
|
|
93
|
+
readonly type: 'input' | 'choice' | 'confirmation' | 'cancel' | 'timeout';
|
|
94
|
+
/** Timestamp of response */
|
|
95
|
+
readonly timestamp: Date;
|
|
96
|
+
/** Additional metadata */
|
|
97
|
+
readonly metadata?: Record<string, unknown>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Schema for human node configuration validation
|
|
102
|
+
*/
|
|
103
|
+
export const HumanNodeConfigSchema = z.object({
|
|
104
|
+
prompt: z.union([z.string(), z.function()]).optional(),
|
|
105
|
+
timeout: z.number().min(0).optional(),
|
|
106
|
+
onTimeout: z.enum(['error', 'skip', 'default']).optional(),
|
|
107
|
+
choices: z
|
|
108
|
+
.array(
|
|
109
|
+
z.object({
|
|
110
|
+
value: z.string(),
|
|
111
|
+
label: z.string(),
|
|
112
|
+
description: z.string().optional(),
|
|
113
|
+
default: z.boolean().optional(),
|
|
114
|
+
}),
|
|
115
|
+
)
|
|
116
|
+
.optional(),
|
|
117
|
+
requireConfirmation: z.boolean().optional(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create a human-in-the-loop node
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* const humanNode = createHumanNode({
|
|
126
|
+
* id: 'approval',
|
|
127
|
+
* name: 'Human Approval',
|
|
128
|
+
* config: {
|
|
129
|
+
* prompt: 'Please review and approve the generated content.',
|
|
130
|
+
* inputHandler: myInputHandler,
|
|
131
|
+
* choices: [
|
|
132
|
+
* { value: 'approve', label: 'Approve' },
|
|
133
|
+
* { value: 'reject', label: 'Reject' },
|
|
134
|
+
* { value: 'modify', label: 'Request Modifications' }
|
|
135
|
+
* ],
|
|
136
|
+
* timeout: 300000 // 5 minutes
|
|
137
|
+
* }
|
|
138
|
+
* });
|
|
139
|
+
*
|
|
140
|
+
* graph.addNode('approval', humanNode);
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* @param options - Node creation options
|
|
144
|
+
* @returns NodeDefinition for use in StateGraph
|
|
145
|
+
*/
|
|
146
|
+
export function createHumanNode<
|
|
147
|
+
TState extends AgentState = AgentState,
|
|
148
|
+
>(options: {
|
|
149
|
+
id: string;
|
|
150
|
+
name: string;
|
|
151
|
+
config: HumanNodeConfig;
|
|
152
|
+
nodeConfig?: NodeDefinition<TState>['config'];
|
|
153
|
+
}): NodeDefinition<TState> {
|
|
154
|
+
const { id, name, config, nodeConfig = {} } = options;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
id,
|
|
158
|
+
name,
|
|
159
|
+
type: 'human',
|
|
160
|
+
config: nodeConfig,
|
|
161
|
+
execute: async (
|
|
162
|
+
state: TState,
|
|
163
|
+
context: NodeContext,
|
|
164
|
+
): Promise<NodeResult<TState>> => {
|
|
165
|
+
const requestId = uuidv4();
|
|
166
|
+
|
|
167
|
+
// Build prompt
|
|
168
|
+
const prompt =
|
|
169
|
+
typeof config.prompt === 'function'
|
|
170
|
+
? config.prompt(state)
|
|
171
|
+
: (config.prompt ?? 'Please provide input:');
|
|
172
|
+
|
|
173
|
+
context.services.logger.info('Requesting human input', {
|
|
174
|
+
requestId,
|
|
175
|
+
hasChoices: Boolean(config.choices?.length),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Build input context
|
|
179
|
+
const inputContext: HumanInputContext = {
|
|
180
|
+
requestId,
|
|
181
|
+
prompt,
|
|
182
|
+
choices: config.choices,
|
|
183
|
+
state: sanitizeStateForHuman(state),
|
|
184
|
+
timeout: config.timeout,
|
|
185
|
+
metadata: {
|
|
186
|
+
nodeId: id,
|
|
187
|
+
nodeName: name,
|
|
188
|
+
executionId: context.executionId,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
let response: HumanResponse;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
// Request input with timeout if configured
|
|
196
|
+
if (config.timeout) {
|
|
197
|
+
response = await Promise.race([
|
|
198
|
+
config.inputHandler.request(inputContext),
|
|
199
|
+
createTimeoutPromise(config.timeout, requestId),
|
|
200
|
+
]);
|
|
201
|
+
} else {
|
|
202
|
+
response = await config.inputHandler.request(inputContext);
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
// Handle timeout
|
|
206
|
+
if ((error as Error).message === 'TIMEOUT') {
|
|
207
|
+
response = {
|
|
208
|
+
value: config.defaultValue,
|
|
209
|
+
type: 'timeout',
|
|
210
|
+
timestamp: new Date(),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
if (config.onTimeout === 'error') {
|
|
214
|
+
throw new Error(`Human input timed out after ${config.timeout}ms`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (config.onTimeout === 'skip') {
|
|
218
|
+
context.services.logger.warn('Human input timed out, skipping');
|
|
219
|
+
return { state };
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
context.services.logger.debug('Received human response', {
|
|
227
|
+
requestId,
|
|
228
|
+
type: response.type,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Handle cancellation
|
|
232
|
+
if (response.type === 'cancel') {
|
|
233
|
+
context.services.logger.info('Human cancelled input');
|
|
234
|
+
return {
|
|
235
|
+
state,
|
|
236
|
+
terminate: true,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Validate response if schema provided
|
|
241
|
+
if (config.validation && response.type !== 'timeout') {
|
|
242
|
+
try {
|
|
243
|
+
config.validation.parse(response.value);
|
|
244
|
+
} catch (validationError) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Human input validation failed: ${validationError instanceof Error ? validationError.message : String(validationError)}`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Build human message
|
|
252
|
+
const humanMessage: Message = {
|
|
253
|
+
id: uuidv4(),
|
|
254
|
+
role: 'user',
|
|
255
|
+
content: formatResponseContent(response),
|
|
256
|
+
timestamp: response.timestamp,
|
|
257
|
+
metadata: {
|
|
258
|
+
requestId,
|
|
259
|
+
responseType: response.type,
|
|
260
|
+
...response.metadata,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Build updated state
|
|
265
|
+
let newData = { ...state.data };
|
|
266
|
+
newData['lastHumanResponse'] = response;
|
|
267
|
+
|
|
268
|
+
// Apply custom response processor
|
|
269
|
+
if (config.processResponse) {
|
|
270
|
+
const processed = config.processResponse(response, state);
|
|
271
|
+
newData = { ...newData, ...processed };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const newState: TState = {
|
|
275
|
+
...state,
|
|
276
|
+
messages: [...state.messages, humanMessage],
|
|
277
|
+
data: newData,
|
|
278
|
+
} as TState;
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
state: newState,
|
|
282
|
+
metadata: {
|
|
283
|
+
duration: 0,
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Sanitize state for human viewing
|
|
292
|
+
*/
|
|
293
|
+
function sanitizeStateForHuman(state: AgentState): Partial<AgentState> {
|
|
294
|
+
return {
|
|
295
|
+
currentStep: state.currentStep,
|
|
296
|
+
data: state.data,
|
|
297
|
+
messages: state.messages.slice(-5), // Only show recent messages
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Create a timeout promise
|
|
303
|
+
*/
|
|
304
|
+
function createTimeoutPromise(
|
|
305
|
+
timeout: number,
|
|
306
|
+
_requestId: string,
|
|
307
|
+
): Promise<HumanResponse> {
|
|
308
|
+
return new Promise((_, reject) => {
|
|
309
|
+
setTimeout(() => reject(new Error('TIMEOUT')), timeout);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Format response content as string
|
|
315
|
+
*/
|
|
316
|
+
function formatResponseContent(response: HumanResponse): string {
|
|
317
|
+
if (response.type === 'timeout') {
|
|
318
|
+
return '[Timed out - using default]';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (response.type === 'cancel') {
|
|
322
|
+
return '[Cancelled]';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (typeof response.value === 'string') {
|
|
326
|
+
return response.value;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return JSON.stringify(response.value);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Create a console-based human input handler
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* ```typescript
|
|
337
|
+
* const handler = createConsoleInputHandler();
|
|
338
|
+
*
|
|
339
|
+
* const humanNode = createHumanNode({
|
|
340
|
+
* id: 'input',
|
|
341
|
+
* name: 'User Input',
|
|
342
|
+
* config: {
|
|
343
|
+
* inputHandler: handler,
|
|
344
|
+
* prompt: 'Enter your response:'
|
|
345
|
+
* }
|
|
346
|
+
* });
|
|
347
|
+
* ```
|
|
348
|
+
*
|
|
349
|
+
* @returns HumanInputHandler for console input
|
|
350
|
+
*/
|
|
351
|
+
export function createConsoleInputHandler(): HumanInputHandler {
|
|
352
|
+
// Note: This is a placeholder - actual console input would need readline
|
|
353
|
+
// In production, use a proper event-based input handler
|
|
354
|
+
return {
|
|
355
|
+
async request(_context: HumanInputContext): Promise<HumanResponse> {
|
|
356
|
+
// In a real implementation, this would read from stdin
|
|
357
|
+
// For now, return a placeholder response
|
|
358
|
+
return {
|
|
359
|
+
value: 'placeholder-response',
|
|
360
|
+
type: 'input',
|
|
361
|
+
timestamp: new Date(),
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Create a callback-based human input handler
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* ```typescript
|
|
372
|
+
* const pendingRequests = new Map();
|
|
373
|
+
*
|
|
374
|
+
* const handler = createCallbackInputHandler({
|
|
375
|
+
* onRequest: (context) => {
|
|
376
|
+
* pendingRequests.set(context.requestId, context);
|
|
377
|
+
* // Notify UI or external system
|
|
378
|
+
* },
|
|
379
|
+
* onResolve: (requestId) => pendingRequests.delete(requestId)
|
|
380
|
+
* });
|
|
381
|
+
*
|
|
382
|
+
* // External system resolves:
|
|
383
|
+
* handler.resolve(requestId, { value: 'user input', type: 'input', timestamp: new Date() });
|
|
384
|
+
* ```
|
|
385
|
+
*
|
|
386
|
+
* @param options - Handler options
|
|
387
|
+
* @returns HumanInputHandler with resolve capability
|
|
388
|
+
*/
|
|
389
|
+
export function createCallbackInputHandler(options: {
|
|
390
|
+
onRequest?: (context: HumanInputContext) => void;
|
|
391
|
+
onResolve?: (requestId: string) => void;
|
|
392
|
+
onCancel?: (requestId: string) => void;
|
|
393
|
+
}): HumanInputHandler & {
|
|
394
|
+
resolve: (requestId: string, response: HumanResponse) => void;
|
|
395
|
+
reject: (requestId: string, error: Error) => void;
|
|
396
|
+
} {
|
|
397
|
+
const pending = new Map<
|
|
398
|
+
string,
|
|
399
|
+
{
|
|
400
|
+
resolve: (response: HumanResponse) => void;
|
|
401
|
+
reject: (error: Error) => void;
|
|
402
|
+
}
|
|
403
|
+
>();
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
async request(context: HumanInputContext): Promise<HumanResponse> {
|
|
407
|
+
return new Promise((resolve, reject) => {
|
|
408
|
+
pending.set(context.requestId, { resolve, reject });
|
|
409
|
+
options.onRequest?.(context);
|
|
410
|
+
});
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
async cancel(requestId: string): Promise<void> {
|
|
414
|
+
const handlers = pending.get(requestId);
|
|
415
|
+
if (handlers) {
|
|
416
|
+
handlers.resolve({
|
|
417
|
+
value: null,
|
|
418
|
+
type: 'cancel',
|
|
419
|
+
timestamp: new Date(),
|
|
420
|
+
});
|
|
421
|
+
pending.delete(requestId);
|
|
422
|
+
options.onCancel?.(requestId);
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
resolve(requestId: string, response: HumanResponse): void {
|
|
427
|
+
const handlers = pending.get(requestId);
|
|
428
|
+
if (handlers) {
|
|
429
|
+
handlers.resolve(response);
|
|
430
|
+
pending.delete(requestId);
|
|
431
|
+
options.onResolve?.(requestId);
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
reject(requestId: string, error: Error): void {
|
|
436
|
+
const handlers = pending.get(requestId);
|
|
437
|
+
if (handlers) {
|
|
438
|
+
handlers.reject(error);
|
|
439
|
+
pending.delete(requestId);
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Create a confirmation node for human approval
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* ```typescript
|
|
450
|
+
* const confirmNode = createConfirmationNode({
|
|
451
|
+
* id: 'confirm-action',
|
|
452
|
+
* name: 'Confirm Action',
|
|
453
|
+
* inputHandler: myHandler,
|
|
454
|
+
* message: (state) => `Are you sure you want to ${state.data.action}?`,
|
|
455
|
+
* onConfirm: 'execute-action',
|
|
456
|
+
* onReject: 'cancel-action'
|
|
457
|
+
* });
|
|
458
|
+
* ```
|
|
459
|
+
*
|
|
460
|
+
* @param options - Confirmation node options
|
|
461
|
+
* @returns NodeDefinition for use in StateGraph
|
|
462
|
+
*/
|
|
463
|
+
export function createConfirmationNode<
|
|
464
|
+
TState extends AgentState = AgentState,
|
|
465
|
+
>(options: {
|
|
466
|
+
id: string;
|
|
467
|
+
name: string;
|
|
468
|
+
inputHandler: HumanInputHandler;
|
|
469
|
+
message: string | ((state: TState) => string);
|
|
470
|
+
onConfirm: string;
|
|
471
|
+
onReject: string;
|
|
472
|
+
timeout?: number;
|
|
473
|
+
nodeConfig?: NodeDefinition<TState>['config'];
|
|
474
|
+
}): NodeDefinition<TState> {
|
|
475
|
+
return createHumanNode<TState>({
|
|
476
|
+
id: options.id,
|
|
477
|
+
name: options.name,
|
|
478
|
+
config: {
|
|
479
|
+
prompt: options.message as string | ((state: AgentState) => string),
|
|
480
|
+
inputHandler: options.inputHandler,
|
|
481
|
+
choices: [
|
|
482
|
+
{ value: 'confirm', label: 'Confirm', default: false },
|
|
483
|
+
{ value: 'reject', label: 'Reject', default: false },
|
|
484
|
+
],
|
|
485
|
+
timeout: options.timeout,
|
|
486
|
+
onTimeout: 'error',
|
|
487
|
+
processResponse: response => ({
|
|
488
|
+
confirmationResult: response.value,
|
|
489
|
+
}),
|
|
490
|
+
},
|
|
491
|
+
nodeConfig: options.nodeConfig,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Create a feedback collection node
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* ```typescript
|
|
500
|
+
* const feedbackNode = createFeedbackNode({
|
|
501
|
+
* id: 'collect-feedback',
|
|
502
|
+
* name: 'Collect Feedback',
|
|
503
|
+
* inputHandler: myHandler,
|
|
504
|
+
* questions: [
|
|
505
|
+
* { id: 'rating', prompt: 'Rate this response (1-5):', type: 'number' },
|
|
506
|
+
* { id: 'comments', prompt: 'Any additional comments?', type: 'text' }
|
|
507
|
+
* ]
|
|
508
|
+
* });
|
|
509
|
+
* ```
|
|
510
|
+
*
|
|
511
|
+
* @param options - Feedback node options
|
|
512
|
+
* @returns NodeDefinition for use in StateGraph
|
|
513
|
+
*/
|
|
514
|
+
export function createFeedbackNode<
|
|
515
|
+
TState extends AgentState = AgentState,
|
|
516
|
+
>(options: {
|
|
517
|
+
id: string;
|
|
518
|
+
name: string;
|
|
519
|
+
inputHandler: HumanInputHandler;
|
|
520
|
+
questions: Array<{
|
|
521
|
+
id: string;
|
|
522
|
+
prompt: string;
|
|
523
|
+
type: 'text' | 'number' | 'choice';
|
|
524
|
+
choices?: string[];
|
|
525
|
+
required?: boolean;
|
|
526
|
+
}>;
|
|
527
|
+
timeout?: number;
|
|
528
|
+
nodeConfig?: NodeDefinition<TState>['config'];
|
|
529
|
+
}): NodeDefinition<TState> {
|
|
530
|
+
return {
|
|
531
|
+
id: options.id,
|
|
532
|
+
name: options.name,
|
|
533
|
+
type: 'human',
|
|
534
|
+
config: options.nodeConfig ?? {},
|
|
535
|
+
execute: async (
|
|
536
|
+
state: TState,
|
|
537
|
+
_context: NodeContext,
|
|
538
|
+
): Promise<NodeResult<TState>> => {
|
|
539
|
+
const feedback: Record<string, unknown> = {};
|
|
540
|
+
|
|
541
|
+
for (const question of options.questions) {
|
|
542
|
+
const requestId = uuidv4();
|
|
543
|
+
|
|
544
|
+
const inputContext: HumanInputContext = {
|
|
545
|
+
requestId,
|
|
546
|
+
prompt: question.prompt,
|
|
547
|
+
choices: question.choices?.map(c => ({ value: c, label: c })),
|
|
548
|
+
state: sanitizeStateForHuman(state),
|
|
549
|
+
timeout: options.timeout,
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const response = await options.inputHandler.request(inputContext);
|
|
553
|
+
|
|
554
|
+
if (response.type === 'cancel') {
|
|
555
|
+
return { state, terminate: true };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
feedback[question.id] = response.value;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const newState: TState = {
|
|
562
|
+
...state,
|
|
563
|
+
data: {
|
|
564
|
+
...state.data,
|
|
565
|
+
feedback,
|
|
566
|
+
},
|
|
567
|
+
} as TState;
|
|
568
|
+
|
|
569
|
+
return { state: newState };
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
}
|