diff --git a/blocks/generateMessage.ts b/blocks/generateMessage.ts index ec00947..f8cb38c 100644 --- a/blocks/generateMessage.ts +++ b/blocks/generateMessage.ts @@ -50,7 +50,8 @@ export const generateMessage: AppBlock = { }, schema: { name: "Schema", - description: "The JSON schema to generate the object from.", + description: + "Schema used to constrain the output of the model to follow a specific schema, ensuring valid, parseable output for downstream processing.", type: { type: "object", additionalProperties: true, diff --git a/blocks/utils.ts b/blocks/utils.ts index 05865ca..612782d 100644 --- a/blocks/utils.ts +++ b/blocks/utils.ts @@ -1,6 +1,5 @@ import Anthropic from "@anthropic-ai/sdk"; import { events, kv, timers, messaging } from "@slflows/sdk/v1"; -import { Schema, Validator } from "jsonschema"; interface ToolDefinition { blockId: string; @@ -66,6 +65,7 @@ export function streamMessage(params: { force: boolean | string; thinking?: boolean | undefined; thinkingBudget?: number | undefined; + schema?: Anthropic.Messages.Tool.InputSchema | undefined; }) { const { apiKey, @@ -79,6 +79,7 @@ export function streamMessage(params: { force, thinking, thinkingBudget, + schema, } = params; const client = new Anthropic({ @@ -136,7 +137,13 @@ export function streamMessage(params: { disable_parallel_tool_use: hasMCPServers, } : undefined, - betas: ["mcp-client-2025-04-04"], + output_format: schema + ? { + type: "json_schema", + schema, + } + : undefined, + betas: ["mcp-client-2025-04-04", "structured-outputs-2025-11-13"], }); } @@ -269,135 +276,11 @@ export async function syncPendingEventWithStream( } } -export async function generateObject( - finalText: string, - params: { - apiKey: string; - model: string; - maxTokens: number; - messages: Anthropic.Beta.Messages.BetaMessageParam[]; - schema: Anthropic.Messages.Tool.InputSchema; - maxRetries: number; - pendingId: string; - inputTokens: number; - outputTokens: number; - parentEventId: string; - }, -): Promise { - const { - apiKey, - model, - maxTokens, - messages, - schema, - maxRetries, - pendingId, - parentEventId, - } = params; - - let retryCount = 0; - let { inputTokens, outputTokens } = params; - - let lastError: Error | undefined; - - while (retryCount < maxRetries) { - try { - await events.updatePending(pendingId, { - statusDescription: - retryCount === 0 - ? "Generating object..." - : `Generating object... (retry ${retryCount + 1})`, - }); - - // Anthropic currently does not support structured output in the same request as the user prompt. - // So we need to call the model one more time and force it to use the JSON tool. - // The arguments that the model will respond with will be the object that we want to generate. - - // Remove thinking blocks from messages since we're disabling thinking for this call - const messagesWithoutThinking = messages.map((msg) => ({ - ...msg, - content: Array.isArray(msg.content) - ? msg.content.filter((block: any) => block.type !== "thinking") - : msg.content, - })); - - const stream = streamMessage({ - maxTokens, - model, - messages: messagesWithoutThinking, - tools: [ - { - name: "json", - description: "Respond with a JSON object.", - input_schema: schema, - }, - ], - mcpServers: [], - force: "json", - apiKey, - }); - - const message = await stream.finalMessage(); - - inputTokens += message.usage.input_tokens; - outputTokens += message.usage.output_tokens; - - if (message.stop_reason === "tool_use") { - const toolCall = message.content.find( - (content) => content.type === "tool_use", - ); - - if (toolCall) { - const validator = new Validator(); - const result = validator.validate(toolCall.input, schema as Schema); - - if (result.errors.length === 0) { - return emitResult( - pendingId, - { - text: finalText, - object: toolCall.input, - usage: { - inputTokens, - outputTokens, - }, - }, - parentEventId, - ); - } - } - } - - retryCount++; - } catch (error) { - lastError = error instanceof Error ? error : new Error(String(error)); - retryCount++; - - // If this was the last retry, we'll exit the loop and handle the error below - if (retryCount >= maxRetries) { - break; - } - } - } - - // If we get here, all retries failed - await events.cancelPending( - pendingId, - lastError - ? `Object generation failed: ${lastError.message}` - : "Failed to generate object", - ); - - if (lastError) { - throw lastError; - } -} - export async function emitResult( pendingId: string, result: { text: string | null; - object: unknown; + output: unknown; usage: { inputTokens: number; outputTokens: number; @@ -408,8 +291,11 @@ export async function emitResult( await events.emit( { text: result.text, - object: result.object, + output: result.output, usage: result.usage, + + // TODO: Deprecated + object: null, }, { complete: pendingId, @@ -658,7 +544,6 @@ export async function handleModelResponse(params: { mcpServers, systemPrompt, turn, - apiKey, maxRetries, schema, thinking, @@ -676,32 +561,21 @@ export async function handleModelResponse(params: { throw new Error("Model did not respond with text"); } + let output = null; + if (schema) { - return generateObject(textPart.text, { - apiKey, - model, - maxTokens, - messages: [ - ...previousMessages, - { - role: message.role, - content: message.content, - }, - ], - schema, - maxRetries, - pendingId, - inputTokens: message.usage.input_tokens, - outputTokens: message.usage.output_tokens, - parentEventId: eventId, - }); + try { + output = JSON.parse(textPart.text); + } catch { + console.error("Failed to parse structured output"); + } } return emitResult( pendingId, { - text: textPart.text, - object: null, + text: schema ? null : textPart.text, + output, usage: { inputTokens: message.usage.input_tokens, outputTokens: message.usage.output_tokens, @@ -833,6 +707,7 @@ export async function executeTurn(params: { thinking, thinkingBudget, temperature, + schema, }); await syncPendingEventWithStream(pendingId, stream); diff --git a/package-lock.json b/package-lock.json index 153ca8b..01fb701 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,26 +5,45 @@ "packages": { "": { "dependencies": { - "@anthropic-ai/sdk": "^0.57.0", + "@anthropic-ai/sdk": "^0.69.0", "@modelcontextprotocol/sdk": "^1.12.1", - "@slflows/sdk": "^0.0.8", - "jsonschema": "^1.5.0", - "typescript": "^5.8.3" + "@slflows/sdk": "^0.5.1", + "typescript": "^5.9.3" }, "devDependencies": { - "@types/node": "^24.0.7", - "@useflows/flowctl": "^0.1.1", + "@types/node": "^24.10.1", + "@useflows/flowctl": "^0.4.0", "prettier": "^3.6.2", "ts-node": "^10.9.2" } }, "node_modules/@anthropic-ai/sdk": { - "version": "0.57.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.57.0.tgz", - "integrity": "sha512-z5LMy0MWu0+w2hflUgj4RlJr1R+0BxKXL7ldXTO8FasU8fu599STghO+QKwId2dAD0d464aHtU+ChWuRHw4FNw==", + "version": "0.69.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.69.0.tgz", + "integrity": "sha512-L92d2q47BSq+7slUqHBL1d2DwloulZotYGCTDt9AYRtPmYF+iK6rnwq9JaZwPPJgk+LenbcbQ/nj6gfaDFsl9w==", "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, "bin": { "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, "node_modules/@clack/core": { @@ -570,9 +589,9 @@ } }, "node_modules/@slflows/sdk": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@slflows/sdk/-/sdk-0.0.8.tgz", - "integrity": "sha512-dT1+Lt3uX4001gl3fdBiW/zih2b50DZVBuRvljD0HeNO1DlP285dthAqeaSNstukgy7G3zJznYQRDLpBdUwhVQ==" + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@slflows/sdk/-/sdk-0.5.1.tgz", + "integrity": "sha512-QRfr3t5O51QdMhSaeWQPTHVSfPPBB4MH4zYq31hSRJFDHzLnICf5G8QCV3Q9v6hfIyIXP7JdxGbEeUoBXG+/bw==" }, "node_modules/@tsconfig/node10": { "version": "1.0.11", @@ -603,13 +622,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/semver": { @@ -620,11 +639,11 @@ "license": "MIT" }, "node_modules/@useflows/flowctl": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@useflows/flowctl/-/flowctl-0.1.1.tgz", - "integrity": "sha512-qyZMv7vvBHa61S31mXxID4hAvRTa9bZL1OYsy3I+K+Xsa8TdVFkP0JgYxgJT4QB4fTb7lGkcX04g541VsLFvcQ==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@useflows/flowctl/-/flowctl-0.4.0.tgz", + "integrity": "sha512-uR/PopbhSzvvEwKuqIRKRrT0U3a4sdM/Ky1KCPfPBBPtBrU2fMIZpqNaHbctgChgLcKgtGFQR8E1iR13B0GEpg==", "dev": true, - "license": "UNLICENSED", + "license": "MIT", "dependencies": { "@clack/prompts": "^0.11.0", "@types/semver": "^7.7.0", @@ -1397,21 +1416,25 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, - "node_modules/jsonschema": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", - "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -1962,6 +1985,12 @@ "node": ">=0.6" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -2021,9 +2050,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -2034,9 +2063,9 @@ } }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 172cd05..916c78f 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,14 @@ { "type": "module", "dependencies": { - "@slflows/sdk": "^0.0.8", - "typescript": "^5.8.3", - "@anthropic-ai/sdk": "^0.57.0", + "@anthropic-ai/sdk": "^0.69.0", "@modelcontextprotocol/sdk": "^1.12.1", - "jsonschema": "^1.5.0" + "@slflows/sdk": "^0.5.1", + "typescript": "^5.9.3" }, "devDependencies": { - "@types/node": "^24.0.7", - "@useflows/flowctl": "^0.1.1", + "@types/node": "^24.10.1", + "@useflows/flowctl": "^0.4.0", "prettier": "^3.6.2", "ts-node": "^10.9.2" },