|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright 2025 Steven Roussey <sroussey@gmail.com> |
| 4 | + * SPDX-License-Identifier: Apache-2.0 |
| 5 | + */ |
| 6 | + |
| 7 | +import type { DataPortSchema } from "@workglow/util/schema"; |
| 8 | +import type { ServiceRegistry } from "@workglow/util"; |
| 9 | +import { getInputCompactors } from "@workglow/util"; |
| 10 | +import { getSchemaFormat, getFormatPrefix, getObjectSchema } from "./InputResolver"; |
| 11 | + |
| 12 | +/** |
| 13 | + * Configuration for the input compactor |
| 14 | + */ |
| 15 | +export interface InputCompactorConfig { |
| 16 | + readonly registry: ServiceRegistry; |
| 17 | +} |
| 18 | + |
| 19 | +/** |
| 20 | + * Checks if a schema allows a string variant, recursively checking |
| 21 | + * through oneOf/anyOf nesting (e.g., TypeSingleOrArray(TypeModel(...))). |
| 22 | + */ |
| 23 | +function schemaAllowsString(schema: unknown): boolean { |
| 24 | + if (typeof schema !== "object" || schema === null) return false; |
| 25 | + const s = schema as Record<string, unknown>; |
| 26 | + |
| 27 | + if (s.type === "string") return true; |
| 28 | + |
| 29 | + const variants = (s.oneOf ?? s.anyOf) as unknown[] | undefined; |
| 30 | + if (Array.isArray(variants)) { |
| 31 | + for (const variant of variants) { |
| 32 | + if (schemaAllowsString(variant)) return true; |
| 33 | + } |
| 34 | + } |
| 35 | + |
| 36 | + return false; |
| 37 | +} |
| 38 | + |
| 39 | +/** |
| 40 | + * Compacts resolved inputs by converting instances back to their string IDs. |
| 41 | + * This is the reverse of `resolveSchemaInputs()` — objects with registered |
| 42 | + * compactors are replaced with their string identifier when the schema |
| 43 | + * allows a string variant (oneOf/anyOf with type: "string"). |
| 44 | + * |
| 45 | + * @param input The task input object with resolved values |
| 46 | + * @param schema The task's input/config schema |
| 47 | + * @param config Configuration including the service registry |
| 48 | + * @returns The input with compacted values (objects replaced with string IDs) |
| 49 | + * |
| 50 | + * @example |
| 51 | + * ```typescript |
| 52 | + * // Compact a resolved model config back to its ID |
| 53 | + * const compacted = await compactSchemaInputs( |
| 54 | + * { model: { model_id: "gpt-4", provider: "openai", ... } }, |
| 55 | + * taskSchema, |
| 56 | + * { registry: globalServiceRegistry } |
| 57 | + * ); |
| 58 | + * // compacted.model === "gpt-4" |
| 59 | + * ``` |
| 60 | + */ |
| 61 | +export async function compactSchemaInputs<T extends Record<string, unknown>>( |
| 62 | + input: T, |
| 63 | + schema: DataPortSchema, |
| 64 | + config: InputCompactorConfig |
| 65 | +): Promise<T> { |
| 66 | + if (typeof schema === "boolean") return input; |
| 67 | + |
| 68 | + const properties = schema.properties; |
| 69 | + if (!properties || typeof properties !== "object") return input; |
| 70 | + |
| 71 | + const compactors = getInputCompactors(); |
| 72 | + const compacted: Record<string, unknown> = { ...input }; |
| 73 | + |
| 74 | + for (const [key, propSchema] of Object.entries(properties)) { |
| 75 | + let value = compacted[key]; |
| 76 | + |
| 77 | + const format = getSchemaFormat(propSchema); |
| 78 | + if (format) { |
| 79 | + let compactor = compactors.get(format); |
| 80 | + if (!compactor) { |
| 81 | + const prefix = getFormatPrefix(format); |
| 82 | + compactor = compactors.get(prefix); |
| 83 | + } |
| 84 | + |
| 85 | + if (compactor) { |
| 86 | + // Handle object values: attempt to compact to string ID |
| 87 | + // Only compact if the schema allows a string variant (oneOf/anyOf with type: "string") |
| 88 | + if ( |
| 89 | + value !== null && |
| 90 | + value !== undefined && |
| 91 | + typeof value === "object" && |
| 92 | + !Array.isArray(value) && |
| 93 | + schemaAllowsString(propSchema) |
| 94 | + ) { |
| 95 | + const id = await compactor(value, format, config.registry); |
| 96 | + if (id !== undefined) { |
| 97 | + compacted[key] = id; |
| 98 | + continue; // Replaced with string — skip recursion |
| 99 | + } |
| 100 | + } |
| 101 | + // Handle arrays: compact object elements to strings where possible |
| 102 | + else if (Array.isArray(value)) { |
| 103 | + compacted[key] = await Promise.all( |
| 104 | + value.map(async (item) => { |
| 105 | + if ( |
| 106 | + item !== null && |
| 107 | + item !== undefined && |
| 108 | + typeof item === "object" && |
| 109 | + !Array.isArray(item) |
| 110 | + ) { |
| 111 | + const id = await compactor(item, format, config.registry); |
| 112 | + return id !== undefined ? id : item; |
| 113 | + } |
| 114 | + return item; |
| 115 | + }) |
| 116 | + ); |
| 117 | + continue; |
| 118 | + } |
| 119 | + // String values are already compact — pass through |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + // Recurse into object values that have nested properties in schema |
| 124 | + if ( |
| 125 | + value !== null && |
| 126 | + value !== undefined && |
| 127 | + typeof value === "object" && |
| 128 | + !Array.isArray(value) |
| 129 | + ) { |
| 130 | + const objectSchema = getObjectSchema(propSchema); |
| 131 | + if (objectSchema) { |
| 132 | + compacted[key] = await compactSchemaInputs( |
| 133 | + value as Record<string, unknown>, |
| 134 | + objectSchema as DataPortSchema, |
| 135 | + config |
| 136 | + ); |
| 137 | + } |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + return compacted as T; |
| 142 | +} |
0 commit comments