diff --git a/packages/gensx-openai/src/composition.tsx b/packages/gensx-openai/src/composition.tsx index 76db7c35..d014b14f 100644 --- a/packages/gensx-openai/src/composition.tsx +++ b/packages/gensx-openai/src/composition.tsx @@ -61,21 +61,22 @@ type ToolTransformProps = Omit< type ToolTransformReturn = RawCompletionReturn; -// Structured output transform component -type StructuredTransformProps = Omit< +// Updated type to include retry options +type StructuredOutputProps = Omit< ChatCompletionCreateParamsNonStreaming, "stream" | "tools" > & { structuredOutput: GSXStructuredOutput; tools?: GSXTool[]; + retry?: { + maxAttempts?: number; + backoff?: "exponential" | "linear"; + onRetry?: (attempt: number, error: Error, lastResponse?: string) => void; + shouldRetry?: (error: Error, attempt: number) => boolean; + }; }; -type StructuredTransformReturn = T; - -// Add RetryTransformProps type -type RetryTransformProps = StructuredTransformProps & { - maxAttempts?: number; -}; +type StructuredOutputReturn = T; // Types for the composition-based implementation type StreamingProps = Omit< @@ -268,41 +269,75 @@ export const ToolTransform = gsx.Component< ); }); -// Structured output transform component -export const StructuredTransform = gsx.Component< - StructuredTransformProps, - StructuredTransformReturn ->("StructuredTransform", async (props) => { - const { structuredOutput, tools, ...rest } = props; +// Combined structured output component +export const StructuredOutput = gsx.Component< + StructuredOutputProps, + StructuredOutputReturn +>("StructuredOutput", async (props) => { + const { structuredOutput, tools, retry, ...rest } = props; + const maxAttempts = retry?.maxAttempts ?? 3; + let lastError: Error | undefined; + let lastResponse: string | undefined; - // Make initial completion - const completion = await gsx.execute( - , - ); + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + // Add retry context to messages if not first attempt + const messages = [...rest.messages]; + if (attempt > 1) { + messages.push({ + role: "system", + content: `Previous attempt failed: ${lastError?.message}. Please fix the JSON structure and try again.`, + }); + } - const toolCalls = completion.choices[0].message.tool_calls; - // If we have tool calls, execute them and make another completion - if (toolCalls?.length && tools) { - const toolResult = await gsx.execute( - , - ); + // Make initial completion + const completion = await gsx.execute( + , + ); - // Parse and validate the final result - const content = toolResult.choices[0]?.message.content; - if (!content) { - throw new Error("No content returned from OpenAI after tool execution"); - } + const toolCalls = completion.choices[0].message.tool_calls; + // If we have tool calls, execute them and make another completion + if (toolCalls?.length && tools) { + const toolResult = await gsx.execute( + , + ); - try { + // Parse and validate the final result + const content = toolResult.choices[0]?.message.content; + if (!content) { + throw new Error( + "No content returned from OpenAI after tool execution", + ); + } + + lastResponse = content; + const parsed = JSON.parse(content) as unknown; + const validated = structuredOutput.safeParse(parsed); + if (!validated.success) { + throw new Error( + `Invalid structured output: ${validated.error.message}`, + ); + } + return validated.data; + } + + // No tool calls, parse and validate the direct result + const content = completion.choices[0].message.content; + if (!content) { + throw new Error("No content returned from OpenAI"); + } + + lastResponse = content; const parsed = JSON.parse(content) as unknown; const validated = structuredOutput.safeParse(parsed); if (!validated.success) { @@ -312,70 +347,32 @@ export const StructuredTransform = gsx.Component< } return validated.data; } catch (e) { - throw new Error( - `Failed to parse structured output after tool execution: ${e instanceof Error ? e.message : String(e)}`, - ); - } - } - - // No tool calls, parse and validate the direct result - const content = completion.choices[0].message.content; - if (!content) { - throw new Error("No content returned from OpenAI"); - } - - try { - const parsed = JSON.parse(content) as unknown; - const validated = structuredOutput.safeParse(parsed); - if (!validated.success) { - throw new Error(`Invalid structured output: ${validated.error.message}`); - } - return validated.data; - } catch (e) { - throw new Error( - `Failed to parse structured output: ${e instanceof Error ? e.message : String(e)}`, - ); - } -}); + lastError = e instanceof Error ? e : new Error(String(e)); -// Retry transform component for structured output -export const RetryTransform = gsx.Component< - RetryTransformProps, - StructuredTransformReturn ->("RetryTransform", async (props) => { - const { maxAttempts = 3, ...rest } = props; - let lastError: string | undefined; + // Call onRetry callback if provided + retry?.onRetry?.(attempt, lastError, lastResponse); - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - const retryMessage: ChatCompletionMessageParam | undefined = - attempt > 1 - ? { - role: "system", - content: `Previous attempt failed: ${lastError}. Please fix the JSON structure and try again.`, - } - : undefined; - - const messages = [...rest.messages]; - if (retryMessage) { - messages.push(retryMessage); - } - - return await gsx.execute( - , - ); - } catch (e) { - lastError = e instanceof Error ? e.message : String(e); - if (attempt === maxAttempts) { + // Check if we should retry + const shouldRetry = retry?.shouldRetry?.(lastError, attempt) ?? true; + if (!shouldRetry || attempt === maxAttempts) { throw new Error( - `Failed to get valid structured output after ${maxAttempts} attempts. Last error: ${lastError}`, + `Failed to get valid structured output after ${attempt} attempts. Last error: ${lastError.message}`, ); } + + // Apply backoff if specified + if (retry?.backoff) { + const delay = + retry.backoff === "exponential" + ? Math.pow(2, attempt - 1) * 1000 + : attempt * 1000; + await new Promise((resolve) => setTimeout(resolve, delay)); + } } } }); -// The composition-based implementation that matches ChatCompletion's functionality +// Update CompositionCompletion to use the renamed component export const CompositionCompletion = gsx.Component< CompositionCompletionProps, CompositionCompletionReturn @@ -390,7 +387,7 @@ export const CompositionCompletion = gsx.Component< if ("structuredOutput" in props && props.structuredOutput) { const { tools, structuredOutput, ...rest } = props; return ( -