@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,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Node - LLM-based decision and generation 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
|
+
LLMRequest,
|
|
16
|
+
LLMResponse,
|
|
17
|
+
Tool,
|
|
18
|
+
} from '../types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for LLM node
|
|
22
|
+
*/
|
|
23
|
+
export interface LLMNodeConfig {
|
|
24
|
+
/** Model to use (provider-specific) */
|
|
25
|
+
readonly model?: string;
|
|
26
|
+
/** System prompt for the LLM */
|
|
27
|
+
readonly systemPrompt?: string;
|
|
28
|
+
/** Temperature for generation */
|
|
29
|
+
readonly temperature?: number;
|
|
30
|
+
/** Maximum tokens to generate */
|
|
31
|
+
readonly maxTokens?: number;
|
|
32
|
+
/** Stop sequences */
|
|
33
|
+
readonly stop?: string[];
|
|
34
|
+
/** Available tools for the LLM */
|
|
35
|
+
readonly tools?: Tool[];
|
|
36
|
+
/** Whether to stream the response */
|
|
37
|
+
readonly stream?: boolean;
|
|
38
|
+
/** Custom prompt template */
|
|
39
|
+
readonly promptTemplate?: (state: AgentState) => string;
|
|
40
|
+
/** Post-processing function for response */
|
|
41
|
+
readonly postProcess?: (
|
|
42
|
+
response: LLMResponse,
|
|
43
|
+
state: AgentState
|
|
44
|
+
) => Partial<AgentState>;
|
|
45
|
+
/** Routing function to determine next node based on response */
|
|
46
|
+
readonly router?: (
|
|
47
|
+
response: LLMResponse,
|
|
48
|
+
state: AgentState
|
|
49
|
+
) => string | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Schema for LLM node configuration validation
|
|
54
|
+
*/
|
|
55
|
+
export const LLMNodeConfigSchema = z.object({
|
|
56
|
+
model: z.string().optional(),
|
|
57
|
+
systemPrompt: z.string().optional(),
|
|
58
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
59
|
+
maxTokens: z.number().min(1).optional(),
|
|
60
|
+
stop: z.array(z.string()).optional(),
|
|
61
|
+
stream: z.boolean().optional(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create an LLM node for the workflow graph
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const llmNode = createLLMNode({
|
|
70
|
+
* id: 'agent',
|
|
71
|
+
* name: 'Agent Node',
|
|
72
|
+
* config: {
|
|
73
|
+
* model: 'claude-3-sonnet-20240229',
|
|
74
|
+
* systemPrompt: 'You are a helpful assistant.',
|
|
75
|
+
* temperature: 0.7
|
|
76
|
+
* }
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* graph.addNode('agent', llmNode);
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @param options - Node creation options
|
|
83
|
+
* @returns NodeDefinition for use in StateGraph
|
|
84
|
+
*/
|
|
85
|
+
export function createLLMNode<TState extends AgentState = AgentState>(options: {
|
|
86
|
+
id: string;
|
|
87
|
+
name: string;
|
|
88
|
+
config: LLMNodeConfig;
|
|
89
|
+
nodeConfig?: NodeDefinition<TState>['config'];
|
|
90
|
+
}): NodeDefinition<TState> {
|
|
91
|
+
const { id, name, config, nodeConfig = {} } = options;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
id,
|
|
95
|
+
name,
|
|
96
|
+
type: 'llm',
|
|
97
|
+
config: nodeConfig,
|
|
98
|
+
execute: async (
|
|
99
|
+
state: TState,
|
|
100
|
+
context: NodeContext,
|
|
101
|
+
): Promise<NodeResult<TState>> => {
|
|
102
|
+
const llmProvider = context.services.llmProvider;
|
|
103
|
+
|
|
104
|
+
if (!llmProvider) {
|
|
105
|
+
throw new Error('LLM provider not configured in node services');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Build messages for the request
|
|
109
|
+
const messages = buildMessages(state, config);
|
|
110
|
+
|
|
111
|
+
// Build the request
|
|
112
|
+
const request: LLMRequest = {
|
|
113
|
+
messages,
|
|
114
|
+
model: config.model,
|
|
115
|
+
temperature: config.temperature,
|
|
116
|
+
maxTokens: config.maxTokens,
|
|
117
|
+
stop: config.stop,
|
|
118
|
+
tools: config.tools,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
context.services.logger.debug('Sending LLM request', {
|
|
122
|
+
model: config.model,
|
|
123
|
+
messageCount: messages.length,
|
|
124
|
+
hasTools: Boolean(config.tools?.length),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Execute the request
|
|
128
|
+
const response = await llmProvider.generate(request);
|
|
129
|
+
|
|
130
|
+
context.services.logger.debug('Received LLM response', {
|
|
131
|
+
model: response.model,
|
|
132
|
+
finishReason: response.finishReason,
|
|
133
|
+
tokensUsed: response.usage.totalTokens,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Build updated state
|
|
137
|
+
const newMessages: Message[] = [...state.messages, response.message];
|
|
138
|
+
|
|
139
|
+
let newData = { ...state.data };
|
|
140
|
+
|
|
141
|
+
// Apply post-processing if configured
|
|
142
|
+
if (config.postProcess) {
|
|
143
|
+
const processed = config.postProcess(response, state);
|
|
144
|
+
newData = { ...newData, ...processed.data };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Update token count in metadata
|
|
148
|
+
const tokensUsed =
|
|
149
|
+
(state.metadata.tokensUsed ?? 0) + response.usage.totalTokens;
|
|
150
|
+
|
|
151
|
+
const newState: TState = {
|
|
152
|
+
...state,
|
|
153
|
+
messages: newMessages,
|
|
154
|
+
data: newData,
|
|
155
|
+
metadata: {
|
|
156
|
+
...state.metadata,
|
|
157
|
+
tokensUsed,
|
|
158
|
+
},
|
|
159
|
+
} as TState;
|
|
160
|
+
|
|
161
|
+
// Determine next node
|
|
162
|
+
let next: string | undefined;
|
|
163
|
+
|
|
164
|
+
// Check if there are tool calls - if so, route to tool node
|
|
165
|
+
if (
|
|
166
|
+
response.finishReason === 'tool_calls' &&
|
|
167
|
+
response.message.toolCalls?.length
|
|
168
|
+
) {
|
|
169
|
+
// Store tool calls in state for tool node to process
|
|
170
|
+
newState.data['pendingToolCalls'] = response.message.toolCalls;
|
|
171
|
+
next = 'tools'; // Default name for tool node
|
|
172
|
+
} else if (config.router) {
|
|
173
|
+
next = config.router(response, state);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
state: newState,
|
|
178
|
+
next,
|
|
179
|
+
metadata: {
|
|
180
|
+
duration: 0, // Will be set by executor
|
|
181
|
+
tokensUsed: response.usage.totalTokens,
|
|
182
|
+
toolCalls: response.message.toolCalls,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Build messages array for LLM request
|
|
191
|
+
*/
|
|
192
|
+
function buildMessages(state: AgentState, config: LLMNodeConfig): Message[] {
|
|
193
|
+
const messages: Message[] = [];
|
|
194
|
+
|
|
195
|
+
// Add system prompt if configured
|
|
196
|
+
if (config.systemPrompt) {
|
|
197
|
+
messages.push({
|
|
198
|
+
id: uuidv4(),
|
|
199
|
+
role: 'system',
|
|
200
|
+
content: config.systemPrompt,
|
|
201
|
+
timestamp: new Date(),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Add existing messages from state
|
|
206
|
+
messages.push(...state.messages);
|
|
207
|
+
|
|
208
|
+
// If there's a custom prompt template, add it as a user message
|
|
209
|
+
if (config.promptTemplate) {
|
|
210
|
+
const prompt = config.promptTemplate(state);
|
|
211
|
+
messages.push({
|
|
212
|
+
id: uuidv4(),
|
|
213
|
+
role: 'user',
|
|
214
|
+
content: prompt,
|
|
215
|
+
timestamp: new Date(),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return messages;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Create a router function for LLM decision-making
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* const router = createLLMRouter({
|
|
228
|
+
* routes: {
|
|
229
|
+
* 'continue': 'process-node',
|
|
230
|
+
* 'finish': 'end-node',
|
|
231
|
+
* 'error': 'error-handler'
|
|
232
|
+
* },
|
|
233
|
+
* defaultRoute: 'continue',
|
|
234
|
+
* extractDecision: (response) => {
|
|
235
|
+
* const content = response.message.content;
|
|
236
|
+
* if (content.includes('DONE')) return 'finish';
|
|
237
|
+
* if (content.includes('ERROR')) return 'error';
|
|
238
|
+
* return 'continue';
|
|
239
|
+
* }
|
|
240
|
+
* });
|
|
241
|
+
* ```
|
|
242
|
+
*
|
|
243
|
+
* @param options - Router configuration
|
|
244
|
+
* @returns Router function for use in LLM node config
|
|
245
|
+
*/
|
|
246
|
+
export function createLLMRouter<
|
|
247
|
+
TState extends AgentState = AgentState,
|
|
248
|
+
>(options: {
|
|
249
|
+
routes: Record<string, string>;
|
|
250
|
+
defaultRoute?: string;
|
|
251
|
+
extractDecision: (response: LLMResponse, state: TState) => string;
|
|
252
|
+
}): (response: LLMResponse, state: TState) => string | undefined {
|
|
253
|
+
return (response: LLMResponse, state: TState): string | undefined => {
|
|
254
|
+
const decision = options.extractDecision(response, state);
|
|
255
|
+
return options.routes[decision] ?? options.defaultRoute;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create a structured output LLM node that validates responses
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```typescript
|
|
264
|
+
* const responseSchema = z.object({
|
|
265
|
+
* action: z.enum(['search', 'answer', 'clarify']),
|
|
266
|
+
* content: z.string(),
|
|
267
|
+
* confidence: z.number()
|
|
268
|
+
* });
|
|
269
|
+
*
|
|
270
|
+
* const structuredNode = createStructuredLLMNode({
|
|
271
|
+
* id: 'structured-agent',
|
|
272
|
+
* name: 'Structured Agent',
|
|
273
|
+
* config: {
|
|
274
|
+
* model: 'claude-3-sonnet-20240229',
|
|
275
|
+
* systemPrompt: 'Respond in JSON format with action, content, and confidence fields.'
|
|
276
|
+
* },
|
|
277
|
+
* outputSchema: responseSchema,
|
|
278
|
+
* stateMapper: (parsed) => ({ agentDecision: parsed })
|
|
279
|
+
* });
|
|
280
|
+
* ```
|
|
281
|
+
*
|
|
282
|
+
* @param options - Structured node options
|
|
283
|
+
* @returns NodeDefinition for use in StateGraph
|
|
284
|
+
*/
|
|
285
|
+
export function createStructuredLLMNode<
|
|
286
|
+
TState extends AgentState = AgentState,
|
|
287
|
+
TOutput = unknown,
|
|
288
|
+
>(options: {
|
|
289
|
+
id: string;
|
|
290
|
+
name: string;
|
|
291
|
+
config: LLMNodeConfig;
|
|
292
|
+
outputSchema: z.ZodSchema<TOutput>;
|
|
293
|
+
stateMapper: (parsed: TOutput) => Partial<TState['data']>;
|
|
294
|
+
nodeConfig?: NodeDefinition<TState>['config'];
|
|
295
|
+
}): NodeDefinition<TState> {
|
|
296
|
+
const {
|
|
297
|
+
id,
|
|
298
|
+
name,
|
|
299
|
+
config,
|
|
300
|
+
outputSchema,
|
|
301
|
+
stateMapper,
|
|
302
|
+
nodeConfig = {},
|
|
303
|
+
} = options;
|
|
304
|
+
|
|
305
|
+
const baseNode = createLLMNode<TState>({
|
|
306
|
+
id,
|
|
307
|
+
name,
|
|
308
|
+
config: {
|
|
309
|
+
...config,
|
|
310
|
+
postProcess: (response, state) => {
|
|
311
|
+
// Try to parse JSON from response
|
|
312
|
+
const content = response.message.content;
|
|
313
|
+
let parsed: TOutput;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
// Try to extract JSON from the response
|
|
317
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
318
|
+
if (jsonMatch) {
|
|
319
|
+
const raw = JSON.parse(jsonMatch[0]);
|
|
320
|
+
parsed = outputSchema.parse(raw);
|
|
321
|
+
} else {
|
|
322
|
+
throw new Error('No JSON object found in response');
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`Failed to parse structured output: ${error instanceof Error ? error.message : String(error)}`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Map parsed output to state
|
|
331
|
+
const mappedData = stateMapper(parsed);
|
|
332
|
+
return { data: { ...state.data, ...mappedData } };
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
nodeConfig,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
...baseNode,
|
|
340
|
+
outputSchema,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Create a conversational LLM node that maintains chat history
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```typescript
|
|
349
|
+
* const chatNode = createConversationalLLMNode({
|
|
350
|
+
* id: 'chat',
|
|
351
|
+
* name: 'Chat Node',
|
|
352
|
+
* config: {
|
|
353
|
+
* model: 'claude-3-sonnet-20240229',
|
|
354
|
+
* systemPrompt: 'You are a helpful assistant.'
|
|
355
|
+
* },
|
|
356
|
+
* maxHistoryLength: 10
|
|
357
|
+
* });
|
|
358
|
+
* ```
|
|
359
|
+
*
|
|
360
|
+
* @param options - Conversational node options
|
|
361
|
+
* @returns NodeDefinition for use in StateGraph
|
|
362
|
+
*/
|
|
363
|
+
export function createConversationalLLMNode<
|
|
364
|
+
TState extends AgentState = AgentState,
|
|
365
|
+
>(options: {
|
|
366
|
+
id: string;
|
|
367
|
+
name: string;
|
|
368
|
+
config: LLMNodeConfig;
|
|
369
|
+
maxHistoryLength?: number;
|
|
370
|
+
nodeConfig?: NodeDefinition<TState>['config'];
|
|
371
|
+
}): NodeDefinition<TState> {
|
|
372
|
+
const { id, name, config, maxHistoryLength = 50, nodeConfig = {} } = options;
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
id,
|
|
376
|
+
name,
|
|
377
|
+
type: 'llm',
|
|
378
|
+
config: nodeConfig,
|
|
379
|
+
execute: async (
|
|
380
|
+
state: TState,
|
|
381
|
+
context: NodeContext,
|
|
382
|
+
): Promise<NodeResult<TState>> => {
|
|
383
|
+
const llmProvider = context.services.llmProvider;
|
|
384
|
+
|
|
385
|
+
if (!llmProvider) {
|
|
386
|
+
throw new Error('LLM provider not configured in node services');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Trim history if needed
|
|
390
|
+
let historyMessages = state.messages;
|
|
391
|
+
if (historyMessages.length > maxHistoryLength) {
|
|
392
|
+
// Keep system message if present, then most recent messages
|
|
393
|
+
const systemMessages = historyMessages.filter(m => m.role === 'system');
|
|
394
|
+
const nonSystemMessages = historyMessages.filter(
|
|
395
|
+
m => m.role !== 'system',
|
|
396
|
+
);
|
|
397
|
+
const trimmedNonSystem = nonSystemMessages.slice(-maxHistoryLength);
|
|
398
|
+
historyMessages = [...systemMessages, ...trimmedNonSystem];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Build messages
|
|
402
|
+
const messages: Message[] = [];
|
|
403
|
+
|
|
404
|
+
if (config.systemPrompt) {
|
|
405
|
+
messages.push({
|
|
406
|
+
id: uuidv4(),
|
|
407
|
+
role: 'system',
|
|
408
|
+
content: config.systemPrompt,
|
|
409
|
+
timestamp: new Date(),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
messages.push(...historyMessages.filter(m => m.role !== 'system'));
|
|
414
|
+
|
|
415
|
+
const request: LLMRequest = {
|
|
416
|
+
messages,
|
|
417
|
+
model: config.model,
|
|
418
|
+
temperature: config.temperature,
|
|
419
|
+
maxTokens: config.maxTokens,
|
|
420
|
+
stop: config.stop,
|
|
421
|
+
tools: config.tools,
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const response = await llmProvider.generate(request);
|
|
425
|
+
|
|
426
|
+
const newMessages = [...state.messages, response.message];
|
|
427
|
+
const tokensUsed =
|
|
428
|
+
(state.metadata.tokensUsed ?? 0) + response.usage.totalTokens;
|
|
429
|
+
|
|
430
|
+
const newState: TState = {
|
|
431
|
+
...state,
|
|
432
|
+
messages: newMessages,
|
|
433
|
+
metadata: {
|
|
434
|
+
...state.metadata,
|
|
435
|
+
tokensUsed,
|
|
436
|
+
},
|
|
437
|
+
} as TState;
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
state: newState,
|
|
441
|
+
metadata: {
|
|
442
|
+
duration: 0,
|
|
443
|
+
tokensUsed: response.usage.totalTokens,
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|