nt convo const streamModelResponse = () => engineInstance.runWithGenerator({ streamOptions: { enabled: true }, fxAccountToken, tool_choice: "auto", tools: toolsConfig, args: convo, }); // Keep calling until the model finishes without requesting tools while (true) { let pendingToolCalls = null; // 1) First pass: stream tokens; capture any toolCalls for await (const chunk of streamModelResponse()) { // Stream assistant text to the UI if (chunk?.text) { yield chunk.text; } // Capture tool calls (do not echo raw tool plumbing to the user) if (chunk?.toolCalls?.length) { pendingToolCalls = chunk.toolCalls; } } // 2) Watch for tool calls; if none, we are done if (!pendingToolCalls || pendingToolCalls.length === 0) { return; } // 3) Build the assistant tool_calls message exactly as expected by the API const assistantToolMsg = { role: "assistant", tool_calls: pendingToolCalls.map(toolCall => ({ id: toolCall.id, type: "function", function: { name: toolCall.function.name, arguments: toolCall.function.arguments, }, })), }; // 4) Execute each tool locally and create a tool message with the result const toolResultMessages = []; for (const toolCall of pendingToolCalls) { const { id, function: functionSpec } = toolCall; const name = functionSpec?.name || ""; let toolParams = {}; try { toolParams = functionSpec?.arguments ? JSON.parse(functionSpec.arguments) : {}; } catch { toolResultMessages.push({ role: "tool", tool_call_id: id, content: JSON.stringify({ error: "Invalid JSON arguments" }), }); continue; } let result; try { // Call the appropriate tool by name const toolFunc = this.toolMap[name]; if (typeof toolFunc !== "function") { throw new Error(`No such tool: ${name}`); } result = await toolFunc(toolParams); // Create special tool call log message to show in the UI log panel const assistantToolCallLogMsg = { role: "assistant", content: `Tool Call: ${name} with parameters: ${JSON.stringify( toolParams )}`, type: "tool_call_log", result, }; convo.push(assistantToolCallLogMsg); yield assistantToolCallLogMsg; } catch (e) { result = { error: `Tool execution failed: ${String(e)}` }; } toolResultMessages.push({ role: "tool", tool_call_id: id, content: typeof result === "string" ? result : JSON.stringify(result), }); } convo = [...convo, assistantToolMsg, ...toolResultMessages]; } }, /** * Gets the intent of the prompt using a text classification model. * * @param {string} prompt * @returns {string} "search" | "chat" */ async getPromptIntent(query) { try { const engine = await this._createEngine({ featureId: "smart-intent", modelId: "mozilla/mobilebert-query-intent-detection", modelRevision: "v0.2.0", taskName: "text-classification", }); const threshold = 0.6; const cleanedQuery = this._preprocessQuery(query); const resp = await engine.run({ args: [[cleanedQuery]] }); // resp example: [{ label: "chat", score: 0.95 }, { label: "search", score: 0.04 }] if ( resp[0].label.toLowerCase() === "chat" && resp[0].score >= threshold ) { return "chat"; } return "search"; } catch (error) { console.error("Error using intent detection model:", error); throw error; } }, // Helper function for preprocessing text input _preprocessQuery(query) { if (typeof query !== "string") { throw new TypeError( `Expected a string for query preprocessing, but received ${typeof query}` ); } return query.replace(/\?/g, "").trim(); }, }; PK