@codemation/core-nodes 0.1.1 → 0.3.0
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/CHANGELOG.md +46 -0
- package/dist/index.cjs +446 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +175 -66
- package/dist/index.d.ts +175 -66
- package/dist/index.js +430 -140
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/chatModels/OpenAIStructuredOutputMethodFactory.ts +46 -0
- package/src/index.ts +1 -0
- package/src/nodes/AIAgentConfig.ts +3 -0
- package/src/nodes/AIAgentExecutionHelpersFactory.ts +18 -11
- package/src/nodes/AIAgentNode.ts +145 -6
- package/src/nodes/AgentStructuredOutputRepairPromptFactory.ts +61 -0
- package/src/nodes/AgentStructuredOutputRunner.ts +222 -0
- package/src/nodes/NodeBackedToolRuntime.ts +12 -3
- package/src/nodes/aiAgent.ts +2 -0
- package/src/nodes/mapData.ts +15 -2
- package/src/nodes/switch.ts +0 -1
- package/src/workflowAuthoring/WorkflowAgentNodeFactory.types.ts +5 -9
- package/src/workflowAuthoring/WorkflowAuthoringOptions.types.ts +9 -3
- package/src/workflowAuthoring/WorkflowBranchBuilder.types.ts +13 -9
- package/src/workflowAuthoring/WorkflowChain.types.ts +36 -19
- package/src/workflowAuthoring.types.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemation/core-nodes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@langchain/core": "^1.1.31",
|
|
32
32
|
"@langchain/openai": "^1.2.12",
|
|
33
33
|
"lucide-react": "^0.577.0",
|
|
34
|
-
"@codemation/core": "0.
|
|
34
|
+
"@codemation/core": "0.6.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.3.5",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ChatModelConfig, ChatModelStructuredOutputOptions } from "@codemation/core";
|
|
2
|
+
import { injectable } from "@codemation/core";
|
|
3
|
+
|
|
4
|
+
import { OpenAIChatModelFactory } from "./OpenAIChatModelFactory";
|
|
5
|
+
|
|
6
|
+
@injectable()
|
|
7
|
+
export class OpenAIStructuredOutputMethodFactory {
|
|
8
|
+
private static readonly isoDatePattern = /^\d{4}-\d{2}-\d{2}$/;
|
|
9
|
+
|
|
10
|
+
create(chatModelConfig: ChatModelConfig): ChatModelStructuredOutputOptions | undefined {
|
|
11
|
+
if (chatModelConfig.type !== OpenAIChatModelFactory) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const model = this.readModelName(chatModelConfig);
|
|
15
|
+
if (!model) {
|
|
16
|
+
return { method: "functionCalling", strict: true };
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
method: this.supportsJsonSchema(model) ? "jsonSchema" : "functionCalling",
|
|
20
|
+
strict: true,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private readModelName(chatModelConfig: ChatModelConfig): string | undefined {
|
|
25
|
+
const candidate = chatModelConfig as Readonly<{ model?: unknown }>;
|
|
26
|
+
return typeof candidate.model === "string" ? candidate.model : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private supportsJsonSchema(model: string): boolean {
|
|
30
|
+
if (model === "gpt-4o" || model === "gpt-4o-mini") {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return (
|
|
34
|
+
this.supportsSnapshotAtOrAfter(model, "gpt-4o-", "2024-08-06") ||
|
|
35
|
+
this.supportsSnapshotAtOrAfter(model, "gpt-4o-mini-", "2024-07-18")
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private supportsSnapshotAtOrAfter(model: string, prefix: string, minimumSnapshotDate: string): boolean {
|
|
40
|
+
if (!model.startsWith(prefix)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const snapshotDate = model.slice(prefix.length);
|
|
44
|
+
return OpenAIStructuredOutputMethodFactory.isoDatePattern.test(snapshotDate) && snapshotDate >= minimumSnapshotDate;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from "./canvasIconName";
|
|
2
2
|
export * from "./chatModels/OpenAIChatModelFactory";
|
|
3
|
+
export * from "./chatModels/OpenAIStructuredOutputMethodFactory";
|
|
3
4
|
export * from "./chatModels/OpenAiCredentialSession";
|
|
4
5
|
export * from "./chatModels/openAiChatModelConfig";
|
|
5
6
|
export * from "./chatModels/OpenAiChatModelPresetsFactory";
|
|
@@ -23,6 +23,7 @@ export interface AIAgentOptions<TInputJson = unknown, _TOutputJson = unknown> {
|
|
|
23
23
|
readonly guardrails?: AgentGuardrailConfig;
|
|
24
24
|
/** Engine applies with {@link RunnableNodeConfig.inputSchema} before {@link AIAgentNode.execute}. */
|
|
25
25
|
readonly inputSchema?: ZodType<TInputJson>;
|
|
26
|
+
readonly outputSchema?: ZodType<_TOutputJson>;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -44,6 +45,7 @@ export class AIAgent<TInputJson = unknown, TOutputJson = unknown>
|
|
|
44
45
|
readonly retryPolicy: RetryPolicySpec;
|
|
45
46
|
readonly guardrails?: AgentGuardrailConfig;
|
|
46
47
|
readonly inputSchema?: ZodType<TInputJson>;
|
|
48
|
+
readonly outputSchema?: ZodType<TOutputJson>;
|
|
47
49
|
|
|
48
50
|
constructor(options: AIAgentOptions<TInputJson, TOutputJson>) {
|
|
49
51
|
this.name = options.name;
|
|
@@ -54,5 +56,6 @@ export class AIAgent<TInputJson = unknown, TOutputJson = unknown>
|
|
|
54
56
|
this.retryPolicy = options.retryPolicy ?? RetryPolicy.defaultForAiAgent;
|
|
55
57
|
this.guardrails = options.guardrails;
|
|
56
58
|
this.inputSchema = options.inputSchema;
|
|
59
|
+
this.outputSchema = options.outputSchema;
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -33,10 +33,10 @@ export class AIAgentExecutionHelpersFactory {
|
|
|
33
33
|
`Cannot create LangChain tool "${entry.config.name}": missing inputSchema (broken tool runtime resolution).`,
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
|
-
const schemaForOpenAi = this.
|
|
37
|
-
entry.config.name,
|
|
38
|
-
|
|
39
|
-
);
|
|
36
|
+
const schemaForOpenAi = this.createJsonSchemaRecord(entry.runtime.inputSchema, {
|
|
37
|
+
schemaName: entry.config.name,
|
|
38
|
+
requireObjectRoot: true,
|
|
39
|
+
});
|
|
40
40
|
return new DynamicStructuredTool({
|
|
41
41
|
name: entry.config.name,
|
|
42
42
|
description: entry.config.description ?? entry.runtime.defaultDescription,
|
|
@@ -63,9 +63,12 @@ export class AIAgentExecutionHelpersFactory {
|
|
|
63
63
|
* (duplicate `zod` copies), fall back to Zod `toJSONSchema` with draft-07.
|
|
64
64
|
* - Strip root `$schema` for OpenAI; normalize invalid `required` keywords for cfworker; ensure `properties`.
|
|
65
65
|
*/
|
|
66
|
-
|
|
67
|
-
toolName: string,
|
|
66
|
+
createJsonSchemaRecord(
|
|
68
67
|
inputSchema: ZodSchemaAny,
|
|
68
|
+
options: Readonly<{
|
|
69
|
+
schemaName: string;
|
|
70
|
+
requireObjectRoot: boolean;
|
|
71
|
+
}>,
|
|
69
72
|
): Record<string, unknown> {
|
|
70
73
|
const draft07Params = { target: "draft-07" as const };
|
|
71
74
|
let converted: unknown;
|
|
@@ -79,17 +82,21 @@ export class AIAgentExecutionHelpersFactory {
|
|
|
79
82
|
}
|
|
80
83
|
const record = converted as Record<string, unknown>;
|
|
81
84
|
const { $schema: _draftSchemaOmitted, ...rest } = record;
|
|
82
|
-
if (rest.type !== "object") {
|
|
85
|
+
if (options.requireObjectRoot && rest.type !== "object") {
|
|
83
86
|
throw new Error(
|
|
84
|
-
`Cannot create LangChain tool "${
|
|
87
|
+
`Cannot create LangChain tool "${options.schemaName}": tool input schema must be a JSON Schema object type (got type=${String(rest.type)}).`,
|
|
85
88
|
);
|
|
86
89
|
}
|
|
87
|
-
if (
|
|
90
|
+
if (
|
|
91
|
+
options.requireObjectRoot &&
|
|
92
|
+
rest.properties !== undefined &&
|
|
93
|
+
(typeof rest.properties !== "object" || Array.isArray(rest.properties))
|
|
94
|
+
) {
|
|
88
95
|
throw new Error(
|
|
89
|
-
`Cannot create LangChain tool "${
|
|
96
|
+
`Cannot create LangChain tool "${options.schemaName}": tool input schema "properties" must be an object (got ${JSON.stringify(rest.properties)}).`,
|
|
90
97
|
);
|
|
91
98
|
}
|
|
92
|
-
if (rest.properties === undefined) {
|
|
99
|
+
if (options.requireObjectRoot && rest.properties === undefined) {
|
|
93
100
|
rest.properties = {};
|
|
94
101
|
}
|
|
95
102
|
this.sanitizeJsonSchemaRequiredKeywordsForCfworker(rest);
|
package/src/nodes/AIAgentNode.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
RunnableNodeExecuteArgs,
|
|
14
14
|
Tool,
|
|
15
15
|
ToolConfig,
|
|
16
|
+
LangChainStructuredOutputModelLike,
|
|
16
17
|
ZodSchemaAny,
|
|
17
18
|
} from "@codemation/core";
|
|
18
19
|
|
|
@@ -20,6 +21,7 @@ import type { CredentialSessionService } from "@codemation/core";
|
|
|
20
21
|
import {
|
|
21
22
|
AgentGuardrailDefaults,
|
|
22
23
|
AgentMessageConfigNormalizer,
|
|
24
|
+
CallableToolConfig,
|
|
23
25
|
ConnectionInvocationIdFactory,
|
|
24
26
|
ConnectionNodeIdFactory,
|
|
25
27
|
CoreTokens,
|
|
@@ -37,6 +39,7 @@ import { AIAgentExecutionHelpersFactory } from "./AIAgentExecutionHelpersFactory
|
|
|
37
39
|
import { ConnectionCredentialExecutionContextFactory } from "./ConnectionCredentialExecutionContextFactory";
|
|
38
40
|
import { AgentMessageFactory } from "./AgentMessageFactory";
|
|
39
41
|
import { AgentOutputFactory } from "./AgentOutputFactory";
|
|
42
|
+
import { AgentStructuredOutputRunner } from "./AgentStructuredOutputRunner";
|
|
40
43
|
import { AgentToolCallPortMap } from "./AgentToolCallPortMapFactory";
|
|
41
44
|
import { NodeBackedToolRuntime } from "./NodeBackedToolRuntime";
|
|
42
45
|
import {
|
|
@@ -86,6 +89,8 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
86
89
|
private readonly nodeBackedToolRuntime: NodeBackedToolRuntime,
|
|
87
90
|
@inject(AIAgentExecutionHelpersFactory)
|
|
88
91
|
private readonly executionHelpers: AIAgentExecutionHelpersFactory,
|
|
92
|
+
@inject(AgentStructuredOutputRunner)
|
|
93
|
+
private readonly structuredOutputRunner: AgentStructuredOutputRunner,
|
|
89
94
|
) {
|
|
90
95
|
this.connectionCredentialExecutionContextFactory =
|
|
91
96
|
this.executionHelpers.createConnectionCredentialExecutionContextFactory(credentialSessions);
|
|
@@ -146,6 +151,35 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
146
151
|
const itemInputsByPort = AgentItemPortMap.fromItem(item);
|
|
147
152
|
const itemScopedTools = this.createItemScopedTools(prepared.resolvedTools, ctx, item, itemIndex, items);
|
|
148
153
|
const conversation: BaseMessage[] = [...this.createPromptMessages(item, itemIndex, items, ctx)];
|
|
154
|
+
if (ctx.config.outputSchema && itemScopedTools.length === 0) {
|
|
155
|
+
const structuredOutput = await this.structuredOutputRunner.resolve({
|
|
156
|
+
model: prepared.model,
|
|
157
|
+
chatModelConfig: ctx.config.chatModel,
|
|
158
|
+
schema: ctx.config.outputSchema,
|
|
159
|
+
conversation,
|
|
160
|
+
agentName: this.getAgentDisplayName(ctx),
|
|
161
|
+
nodeId: ctx.nodeId,
|
|
162
|
+
invokeTextModel: async (messages) =>
|
|
163
|
+
await this.invokeModel(
|
|
164
|
+
prepared.model,
|
|
165
|
+
prepared.languageModelConnectionNodeId,
|
|
166
|
+
messages,
|
|
167
|
+
ctx,
|
|
168
|
+
itemInputsByPort,
|
|
169
|
+
prepared.guardrails.modelInvocationOptions,
|
|
170
|
+
),
|
|
171
|
+
invokeStructuredModel: async (structuredModel, messages) =>
|
|
172
|
+
await this.invokeStructuredModel(
|
|
173
|
+
structuredModel,
|
|
174
|
+
prepared.languageModelConnectionNodeId,
|
|
175
|
+
messages,
|
|
176
|
+
ctx,
|
|
177
|
+
itemInputsByPort,
|
|
178
|
+
prepared.guardrails.modelInvocationOptions,
|
|
179
|
+
),
|
|
180
|
+
});
|
|
181
|
+
return this.buildOutputItem(item, structuredOutput);
|
|
182
|
+
}
|
|
149
183
|
const modelWithTools = this.bindToolsToModel(prepared.model, itemScopedTools);
|
|
150
184
|
const finalResponse = await this.runTurnLoopUntilFinalAnswer({
|
|
151
185
|
prepared,
|
|
@@ -154,7 +188,14 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
154
188
|
conversation,
|
|
155
189
|
modelWithTools,
|
|
156
190
|
});
|
|
157
|
-
|
|
191
|
+
const outputJson = await this.resolveFinalOutputJson(
|
|
192
|
+
prepared,
|
|
193
|
+
itemInputsByPort,
|
|
194
|
+
conversation,
|
|
195
|
+
finalResponse,
|
|
196
|
+
itemScopedTools.length > 0,
|
|
197
|
+
);
|
|
198
|
+
return this.buildOutputItem(item, outputJson);
|
|
158
199
|
}
|
|
159
200
|
|
|
160
201
|
/**
|
|
@@ -234,11 +275,47 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
234
275
|
);
|
|
235
276
|
}
|
|
236
277
|
|
|
237
|
-
private
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
278
|
+
private async resolveFinalOutputJson(
|
|
279
|
+
prepared: PreparedAgentExecution,
|
|
280
|
+
itemInputsByPort: NodeInputsByPort,
|
|
281
|
+
conversation: ReadonlyArray<BaseMessage>,
|
|
282
|
+
finalResponse: AIMessage,
|
|
283
|
+
wasToolEnabledRun: boolean,
|
|
284
|
+
): Promise<unknown> {
|
|
285
|
+
if (!prepared.ctx.config.outputSchema) {
|
|
286
|
+
return AgentOutputFactory.fromAgentContent(AgentMessageFactory.extractContent(finalResponse));
|
|
287
|
+
}
|
|
288
|
+
return await this.structuredOutputRunner.resolve({
|
|
289
|
+
model: prepared.model,
|
|
290
|
+
chatModelConfig: prepared.ctx.config.chatModel,
|
|
291
|
+
schema: prepared.ctx.config.outputSchema,
|
|
292
|
+
conversation: wasToolEnabledRun ? [...conversation, finalResponse] : conversation,
|
|
293
|
+
rawFinalResponse: finalResponse,
|
|
294
|
+
agentName: this.getAgentDisplayName(prepared.ctx),
|
|
295
|
+
nodeId: prepared.ctx.nodeId,
|
|
296
|
+
invokeTextModel: async (messages) =>
|
|
297
|
+
await this.invokeModel(
|
|
298
|
+
prepared.model,
|
|
299
|
+
prepared.languageModelConnectionNodeId,
|
|
300
|
+
messages,
|
|
301
|
+
prepared.ctx,
|
|
302
|
+
itemInputsByPort,
|
|
303
|
+
prepared.guardrails.modelInvocationOptions,
|
|
304
|
+
),
|
|
305
|
+
invokeStructuredModel: async (structuredModel, messages) =>
|
|
306
|
+
await this.invokeStructuredModel(
|
|
307
|
+
structuredModel,
|
|
308
|
+
prepared.languageModelConnectionNodeId,
|
|
309
|
+
messages,
|
|
310
|
+
prepared.ctx,
|
|
311
|
+
itemInputsByPort,
|
|
312
|
+
prepared.guardrails.modelInvocationOptions,
|
|
313
|
+
),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private buildOutputItem(item: Item, outputJson: unknown): Item {
|
|
318
|
+
return AgentOutputFactory.replaceJson(item, outputJson);
|
|
242
319
|
}
|
|
243
320
|
|
|
244
321
|
private bindToolsToModel(
|
|
@@ -326,6 +403,40 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
326
403
|
}
|
|
327
404
|
}
|
|
328
405
|
|
|
406
|
+
private async invokeStructuredModel(
|
|
407
|
+
model: LangChainStructuredOutputModelLike,
|
|
408
|
+
nodeId: string,
|
|
409
|
+
messages: ReadonlyArray<BaseMessage>,
|
|
410
|
+
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
411
|
+
inputsByPort: NodeInputsByPort,
|
|
412
|
+
options?: AgentGuardrailConfig["modelInvocationOptions"],
|
|
413
|
+
): Promise<unknown> {
|
|
414
|
+
await ctx.nodeState?.markQueued({ nodeId, activationId: ctx.activationId, inputsByPort });
|
|
415
|
+
await ctx.nodeState?.markRunning({ nodeId, activationId: ctx.activationId, inputsByPort });
|
|
416
|
+
try {
|
|
417
|
+
const response = await model.invoke(messages, options);
|
|
418
|
+
await ctx.nodeState?.markCompleted({
|
|
419
|
+
nodeId,
|
|
420
|
+
activationId: ctx.activationId,
|
|
421
|
+
inputsByPort,
|
|
422
|
+
outputs: AgentOutputFactory.fromUnknown(response),
|
|
423
|
+
});
|
|
424
|
+
await ctx.nodeState?.appendConnectionInvocation({
|
|
425
|
+
invocationId: ConnectionInvocationIdFactory.create(),
|
|
426
|
+
connectionNodeId: nodeId,
|
|
427
|
+
parentAgentNodeId: ctx.nodeId,
|
|
428
|
+
parentAgentActivationId: ctx.activationId,
|
|
429
|
+
status: "completed",
|
|
430
|
+
managedInput: this.summarizeLlmMessages(messages),
|
|
431
|
+
managedOutput: this.resultToJsonValue(response),
|
|
432
|
+
finishedAt: new Date().toISOString(),
|
|
433
|
+
});
|
|
434
|
+
return response;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
throw await this.failTrackedNodeInvocation(error, nodeId, ctx, inputsByPort, this.summarizeLlmMessages(messages));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
329
440
|
private async markQueuedTools(
|
|
330
441
|
plannedToolCalls: ReadonlyArray<PlannedToolCall>,
|
|
331
442
|
ctx: NodeExecutionContext<AIAgent<any, any>>,
|
|
@@ -514,6 +625,20 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
514
625
|
execute: async (args) => await this.nodeBackedToolRuntime.execute(config, args),
|
|
515
626
|
};
|
|
516
627
|
}
|
|
628
|
+
if (this.isCallableToolConfig(config)) {
|
|
629
|
+
const inputSchema = config.getInputSchema();
|
|
630
|
+
if (inputSchema == null) {
|
|
631
|
+
throw new Error(
|
|
632
|
+
`AIAgent tool "${config.name}": callable tool is missing inputSchema (cannot build LangChain tool).`,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
defaultDescription: config.description ?? `Callable tool "${config.name}".`,
|
|
637
|
+
inputSchema,
|
|
638
|
+
execute: async (args) =>
|
|
639
|
+
await config.executeTool({ ...args, config: config as CallableToolConfig<ZodSchemaAny, ZodSchemaAny> }),
|
|
640
|
+
};
|
|
641
|
+
}
|
|
517
642
|
const tool = this.nodeResolver.resolve(config.type) as Tool<ToolConfig, ZodSchemaAny, ZodSchemaAny>;
|
|
518
643
|
if (tool.inputSchema == null) {
|
|
519
644
|
throw new Error(`AIAgent tool "${config.name}": plugin tool "${String(config.type)}" is missing inputSchema.`);
|
|
@@ -537,6 +662,16 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
537
662
|
);
|
|
538
663
|
}
|
|
539
664
|
|
|
665
|
+
/**
|
|
666
|
+
* Callable tools use {@link CallableToolConfig#toolKind} for cross-package / JSON round-trip safety.
|
|
667
|
+
*/
|
|
668
|
+
private isCallableToolConfig(config: ToolConfig): config is CallableToolConfig<ZodSchemaAny, ZodSchemaAny> {
|
|
669
|
+
return (
|
|
670
|
+
config instanceof CallableToolConfig ||
|
|
671
|
+
(typeof config === "object" && config !== null && (config as { toolKind?: unknown }).toolKind === "callable")
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
540
675
|
private resolveGuardrails(guardrails: AgentGuardrailConfig | undefined): ResolvedGuardrails {
|
|
541
676
|
const maxTurns = guardrails?.maxTurns ?? AgentGuardrailDefaults.maxTurns;
|
|
542
677
|
if (!Number.isInteger(maxTurns) || maxTurns < 1) {
|
|
@@ -548,4 +683,8 @@ export class AIAgentNode implements RunnableNode<AIAgent<any, any>> {
|
|
|
548
683
|
modelInvocationOptions: guardrails?.modelInvocationOptions,
|
|
549
684
|
};
|
|
550
685
|
}
|
|
686
|
+
|
|
687
|
+
private getAgentDisplayName(ctx: NodeExecutionContext<AIAgent<any, any>>): string {
|
|
688
|
+
return ctx.config.name ?? ctx.nodeId;
|
|
689
|
+
}
|
|
551
690
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AgentMessageDto, ZodSchemaAny } from "@codemation/core";
|
|
2
|
+
import { inject, injectable } from "@codemation/core";
|
|
3
|
+
|
|
4
|
+
import { AIAgentExecutionHelpersFactory } from "./AIAgentExecutionHelpersFactory";
|
|
5
|
+
|
|
6
|
+
@injectable()
|
|
7
|
+
export class AgentStructuredOutputRepairPromptFactory {
|
|
8
|
+
private static readonly maxSchemaLength = 8000;
|
|
9
|
+
private static readonly maxInvalidContentLength = 4000;
|
|
10
|
+
private static readonly maxValidationErrorLength = 4000;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
@inject(AIAgentExecutionHelpersFactory)
|
|
14
|
+
private readonly executionHelpers: AIAgentExecutionHelpersFactory,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
create(
|
|
18
|
+
args: Readonly<{
|
|
19
|
+
schema: ZodSchemaAny;
|
|
20
|
+
invalidContent: string;
|
|
21
|
+
validationError: string;
|
|
22
|
+
}>,
|
|
23
|
+
): ReadonlyArray<AgentMessageDto> {
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
role: "system",
|
|
27
|
+
content:
|
|
28
|
+
"Return only JSON that matches the required schema exactly. Do not include markdown fences, commentary, or prose.",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
role: "user",
|
|
32
|
+
content: JSON.stringify({
|
|
33
|
+
requiredSchema: this.truncate(
|
|
34
|
+
JSON.stringify(
|
|
35
|
+
this.executionHelpers.createJsonSchemaRecord(args.schema, {
|
|
36
|
+
schemaName: "agent_output",
|
|
37
|
+
requireObjectRoot: false,
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
AgentStructuredOutputRepairPromptFactory.maxSchemaLength,
|
|
41
|
+
),
|
|
42
|
+
invalidModelOutput: this.truncate(
|
|
43
|
+
args.invalidContent,
|
|
44
|
+
AgentStructuredOutputRepairPromptFactory.maxInvalidContentLength,
|
|
45
|
+
),
|
|
46
|
+
validationError: this.truncate(
|
|
47
|
+
args.validationError,
|
|
48
|
+
AgentStructuredOutputRepairPromptFactory.maxValidationErrorLength,
|
|
49
|
+
),
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private truncate(value: string, maxLength: number): string {
|
|
56
|
+
if (value.length <= maxLength) {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
return `${value.slice(0, maxLength)}...(truncated)`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChatModelConfig,
|
|
3
|
+
ChatModelStructuredOutputOptions,
|
|
4
|
+
LangChainChatModelLike,
|
|
5
|
+
LangChainStructuredOutputModelLike,
|
|
6
|
+
ZodSchemaAny,
|
|
7
|
+
} from "@codemation/core";
|
|
8
|
+
import { inject, injectable } from "@codemation/core";
|
|
9
|
+
|
|
10
|
+
import { AIMessage, type BaseMessage } from "@langchain/core/messages";
|
|
11
|
+
import { ZodError } from "zod";
|
|
12
|
+
|
|
13
|
+
import { OpenAIStructuredOutputMethodFactory } from "../chatModels/OpenAIStructuredOutputMethodFactory";
|
|
14
|
+
import { AgentMessageFactory } from "./AgentMessageFactory";
|
|
15
|
+
import { AgentStructuredOutputRepairPromptFactory } from "./AgentStructuredOutputRepairPromptFactory";
|
|
16
|
+
|
|
17
|
+
interface ParsedStructuredOutputSuccess<TValue> {
|
|
18
|
+
readonly ok: true;
|
|
19
|
+
readonly value: TValue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ParsedStructuredOutputFailure {
|
|
23
|
+
readonly ok: false;
|
|
24
|
+
readonly invalidContent: string;
|
|
25
|
+
readonly validationError: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type ParsedStructuredOutputResult<TValue> = ParsedStructuredOutputSuccess<TValue> | ParsedStructuredOutputFailure;
|
|
29
|
+
|
|
30
|
+
@injectable()
|
|
31
|
+
export class AgentStructuredOutputRunner {
|
|
32
|
+
private static readonly repairAttemptCount = 2;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
@inject(AgentStructuredOutputRepairPromptFactory)
|
|
36
|
+
private readonly repairPromptFactory: AgentStructuredOutputRepairPromptFactory,
|
|
37
|
+
@inject(OpenAIStructuredOutputMethodFactory)
|
|
38
|
+
private readonly openAiStructuredOutputMethodFactory: OpenAIStructuredOutputMethodFactory,
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
async resolve<TOutput>(
|
|
42
|
+
args: Readonly<{
|
|
43
|
+
model: LangChainChatModelLike;
|
|
44
|
+
chatModelConfig: ChatModelConfig;
|
|
45
|
+
schema: ZodSchemaAny;
|
|
46
|
+
conversation: ReadonlyArray<BaseMessage>;
|
|
47
|
+
rawFinalResponse?: AIMessage;
|
|
48
|
+
agentName: string;
|
|
49
|
+
nodeId: string;
|
|
50
|
+
invokeTextModel: (messages: ReadonlyArray<BaseMessage>) => Promise<AIMessage>;
|
|
51
|
+
invokeStructuredModel: (
|
|
52
|
+
model: LangChainStructuredOutputModelLike,
|
|
53
|
+
messages: ReadonlyArray<BaseMessage>,
|
|
54
|
+
) => Promise<unknown>;
|
|
55
|
+
}>,
|
|
56
|
+
): Promise<TOutput> {
|
|
57
|
+
let lastFailure: ParsedStructuredOutputFailure | undefined;
|
|
58
|
+
|
|
59
|
+
if (args.rawFinalResponse) {
|
|
60
|
+
const directResult = this.tryParseAndValidate<TOutput>(
|
|
61
|
+
AgentMessageFactory.extractContent(args.rawFinalResponse),
|
|
62
|
+
args.schema,
|
|
63
|
+
);
|
|
64
|
+
if (directResult.ok) {
|
|
65
|
+
return directResult.value;
|
|
66
|
+
}
|
|
67
|
+
lastFailure = directResult;
|
|
68
|
+
} else if (!this.supportsNativeStructuredOutput(args.model)) {
|
|
69
|
+
const rawResponse = await args.invokeTextModel(args.conversation);
|
|
70
|
+
const directResult = this.tryParseAndValidate<TOutput>(
|
|
71
|
+
AgentMessageFactory.extractContent(rawResponse),
|
|
72
|
+
args.schema,
|
|
73
|
+
);
|
|
74
|
+
if (directResult.ok) {
|
|
75
|
+
return directResult.value;
|
|
76
|
+
}
|
|
77
|
+
lastFailure = directResult;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const nativeStructuredModel = this.createStructuredOutputModel(args.model, args.chatModelConfig, args.schema);
|
|
82
|
+
if (nativeStructuredModel) {
|
|
83
|
+
const nativeResult = this.tryValidateStructuredValue<TOutput>(
|
|
84
|
+
await args.invokeStructuredModel(nativeStructuredModel, args.conversation),
|
|
85
|
+
args.schema,
|
|
86
|
+
);
|
|
87
|
+
if (nativeResult.ok) {
|
|
88
|
+
return nativeResult.value;
|
|
89
|
+
}
|
|
90
|
+
lastFailure = nativeResult;
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
lastFailure = {
|
|
94
|
+
ok: false,
|
|
95
|
+
invalidContent: "",
|
|
96
|
+
validationError: `Native structured output failed: ${this.summarizeError(error)}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return await this.retryWithRepairPrompt<TOutput>({
|
|
101
|
+
...args,
|
|
102
|
+
lastFailure:
|
|
103
|
+
lastFailure ??
|
|
104
|
+
({
|
|
105
|
+
ok: false,
|
|
106
|
+
invalidContent: "",
|
|
107
|
+
validationError: "Structured output was required but no valid structured response was produced.",
|
|
108
|
+
} satisfies ParsedStructuredOutputFailure),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private async retryWithRepairPrompt<TOutput>(
|
|
113
|
+
args: Readonly<{
|
|
114
|
+
schema: ZodSchemaAny;
|
|
115
|
+
conversation: ReadonlyArray<BaseMessage>;
|
|
116
|
+
lastFailure: ParsedStructuredOutputFailure;
|
|
117
|
+
agentName: string;
|
|
118
|
+
nodeId: string;
|
|
119
|
+
invokeTextModel: (messages: ReadonlyArray<BaseMessage>) => Promise<AIMessage>;
|
|
120
|
+
}>,
|
|
121
|
+
): Promise<TOutput> {
|
|
122
|
+
let failure = args.lastFailure;
|
|
123
|
+
for (let attempt = 1; attempt <= AgentStructuredOutputRunner.repairAttemptCount; attempt++) {
|
|
124
|
+
const repairMessages = [
|
|
125
|
+
...args.conversation,
|
|
126
|
+
...AgentMessageFactory.createPromptMessages(
|
|
127
|
+
this.repairPromptFactory.create({
|
|
128
|
+
schema: args.schema,
|
|
129
|
+
invalidContent: failure.invalidContent,
|
|
130
|
+
validationError: failure.validationError,
|
|
131
|
+
}),
|
|
132
|
+
),
|
|
133
|
+
];
|
|
134
|
+
const repairResponse = await args.invokeTextModel(repairMessages);
|
|
135
|
+
const repairResult = this.tryParseAndValidate<TOutput>(
|
|
136
|
+
AgentMessageFactory.extractContent(repairResponse),
|
|
137
|
+
args.schema,
|
|
138
|
+
);
|
|
139
|
+
if (repairResult.ok) {
|
|
140
|
+
return repairResult.value;
|
|
141
|
+
}
|
|
142
|
+
failure = repairResult;
|
|
143
|
+
}
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Structured output required for AIAgent "${args.agentName}" (${args.nodeId}) but validation still failed after ${AgentStructuredOutputRunner.repairAttemptCount} repair attempts: ${failure.validationError}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private createStructuredOutputModel(
|
|
150
|
+
model: LangChainChatModelLike,
|
|
151
|
+
chatModelConfig: ChatModelConfig,
|
|
152
|
+
schema: ZodSchemaAny,
|
|
153
|
+
): LangChainStructuredOutputModelLike | undefined {
|
|
154
|
+
if (!this.supportsNativeStructuredOutput(model)) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
const options = this.getStructuredOutputOptions(chatModelConfig);
|
|
158
|
+
return model.withStructuredOutput(schema, options);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private getStructuredOutputOptions(chatModelConfig: ChatModelConfig): ChatModelStructuredOutputOptions | undefined {
|
|
162
|
+
return this.openAiStructuredOutputMethodFactory.create(chatModelConfig) ?? { strict: true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private supportsNativeStructuredOutput(model: LangChainChatModelLike): model is LangChainChatModelLike & {
|
|
166
|
+
withStructuredOutput: (
|
|
167
|
+
outputSchema: ZodSchemaAny,
|
|
168
|
+
config?: ChatModelStructuredOutputOptions,
|
|
169
|
+
) => LangChainStructuredOutputModelLike;
|
|
170
|
+
} {
|
|
171
|
+
return typeof model.withStructuredOutput === "function";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private tryParseAndValidate<TOutput>(content: string, schema: ZodSchemaAny): ParsedStructuredOutputResult<TOutput> {
|
|
175
|
+
try {
|
|
176
|
+
return this.tryValidateStructuredValue<TOutput>(JSON.parse(content) as unknown, schema, content);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
invalidContent: content,
|
|
181
|
+
validationError: `Response was not valid JSON: ${this.summarizeError(error)}`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private tryValidateStructuredValue<TOutput>(
|
|
187
|
+
value: unknown,
|
|
188
|
+
schema: ZodSchemaAny,
|
|
189
|
+
invalidContent?: string,
|
|
190
|
+
): ParsedStructuredOutputResult<TOutput> {
|
|
191
|
+
try {
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
value: schema.parse(value) as TOutput,
|
|
195
|
+
};
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
invalidContent: invalidContent ?? this.toJson(value),
|
|
200
|
+
validationError: this.summarizeError(error),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private summarizeError(error: unknown): string {
|
|
206
|
+
if (error instanceof ZodError) {
|
|
207
|
+
return error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
|
|
208
|
+
}
|
|
209
|
+
if (error instanceof Error) {
|
|
210
|
+
return error.message;
|
|
211
|
+
}
|
|
212
|
+
return String(error);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private toJson(value: unknown): string {
|
|
216
|
+
try {
|
|
217
|
+
return JSON.stringify(value);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return `<<unserializable: ${this.summarizeError(error)}>>`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|