{"version":3,"file":"tool-call-engine/StructuredOutputsToolCallEngine.mjs","sources":["webpack://@multimodal/agent/./src/tool-call-engine/StructuredOutputsToolCallEngine.ts"],"sourcesContent":["/*\n * Copyright (c) 2025 Bytedance, Inc. and its affiliates.\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport {\n  ToolCallEngine,\n  Tool,\n  PrepareRequestContext,\n  ChatCompletionCreateParams,\n  ChatCompletion,\n  ChatCompletionChunk,\n  MultimodalToolCallResult,\n  AgentSingleLoopReponse,\n  ChatCompletionMessageParam,\n  ChatCompletionMessageToolCall,\n  ParsedModelResponse,\n  StreamProcessingState,\n  StreamChunkResult,\n  FinishReason,\n} from '@multimodal/agent-interface';\nimport { zodToJsonSchema } from '../utils';\nimport { getLogger } from '../utils/logger';\nimport { buildToolCallResultMessages } from './utils';\nimport { jsonrepair } from 'jsonrepair';\n\n/**\n * StructuredOutputsToolCallEngine - Uses structured outputs (JSON Schema) for tool calls\n *\n * This approach instructs the model to return a structured JSON response\n * with tool call information, avoiding the need to parse\n * tool call markers from text content.\n */\nexport class StructuredOutputsToolCallEngine implements ToolCallEngine {\n  private logger = getLogger('StructuredOutputsToolCallEngine');\n\n  /**\n   * Prepare the system prompt with tool definitions\n   *\n   * @param basePrompt The base system prompt\n   * @param tools Available tools for the agent\n   * @returns Enhanced system prompt with tool information\n   */\n  preparePrompt(basePrompt: string, tools: Tool[]): string {\n    if (!tools.length) {\n      return basePrompt;\n    }\n\n    // Define tools section\n    const toolsSection = tools\n      .map((tool) => {\n        const schema = tool.hasJsonSchema?.() ? tool.schema : zodToJsonSchema(tool.schema);\n\n        return `\nTool name: ${tool.name}\nDescription: ${tool.description}\nParameters: ${JSON.stringify(schema, null, 2)}`;\n      })\n      .join('\\n\\n');\n\n    // Define instructions for using structured outputs\n    const structuredOutputInstructions = `\nWhen you need to use a tool:\n1. Respond with a structured JSON object with the following format:\n{\n  \"content\": \"Always include a brief, concise message about what you're doing or what information you're providing. Avoid lengthy explanations.\",\n  \"toolCall\": {\n    \"name\": \"the_exact_tool_name\",\n    \"args\": {\n      // The arguments as required by the tool's parameter schema\n    }\n  }\n}\nIMPORTANT: Always include both \"content\" and \"toolCall\" when using a tool. The \"content\" should be brief but informative.\n\nIf you want to provide a final answer without calling a tool:\n{\n  \"content\": \"Your complete and helpful response to the user\"\n}`;\n\n    // Combine everything\n    return `${basePrompt}\n\nAVAILABLE TOOLS:\n${toolsSection}\n\n${structuredOutputInstructions}`;\n  }\n\n  /**\n   * Prepare the request parameters for the LLM call\n   *\n   * @param context The request context\n   * @returns ChatCompletionCreateParams with structured outputs configuration\n   */\n  prepareRequest(context: PrepareRequestContext): ChatCompletionCreateParams {\n    // Define the schema for structured outputs\n    const responseSchema = {\n      type: 'object',\n      properties: {\n        content: {\n          type: 'string',\n          description: 'Your response text to the user',\n        },\n        toolCall: {\n          type: 'object',\n          properties: {\n            name: {\n              type: 'string',\n              description: 'The exact name of the tool to call',\n            },\n            args: {\n              type: 'object',\n              description: 'The arguments for the tool call',\n            },\n          },\n          required: ['name', 'args'],\n        },\n      },\n      // At least one of these fields must be present\n      anyOf: [{ required: ['content'] }, { required: ['toolCall'] }],\n    };\n\n    // Basic parameters\n    const params: ChatCompletionCreateParams = {\n      messages: context.messages,\n      model: context.model,\n      temperature: context.temperature || 0.7,\n      stream: true,\n    };\n\n    // Add tools if available\n    if (context.tools && context.tools.length > 0) {\n      // Use JSON Schema response format where supported\n      params.response_format = {\n        type: 'json_schema',\n        json_schema: {\n          name: 'agent_response_schema',\n          strict: true,\n          schema: responseSchema,\n        },\n      };\n    }\n\n    return params;\n  }\n\n  /**\n   * Initialize stream processing state for structured outputs\n   * Adding lastExtractedContent to track what's been extracted from JSON for incremental updates\n   */\n  initStreamProcessingState(): StreamProcessingState {\n    return {\n      contentBuffer: '',\n      toolCalls: [],\n      reasoningBuffer: '',\n      finishReason: null,\n      lastParsedContent: '', // Tracks the last successfully extracted content\n    };\n  }\n\n  /**\n   * Process a streaming chunk for structured outputs\n   * Improved to properly handle incremental JSON content extraction\n   */\n  processStreamingChunk(\n    chunk: ChatCompletionChunk,\n    state: StreamProcessingState,\n  ): StreamChunkResult {\n    const delta = chunk.choices[0]?.delta;\n    let content = '';\n    let reasoningContent = '';\n    let hasToolCallUpdate = false;\n\n    // Extract finish reason if present\n    if (chunk.choices[0]?.finish_reason) {\n      state.finishReason = chunk.choices[0].finish_reason;\n    }\n\n    // Process reasoning content if present\n    // @ts-expect-error Not in OpenAI types but present in compatible LLMs\n    if (delta?.reasoning_content) {\n      // @ts-expect-error\n      reasoningContent = delta.reasoning_content;\n      state.reasoningBuffer += reasoningContent;\n    }\n\n    // Process regular content\n    if (delta?.content) {\n      const newContent = delta.content;\n\n      // Accumulate new content in buffer for JSON parsing\n      state.contentBuffer += newContent;\n\n      // Try to extract content from JSON as it comes in\n      if (this.mightBeCollectingJson(state.contentBuffer)) {\n        try {\n          // Try to repair and parse the potentially incomplete JSON\n          const repairedJson = jsonrepair(state.contentBuffer);\n          const parsed = JSON.parse(repairedJson);\n\n          // If we have a valid JSON with content field\n          if (parsed && typeof parsed.content === 'string') {\n            // Calculate only the new incremental content\n            const newExtractedContent = parsed.content.slice(state.lastParsedContent?.length || 0);\n\n            // Only send if we have new incremental content\n            if (newExtractedContent) {\n              content = newExtractedContent;\n              // Update the last parsed content to the full content\n              state.lastParsedContent = parsed.content;\n            }\n\n            // Check for tool call\n            if (parsed.toolCall && !hasToolCallUpdate) {\n              const { name, args } = parsed.toolCall;\n\n              // Create a tool call and update state\n              const toolCall: ChatCompletionMessageToolCall = {\n                id: `call_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,\n                type: 'function',\n                function: {\n                  name,\n                  arguments: JSON.stringify(args),\n                },\n              };\n\n              state.toolCalls = [toolCall];\n              hasToolCallUpdate = true;\n            }\n          }\n        } catch (e) {\n          // JSON parsing failed - this is expected for incomplete JSON\n          // Don't send any content in this case\n          content = '';\n        }\n      } else {\n        // If not collecting JSON, pass through the content directly\n        content = newContent;\n      }\n    }\n\n    return {\n      content,\n      reasoningContent,\n      hasToolCallUpdate,\n      toolCalls: state.toolCalls,\n    };\n  }\n\n  /**\n   * Finalize the stream processing and extract the final response\n   */\n  finalizeStreamProcessing(state: StreamProcessingState): ParsedModelResponse {\n    // One final attempt to parse JSON\n    try {\n      const repairedJson = jsonrepair(state.contentBuffer);\n      const parsed = JSON.parse(repairedJson);\n\n      if (parsed) {\n        if (parsed.toolCall) {\n          // Found a tool call in the JSON\n          const { name, args } = parsed.toolCall;\n\n          // Create a tool call\n          const toolCall: ChatCompletionMessageToolCall = {\n            id: `call_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,\n            type: 'function',\n            function: {\n              name,\n              arguments: JSON.stringify(args),\n            },\n          };\n\n          state.toolCalls = [toolCall];\n\n          // For JSON-based responses, return only the content field\n          if (parsed.content) {\n            state.contentBuffer = parsed.content;\n          } else {\n            state.contentBuffer = '';\n          }\n        } else if (parsed.content) {\n          // No tool call, just content\n          state.contentBuffer = parsed.content;\n        }\n      }\n    } catch (e) {\n      this.logger.warn(`Failed to parse JSON in final processing: ${e}`);\n    }\n\n    const finishReason: FinishReason =\n      state.toolCalls.length > 0 ? 'tool_calls' : state.finishReason || 'stop';\n\n    return {\n      content: state.contentBuffer,\n      reasoningContent: state.reasoningBuffer || undefined,\n      toolCalls: state.toolCalls.length > 0 ? state.toolCalls : undefined,\n      finishReason,\n    };\n  }\n\n  /**\n   * Check if the text might be in the process of building a JSON object\n   */\n  private mightBeCollectingJson(text: string): boolean {\n    // If it contains an opening brace but not a balancing number of closing braces\n    return text.includes('{');\n  }\n\n  /**\n   * Build a historical assistant message for conversation history\n   *\n   * For structured outputs, we maintain the original content without tool_calls\n   * to ensure compatibility with our JSON schema approach.\n   *\n   * @param response The agent's response\n   * @returns Formatted message parameter for conversation history\n   */\n  buildHistoricalAssistantMessage(response: AgentSingleLoopReponse): ChatCompletionMessageParam {\n    // For structured outputs, we never use the tool_calls field\n    // Instead, the JSON structure is already in the content\n    return {\n      role: 'assistant',\n      content: response.content || '',\n    };\n  }\n\n  /**\n   * Build historical tool call result messages for conversation history\n   *\n   * For structured outputs engine, we format results as user messages\n   * to maintain consistency with our JSON schema approach.\n   *\n   * @param results The tool call results\n   * @returns Array of formatted message parameters\n   */\n  buildHistoricalToolCallResultMessages(\n    results: MultimodalToolCallResult[],\n  ): ChatCompletionMessageParam[] {\n    return buildToolCallResultMessages(results, false);\n  }\n}\n"],"names":["StructuredOutputsToolCallEngine","basePrompt","tools","toolsSection","tool","schema","zodToJsonSchema","JSON","structuredOutputInstructions","context","responseSchema","params","chunk","state","_chunk_choices_","_chunk_choices_1","delta","content","reasoningContent","hasToolCallUpdate","newContent","repairedJson","jsonrepair","parsed","_state_lastParsedContent","newExtractedContent","name","args","toolCall","Date","Math","e","finishReason","undefined","text","response","results","buildToolCallResultMessages","getLogger"],"mappings":"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"}