> ## Documentation Index
> Fetch the complete documentation index at: https://connect.watson-orchestrate.ibm.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Working with tools

> Implement and call tools in your agents that comply with the Agent Connect Framework

Tools are a powerful feature that enable AI agents to invoke external APIs and services, thereby extending their capabilities. This guide outlines the implementation and usage of tools within your agents that follow the Agent Connect Framework specification.

Tools allow agents to:

* **Invoke External Services**: Access APIs, databases, and other external services.
* **Perform Specialized Tasks**: Execute tasks requiring specific capabilities.
* **Collaborate**: Share tool results to work together with other agents.

Within the ACF, both tool and tool responses can be shared among agents. This form of observability allows one agent to leverage the work performed by another agent. For instance, a CRM agent might retrieve account information and store details in a tool call response. Subsequently, an Email agent can utilize this information to find critical details, such as an email address for an account.

By sharing tools and their responses, agents can collaborate on sub-tasks while sharing contextual information essential to the overall success of the main task.

## Creating your agent

Before you create your tools, you must first create an agent that will use the tool. The ACF requires at least 2 endpoints to be implemented in your agent:

* `GET /v1/chat`: The [**Chat API**](/acf/chat/chat-completion) enables agents to communicate and collaborate with each other using a standard chat completions style protocol with server-sent events (SSE) for streaming responses. This API supports both stateful and stateless agents.
* `POST /v1/agents`: The [**Agent Discovery**](/acf/discovery/agent-discovery) endpoint enables the identification of available agents and their capabilities within the Agent Connect Framework (ACF), eliminating the need for hardcoded knowledge of their existence.

You can see examples of implemented agents in the [Examples](/examples) page.

Alternatively, for a better integration with **watsonx Orchestrate**, you can use the [**watsonx Orchestrate Agent Development Kit (ADK)**](https://developer.watson-orchestrate.ibm.com/) to develop your agents and tools. To learn how to create your agent by using the ADK, see [Creating agents](https://developer.watson-orchestrate.ibm.com/agents/build_agent).

## Defining your tools

The ACF assumes tools are defined using a JSON schema that specifies the tool's name, description, and parameters, following the OpenAI specification for function calling:

```json theme={null}
{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get the current weather for a location",
    "parameters": {
      "type": "object",
      "properties": {
        "location": {
          "type": "string",
          "description": "The city and state or country (e.g., 'San Francisco, CA')"
        },
        "unit": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"],
          "description": "The unit of temperature"
        }
      },
      "required": ["location"]
    }
  }
}
```

### Key components

* **`name`**: A unique identifier for the tool
* **`description`**: A human-readable description of what the tool does
* **`parameters`**: A JSON Schema object that defines the tool's input parameters
  * **`properties`**: The parameters the tool accepts
  * **`required`**: Which parameters are required

Certain LLMs and frameworks use a different format for tool definitions, tool calls, and tool responses. ACF requires at least these key components to be present in the tool calls.

You can also use the ADK to create tools that are compatible with watsonx Orchestrate agents. For more information, see [Creating Tools](https://developer.watson-orchestrate.ibm.com/tools/create_tool).

## How to implement tools in your agents

Agent frameworks usually implement their own mechanisms to implement tools.

The watsonx Orchestrate ADK uses [Python decorators](https://developer.watson-orchestrate.ibm.com/tools/create_tool#creating-python-based-tools) to implement Python-based tools.

Regardless of the framework that you use, you must implement the tool call in your `/v1/chat` endpoint.

The following example shows how you can implement a tool from scratch, and how to modify your `/v1/chat` endpoint to run your tools.

<Steps>
  <Step>
    First, define the tools that your agent will use. You can use a JSON file, an OpenAPI specification, or even a JSON-like structure in your programming language of choice:

    <CodeGroup>
      ```javascript JS [expandable] theme={null}
      const tools = [
        {
          type: "function",
          function: {
            name: "get_weather",
            description: "Get the current weather for a location",
            parameters: {
              type: "object",
              properties: {
                location: {
                  type: "string",
                  description: "The city and state or country (e.g., 'San Francisco, CA')"
                },
                unit: {
                  type: "string",
                  enum: ["celsius", "fahrenheit"],
                  description: "The unit of temperature"
                }
              },
              required: ["location"]
            }
          }
        },
        {
          type: "function",
          function: {
            name: "search_database",
            description: "Search a database for information",
            parameters: {
              type: "object",
              properties: {
                query: {
                  type: "string",
                  description: "The search query"
                },
                limit: {
                  type: "integer",
                  description: "Maximum number of results to return"
                }
              },
              required: ["query"]
            }
          }
        }
      ];
      ```

      ```python Python [expandable] theme={null}
      tools = [
          {
              "type": "function",
              "function": {
                  "name": "get_weather",
                  "description": "Get the current weather for a location",
                  "parameters": {
                      "type": "object",
                      "properties": {
                          "location": {
                              "type": "string",
                              "description": "The city and state or country (e.g., 'San Francisco, CA')"
                          },
                          "unit": {
                              "type": "string",
                              "enum": ["celsius", "fahrenheit"],
                              "description": "The unit of temperature"
                          }
                      },
                      "required": ["location"]
                  }
              }
          },
          {
              "type": "function",
              "function": {
                  "name": "search_database",
                  "description": "Search a database for information",
                  "parameters": {
                      "type": "object",
                      "properties": {
                          "query": {
                              "type": "string",
                              "description": "The search query"
                          },
                          "limit": {
                              "type": "integer",
                              "description": "Maximum number of results to return"
                          }
                      },
                      "required": ["query"]
                  }
              }
          }
      ]
      ```

      ```go Go [expandable] theme={null}
      type Property struct {
      	Type        string   `json:"type"`
      	Description string   `json:"description"`
      	Enum        []string `json:"enum,omitempty"`
      }

      type Parameters struct {
      	Type       string              `json:"type"`
      	Properties map[string]Property `json:"properties"`
      	Required   []string            `json:"required"`
      }

      type Function struct {
      	Name        string     `json:"name"`
      	Description string     `json:"description"`
      	Parameters  Parameters `json:"parameters"`
      }

      type Tool struct {
      	Type     string   `json:"type"`
      	Function Function `json:"function"`
      }

      var tools = []Tool{
      	{
      		Type: "function",
      		Function: Function{
      			Name:        "get_weather",
      			Description: "Get the current weather for a location",
      			Parameters: Parameters{
      				Type: "object",
      				Properties: map[string]Property{
      					"location": {
      						Type:        "string",
      						Description: "The city and state or country (e.g., 'San Francisco, CA')",
      					},
      					"unit": {
      						Type:        "string",
      						Description: "The unit of temperature",
      						Enum:        []string{"celsius", "fahrenheit"},
      					},
      				},
      				Required: []string{"location"},
      			},
      		},
      	},
      	{
      		Type: "function",
      		Function: Function{
      			Name:        "search_database",
      			Description: "Search a database for information",
      			Parameters: Parameters{
      				Type: "object",
      				Properties: map[string]Property{
      					"query": {
      						Type:        "string",
      						Description: "The search query",
      					},
      					"limit": {
      						Type:        "integer",
      						Description: "Maximum number of results to return",
      					},
      				},
      				Required: []string{"query"},
      			},
      		},
      	},
      }
      ```
    </CodeGroup>
  </Step>

  <Step>
    Next, implement the actual functions that will be called when a tool is invoked:

    <CodeGroup>
      ```javascript JS [expandable] theme={null}
      const toolFunctions = {
        get_weather: async (args) => {
          const { location, unit = "celsius" } = args;
          
          // In a real implementation, this would call a weather API
          // For this example, we'll return mock data
          const weatherData = {
            location,
            temperature: unit === "celsius" ? 22 : 72,
            condition: "sunny",
            humidity: 45,
            unit
          };
          
          return JSON.stringify(weatherData);
        },
        
        search_database: async (args) => {
          const { query, limit = 10 } = args;
          
          // In a real implementation, this would query a database
          // For this example, we'll return mock data
          const results = [
            { id: 1, title: "Result 1", content: "Content for result 1" },
            { id: 2, title: "Result 2", content: "Content for result 2" },
            { id: 3, title: "Result 3", content: "Content for result 3" }
          ].slice(0, limit);
          
          return JSON.stringify({
            query,
            results,
            total: results.length
          });
        }
      };
      ```

      ```python Python [expandable] theme={null}
      import json

      async def get_weather(args):
          location = args.get("location")
          unit = args.get("unit", "celsius")
          temperature = 22 if unit == "celsius" else 72

          weather_data = {
              "location": location,
              "temperature": temperature,
              "condition": "sunny",
              "humidity": 45,
              "unit": unit
          }

          return json.dumps(weather_data)

      async def search_database(args):
          query = args.get("query")
          limit = args.get("limit", 10)

          results = [
              {"id": 1, "title": "Result 1", "content": "Content for result 1"},
              {"id": 2, "title": "Result 2", "content": "Content for result 2"},
              {"id": 3, "title": "Result 3", "content": "Content for result 3"}
          ][:limit]

          return json.dumps({
              "query": query,
              "results": results,
              "total": len(results)
          })

      tool_functions = {
          "get_weather": get_weather,
          "search_database": search_database
      }
      ```

      ```go Go [expandable] theme={null}
      import (
      	"encoding/json"
      	"fmt"
      )

      func getWeather(args map[string]interface{}) (interface{}, error) {
      	location, _ := args["location"].(string)
      	unit, ok := args["unit"].(string)
      	if !ok {
      		unit = "celsius"
      	}

      	temperature := 22
      	if unit == "fahrenheit" {
      		temperature = 72
      	}

      	weatherData := map[string]interface{}{
      		"location":    location,
      		"temperature": temperature,
      		"condition":   "sunny",
      		"humidity":    45,
      		"unit":        unit,
      	}

      	return weatherData, nil
      }

      func searchDatabase(args map[string]interface{}) (interface{}, error) {
      	query, _ := args["query"].(string)
      	limit := 10
      	if l, ok := args["limit"].(float64); ok {
      		limit = int(l)
      	}

      	allResults := []map[string]interface{}{
      		{"id": 1, "title": "Result 1", "content": "Content for result 1"},
      		{"id": 2, "title": "Result 2", "content": "Content for result 2"},
      		{"id": 3, "title": "Result 3", "content": "Content for result 3"},
      	}

      	if limit > len(allResults) {
      		limit = len(allResults)
      	}

      	results := allResults[:limit]

      	return map[string]interface{}{
      		"query":   query,
      		"results": results,
      		"total":   len(results),
      	}, nil
      }

      var toolFunctions = map[string]func(map[string]interface{}) (interface{}, error){
      	"get_weather":     getWeather,
      	"search_database": searchDatabase,
      }
      ```
    </CodeGroup>
  </Step>

  <Step>
    Modify your chat completion endpoint to handle tool calls:

    <CodeGroup>
      ```javascript JS [expandable] theme={null}
      app.post('/v1/chat', async (req, res) => {
        try {
          const { messages, stream = false } = req.body;
          const threadId = req.headers['x-thread-id'] || 'default';
          
          if (stream) {
            // Set up SSE streaming
            res.setHeader('Content-Type', 'text/event-stream');
            res.setHeader('Cache-Control', 'no-cache');
            res.setHeader('Connection', 'keep-alive');
            
            // Process messages and stream response with tool calls
            await streamResponseWithTools(res, messages, threadId, tools, toolFunctions);
          } else {
            // Process messages and return complete response
            const response = await processMessagesWithTools(messages, threadId, tools, toolFunctions);
            res.json(response);
          }
        } catch (error) {
          console.error('Error processing chat request:', error);
          res.status(500).json({ error: 'Internal server error' });
        }
      });
      ```

      ```python Python [expandable] theme={null}
      from fastapi import FastAPI, Request, Header
      from fastapi.responses import JSONResponse, StreamingResponse
      import asyncio

      app = FastAPI()

      @app.post("/v1/chat")
      async def chat_endpoint(request: Request, x_thread_id: str = Header(default="default")):
          try:
              body = await request.json()
              messages = body.get("messages", [])
              stream = body.get("stream", False)

              if stream:
                  async def event_stream():
                      async for chunk in stream_response_with_tools(messages, x_thread_id, tools, tool_functions):
                          yield f"data: {chunk}\n\n"
                  return StreamingResponse(event_stream(), media_type="text/event-stream")
              else:
                  response = await process_messages_with_tools(messages, x_thread_id, tools, tool_functions)
                  return JSONResponse(content=response)
          except Exception as e:
              print("Error processing chat request:", e)
              return JSONResponse(status_code=500, content={"error": "Internal server error"})
      ```

      ```go Go [expandable] theme={null}
      package main

      import (
      	"encoding/json"
      	"net/http"
      )

      type ChatRequest struct {
      	Messages []interface{} `json:"messages"`
      	Stream   bool          `json:"stream"`
      }

      func chatHandler(w http.ResponseWriter, r *http.Request) {
      	var req ChatRequest
      	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
      		http.Error(w, "Invalid request body", http.StatusBadRequest)
      		return
      	}

      	threadID := r.Header.Get("X-Thread-Id")
      	if threadID == "" {
      		threadID = "default"
      	}

      	if req.Stream {
      		w.Header().Set("Content-Type", "text/event-stream")
      		w.Header().Set("Cache-Control", "no-cache")
      		w.Header().Set("Connection", "keep-alive")

      		if err := streamResponseWithTools(w, req.Messages, threadID); err != nil {
      			http.Error(w, "Streaming error", http.StatusInternalServerError)
      		}
      		return
      	}

      	response, err := processMessagesWithTools(req.Messages, threadID)
      	if err != nil {
      		http.Error(w, "Internal server error", http.StatusInternalServerError)
      		return
      	}

      	w.Header().Set("Content-Type", "application/json")
      	json.NewEncoder(w).Encode(response)
      }
      ```
    </CodeGroup>
  </Step>

  <Step>
    Implement the function to process messages and handle tool calls:

    <CodeGroup>
      ```javascript JS [expandable] theme={null}
      // Import the OpenAI SDK
      import OpenAI from 'openai';

      // Initialize the OpenAI client
      const openai = new OpenAI({
        apiKey: process.env.OPENAI_API_KEY, // Get API key from environment variable
      });

      async function processMessagesWithTools(messages, threadId, tools, toolFunctions) {
        try {
          // First, call the OpenAI API with the messages and tools
          const llmResponse = await openai.chat.completions.create({
            model: "gpt-4.1-mini", // Or your preferred model that supports tool calling
            messages: messages,
            tools: tools,
            tool_choice: "auto" // Let the LLM decide when to use tools
          });
          
          // Extract the assistant's response
          const assistantResponse = llmResponse.choices[0].message;
          
          // Check if the LLM decided to call any tools
          if (assistantResponse.tool_calls && assistantResponse.tool_calls.length > 0) {
            console.log(`LLM decided to call ${assistantResponse.tool_calls.length} tools`);
            
            // Create a new messages array with the assistant's response
            const updatedMessages = [...messages, assistantResponse];
            
            // Execute each tool call and add the results to the messages
            for (const toolCall of assistantResponse.tool_calls) {
              const { id, function: { name, arguments: argsString } } = toolCall;
              
              try {
                // Parse the arguments
                const args = JSON.parse(argsString);
                
                // Execute the tool function
                const result = await toolFunctions[name](args);
                
                // Add the tool result to the messages
                updatedMessages.push({
                  role: "tool",
                  tool_call_id: id,
                  name: name,
                  content: result
                });
              } catch (error) {
                console.error(`Error executing tool ${name}:`, error);
                
                // Add an error message as the tool result
                updatedMessages.push({
                  role: "tool",
                  tool_call_id: id,
                  name: name,
                  content: JSON.stringify({ error: error.message })
                });
              }
            }
            
            // Call the LLM again with the updated messages including tool results
            const finalResponse = await openai.chat.completions.create({
              model: "gpt-4-turbo", // Or your preferred model
              messages: updatedMessages,
            });
            
            // Return the final response
            return {
              id: finalResponse.id,
              object: 'chat.completion',
              created: finalResponse.created,
              model: finalResponse.model,
              choices: finalResponse.choices,
              usage: finalResponse.usage
            };
          } else {
            // The LLM didn't call any tools, just return its response
            return llmResponse;
          }
        } catch (error) {
          console.error("Error in processMessagesWithTools:", error);
          throw error;
        }
      }
      ```

      ```python Python [expandable] theme={null}
      import os
      import json
      import openai
      from openai import AsyncOpenAI

      # Initialize OpenAI client
      openai_client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))

      async def process_messages_with_tools(messages, thread_id, tools, tool_functions):
          try:
              # Step 1: Initial call to OpenAI with tools
              llm_response = await openai_client.chat.completions.create(
                  model="gpt-4.1-mini",
                  messages=messages,
                  tools=tools,
                  tool_choice="auto"
              )

              assistant_response = llm_response.choices[0].message

              # Step 2: Check for tool calls
              if hasattr(assistant_response, "tool_calls") and assistant_response.tool_calls:
                  print(f"LLM decided to call {len(assistant_response.tool_calls)} tools")

                  updated_messages = messages + [assistant_response]

                  for tool_call in assistant_response.tool_calls:
                      tool_id = tool_call.id
                      tool_name = tool_call.function.name
                      args_str = tool_call.function.arguments

                      try:
                          args = json.loads(args_str)
                          result = await tool_functionstool_name
                          updated_messages.append({
                              "role": "tool",
                              "tool_call_id": tool_id,
                              "name": tool_name,
                              "content": result
                          })
                      except Exception as e:
                          print(f"Error executing tool {tool_name}:", e)
                          updated_messages.append({
                              "role": "tool",
                              "tool_call_id": tool_id,
                              "name": tool_name,
                              "content": json.dumps({"error": str(e)})
                          })

                  # Step 3: Final call with tool results
                  final_response = await openai_client.chat.completions.create(
                      model="gpt-4-turbo",
                      messages=updated_messages
                  )

                  return {
                      "id": final_response.id,
                      "object": "chat.completion",
                      "created": final_response.created,
                      "model": final_response.model,
                      "choices": final_response.choices,
                      "usage": final_response.usage
                  }

              else:
                  # No tool calls, return original response
                  return llm_response

          except Exception as e:
              print("Error in process_messages_with_tools:", e)
              raise
      ```

      ```go Go [expandable] theme={null}
      package main

      import (
      	"bytes"
      	"encoding/json"
      	"fmt"
      	"net/http"
      	"os"
      	"time"
      )

      type Message struct {
      	Role       string     `json:"role"`
      	Content    string     `json:"content,omitempty"`
      	Name       string     `json:"name,omitempty"`
      	ToolCallID string     `json:"tool_call_id,omitempty"`
      	ToolCalls  []ToolCall `json:"tool_calls,omitempty"`
      }

      type ToolCall struct {
      	ID       string `json:"id"`
      	Function struct {
      		Name      string `json:"name"`
      		Arguments string `json:"arguments"`
      	} `json:"function"`
      }

      type ToolFunction func(map[string]interface{}) (interface{}, error)

      var toolFunctions = map[string]ToolFunction{
      	"get_weather":     getWeather,
      	"search_database": searchDatabase,
      }

      func getWeather(args map[string]interface{}) (interface{}, error) {
      	location, _ := args["location"].(string)
      	unit, ok := args["unit"].(string)
      	if !ok {
      		unit = "celsius"
      	}
      	temperature := 22
      	if unit == "fahrenheit" {
      		temperature = 72
      	}
      	return map[string]interface{}{
      		"location":    location,
      		"temperature": temperature,
      		"condition":   "sunny",
      		"humidity":    45,
      		"unit":        unit,
      	}, nil
      }

      func searchDatabase(args map[string]interface{}) (interface{}, error) {
      	query, _ := args["query"].(string)
      	limit := 10
      	if l, ok := args["limit"].(float64); ok {
      		limit = int(l)
      	}
      	results := []map[string]interface{}{
      		{"id": 1, "title": "Result 1", "content": "Content for result 1"},
      		{"id": 2, "title": "Result 2", "content": "Content for result 2"},
      		{"id": 3, "title": "Result 3", "content": "Content for result 3"},
      	}
      	if limit > len(results) {
      		limit = len(results)
      	}
      	return map[string]interface{}{
      		"query":   query,
      		"results": results[:limit],
      		"total":   limit,
      	}, nil
      }

      func parseToolCalls(raw []interface{}) []ToolCall {
      	var calls []ToolCall
      	for _, item := range raw {
      		data, _ := json.Marshal(item)
      		var call ToolCall
      		json.Unmarshal(data, &call)
      		calls = append(calls, call)
      	}
      	return calls
      }

      func callOpenAI(messages []Message, tools []map[string]interface{}) (map[string]interface{}, error) {
      	payload := map[string]interface{}{
      		"model":    "gpt-4.1-mini",
      		"messages": messages,
      	}
      	if tools != nil {
      		payload["tools"] = tools
      		payload["tool_choice"] = "auto"
      	}
      	body, _ := json.Marshal(payload)
      	req, _ := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(body))
      	req.Header.Set("Authorization", "Bearer "+os.Getenv("OPENAI_API_KEY"))
      	req.Header.Set("Content-Type", "application/json")

      	client := &http.Client{Timeout: 30 * time.Second}
      	resp, err := client.Do(req)
      	if err != nil {
      		return nil, err
      	}
      	defer resp.Body.Close()

      	var result map[string]interface{}
      	json.NewDecoder(resp.Body).Decode(&result)
      	return result, nil
      }

      func processMessagesWithTools(
      	messages []Message,
      	threadID string,
      	tools []map[string]interface{},
      	toolFunctions map[string]ToolFunction,
      ) (map[string]interface{}, error) {
      	llmResponse, err := callOpenAI(messages, tools)
      	if err != nil {
      		return nil, err
      	}

      	assistantMsg := llmResponse["choices"].([]interface{})[0].(map[string]interface{})["message"].(map[string]interface{})
      	toolCalls, hasToolCalls := assistantMsg["tool_calls"].([]interface{})

      	if hasToolCalls && len(toolCalls) > 0 {
      		fmt.Printf("LLM decided to call %d tools\n", len(toolCalls))
      		assistantMessage := Message{
      			Role:      "assistant",
      			ToolCalls: parseToolCalls(toolCalls),
      		}
      		updatedMessages := append(messages, assistantMessage)

      		for _, call := range assistantMessage.ToolCalls {
      			args := make(map[string]interface{})
      			if err := json.Unmarshal([]byte(call.Function.Arguments), &args); err != nil {
      				args = map[string]interface{}{"error": "invalid arguments"}
      			}

      			var result interface{}
      			if toolFunc, ok := toolFunctions[call.Function.Name]; ok {
      				result, err = toolFunc(args)
      				if err != nil {
      					result = map[string]string{"error": err.Error()}
      				}
      			} else {
      				result = map[string]string{"error": "tool not found"}
      			}

      			resultJSON, _ := json.Marshal(result)
      			updatedMessages = append(updatedMessages, Message{
      				Role:       "tool",
      				ToolCallID: call.ID,
      				Name:       call.Function.Name,
      				Content:    string(resultJSON),
      			})
      		}

      		finalResponse, err := callOpenAI(updatedMessages, nil)
      		if err != nil {
      			return nil, err
      		}
      		return finalResponse, nil
      	}

      	return llmResponse, nil
      }

      func main() {
      	messages := []Message{
      		{Role: "user", Content: "What's the weather like in San Francisco?"},
      	}
      	threadID := "example-thread"
      	tools := []map[string]interface{}{
      		{
      			"type": "function",
      			"function": map[string]interface{}{
      				"name":        "get_weather",
      				"description": "Get the current weather for a location",
      				"parameters": map[string]interface{}{
      					"type": "object",
      					"properties": map[string]interface{}{
      						"location": map[string]interface{}{
      							"type":        "string",
      							"description": "The city and state or country (e.g., 'San Francisco, CA')",
      						},
      						"unit": map[string]interface{}{
      							"type":        "string",
      							"enum":        []string{"celsius", "fahrenheit"},
      							"description": "The unit of temperature",
      						},
      					},
      					"required": []string{"location"},
      				},
      			},
      		},
      	}

      	response, err := processMessagesWithTools(messages, threadID, tools, toolFunctions)
      	if err != nil {
      		fmt.Println("Error:", err)
      		return
      	}

      	responseJSON, _ := json.MarshalIndent(response, "", "  ")
      	fmt.Println(string(responseJSON))
      }

      ```
    </CodeGroup>
  </Step>

  <Step>
    Implement the function to stream responses with tool calls:

    <CodeGroup>
      ```javascript JS [expandable] theme={null}
      async function streamResponseWithTools(res, messages, threadId, tools, toolFunctions) {
        try {
          // First, send a thinking step
          const thinkingStep = {
            id: `step-${Math.random().toString(36).substring(2, 15)}`,
            object: 'thread.run.step.delta',
            thread_id: threadId,
            model: 'agent-model',
            created: Math.floor(Date.now() / 1000),
            choices: [
              {
                delta: {
                  role: 'assistant',
                  step_details: {
                    type: 'thinking',
                    content: 'Analyzing the request and determining if tools are needed...'
                  }
                }
              }
            ]
          };
          
          res.write(`event: thread.run.step.delta\n`);
          res.write(`data: ${JSON.stringify(thinkingStep)}\n\n`);
          
          // Call the OpenAI API with streaming enabled
          const stream = await openai.chat.completions.create({
            model: "gpt-4.1-mini", // Using the same model as in processMessagesWithTools
            messages: messages,
            tools: tools,
            tool_choice: "auto", // Let the LLM decide when to use tools
            stream: true
          });
          
          let assistantMessage = { role: "assistant", content: "", tool_calls: [] };
          let currentToolCall = null;
          
          // Process the stream
          for await (const chunk of stream) {
            const delta = chunk.choices[0]?.delta;
            
            // If there's content in the delta, add it to the assistant message
            if (delta.content) {
              assistantMessage.content += delta.content;
              
              // Stream the content chunk
              const messageDelta = {
                id: `msg-${Math.random().toString(36).substring(2, 15)}`,
                object: 'thread.message.delta',
                thread_id: threadId,
                model: chunk.model,
                created: Math.floor(Date.now() / 1000),
                choices: [
                  {
                    delta: {
                      role: 'assistant',
                      content: delta.content
                    }
                  }
                ]
              };
              
              res.write(`event: thread.message.delta\n`);
              res.write(`data: ${JSON.stringify(messageDelta)}\n\n`);
            }
            
            // If there's a tool call in the delta, process it
            if (delta.tool_calls && delta.tool_calls.length > 0) {
              const toolCallDelta = delta.tool_calls[0];
              
              // If this is a new tool call, initialize it
              if (toolCallDelta.index === 0 && toolCallDelta.id) {
                currentToolCall = {
                  id: toolCallDelta.id,
                  type: "function",
                  function: {
                    name: "",
                    arguments: ""
                  }
                };
                assistantMessage.tool_calls.push(currentToolCall);
              }
              
              // Update the current tool call with the delta
              if (currentToolCall) {
                if (toolCallDelta.function?.name) {
                  currentToolCall.function.name = toolCallDelta.function.name;
                }
                
                if (toolCallDelta.function?.arguments) {
                  currentToolCall.function.arguments += toolCallDelta.function.arguments;
                }
              }
            }
            
            // If this is the end of the completion, check for tool calls
            if (chunk.choices[0]?.finish_reason === "tool_calls") {
              // Stream a tool call step for each tool call
              for (const toolCall of assistantMessage.tool_calls) {
                const toolCallStep = {
                  id: `step-${Math.random().toString(36).substring(2, 15)}`,
                  object: 'thread.run.step.delta',
                  thread_id: threadId,
                  model: chunk.model,
                  created: Math.floor(Date.now() / 1000),
                  choices: [
                    {
                      delta: {
                        role: 'assistant',
                        step_details: {
                          type: 'tool_calls',
                          tool_calls: [
                            {
                              id: toolCall.id,
                              name: toolCall.function.name,
                              args: JSON.parse(toolCall.function.arguments)
                            }
                          ]
                        }
                      }
                    }
                  ]
                };
                
                res.write(`event: thread.run.step.delta\n`);
                res.write(`data: ${JSON.stringify(toolCallStep)}\n\n`);
              }
              
              // Execute each tool call and stream the results
              const updatedMessages = [...messages, assistantMessage];
              
              for (const toolCall of assistantMessage.tool_calls) {
                try {
                  const { id, function: { name, arguments: argsString } } = toolCall;
                  const args = JSON.parse(argsString);
                  
                  // Execute the tool function
                  const result = await toolFunctions[name](args);
                  
                  // Stream the tool response
                  const toolResponseStep = {
                    id: `step-${Math.random().toString(36).substring(2, 15)}`,
                    object: 'thread.run.step.delta',
                    thread_id: threadId,
                    model: chunk.model,
                    created: Math.floor(Date.now() / 1000),
                    choices: [
                      {
                        delta: {
                          role: 'assistant',
                          step_details: {
                            type: 'tool_response',
                            content: result,
                            name: name,
                            tool_call_id: id
                          }
                        }
                      }
                    ]
                  };
                  
                  res.write(`event: thread.run.step.delta\n`);
                  res.write(`data: ${JSON.stringify(toolResponseStep)}\n\n`);
                  
                  // Add the tool result to the messages
                  updatedMessages.push({
                    role: "tool",
                    tool_call_id: id,
                    name: name,
                    content: result
                  });
                } catch (error) {
                  console.error(`Error executing tool ${toolCall.function.name}:`, error);
                  
                  // Stream an error response
                  const errorResponseStep = {
                    id: `step-${Math.random().toString(36).substring(2, 15)}`,
                    object: 'thread.run.step.delta',
                    thread_id: threadId,
                    model: chunk.model,
                    created: Math.floor(Date.now() / 1000),
                    choices: [
                      {
                        delta: {
                          role: 'assistant',
                          step_details: {
                            type: 'tool_response',
                            content: JSON.stringify({ error: error.message }),
                            name: toolCall.function.name,
                            tool_call_id: toolCall.id
                          }
                        }
                      }
                    ]
                  };
                  
                  res.write(`event: thread.run.step.delta\n`);
                  res.write(`data: ${JSON.stringify(errorResponseStep)}\n\n`);
                  
                  // Add the error result to the messages
                  updatedMessages.push({
                    role: "tool",
                    tool_call_id: toolCall.id,
                    name: toolCall.function.name,
                    content: JSON.stringify({ error: error.message })
                  });
                }
              }
              
              // Call the LLM again with the updated messages including tool results
              const finalStream = await openai.chat.completions.create({
                model: "gpt-4.1-mini", // Using the same model as in the first call
                messages: updatedMessages,
                stream: true
              });
              
              // Stream the final response
              for await (const finalChunk of finalStream) {
                if (finalChunk.choices[0]?.delta?.content) {
                  const messageDelta = {
                    id: `msg-${Math.random().toString(36).substring(2, 15)}`,
                    object: 'thread.message.delta',
                    thread_id: threadId,
                    model: finalChunk.model,
                    created: Math.floor(Date.now() / 1000),
                    choices: [
                      {
                        delta: {
                          role: 'assistant',
                          content: finalChunk.choices[0].delta.content
                        }
                      }
                    ]
                  };
                  
                  res.write(`event: thread.message.delta\n`);
                  res.write(`data: ${JSON.stringify(messageDelta)}\n\n`);
                }
              }
            }
          }
          
          // End the stream
          res.end();
        } catch (error) {
          console.error("Error in streamResponseWithTools:", error);
          
          // Send an error message
          const errorMessage = {
            id: `error-${Math.random().toString(36).substring(2, 15)}`,
            object: 'thread.message.delta',
            thread_id: threadId,
            model: 'agent-model',
            created: Math.floor(Date.now() / 1000),
            choices: [
              {
                delta: {
                  role: 'assistant',
                  content: `An error occurred: ${error.message}`
                }
              }
            ]
          };
          
          res.write(`event: thread.message.delta\n`);
          res.write(`data: ${JSON.stringify(errorMessage)}\n\n`);
          res.end();
        }
      }

      // Helper function to split text into chunks
      function splitIntoChunks(text, chunkSize = 10) {
        const words = text.split(' ');
        const chunks = [];
        let currentChunk = [];
        
        for (const word of words) {
          currentChunk.push(word);
          if (currentChunk.length >= chunkSize) {
            chunks.push(currentChunk.join(' '));
            currentChunk = [];
          }
        }
        
        if (currentChunk.length > 0) {
          chunks.push(currentChunk.join(' '));
        }
        
        return chunks;
      }
      ```

      ```python Python [expandable] theme={null}
      import json
      import time
      import random
      import string
      import asyncio
      from openai import AsyncOpenAI # openai library v1.0+

      # --- Helper Functions ---
      async def _send_sse(res, event_type: str, data: dict):
          """Helper to send a Server-Sent Event."""
          payload = f"event: {event_type}\ndata: {json.dumps(data)}\n\n"
          if hasattr(res, 'write') and callable(res.write):
              try:
                  if asyncio.iscoroutinefunction(res.write):
                      await res.write(payload)
                  else:
                      res.write(payload) # For sync write methods, though res should be async
              except Exception as e:
                  print(f"Error writing to SSE stream: {e}") # Fallback logging
          else: # Fallback for testing if res object is not a proper stream writer
              print(payload, end="")

      def _generate_id(prefix: str = "id_") -> str:
          """Generates a short random ID."""
          return f"{prefix}{''.join(random.choices(string.ascii_lowercase + string.digits, k=10))}"

      # --- Main Streaming Function ---
      async def stream_openai_with_tools(
          res,  # Response object with async write method
          messages: list,
          tools: list | None,
          tool_functions: dict,  # Map tool name to Python function (sync or async)
          openai_client: AsyncOpenAI,
          model_name: str = "gpt-4o-mini",
          thread_id: str | None = None, # Optional for event context
      ):
          """
          Streams OpenAI responses, handles tool calls, and sends simplified SSE events.

          SSE Event Types:
          - `assistant_thinking`: Signals the start of processing.
          - `text_delta`: Streams parts of the assistant's text response.
          - `tool_call_start`: Signals a tool call is being initiated by the assistant.
          - `tool_result`: Provides the output of an executed tool.
          - `stream_error`: Indicates an error occurred during the process.
          - `stream_end`: Signals the completion or failure of the stream.
          """
          current_messages = list(messages)
          run_id = _generate_id("run_")
          event_context = {"thread_id": thread_id, "run_id": run_id, "timestamp": int(time.time())}

          try:
              await _send_sse(res, "assistant_thinking", {**event_context})

              # --- First API Call ---
              stream_params = {
                  "model": model_name, "messages": current_messages, "stream": True
              }
              if tools:
                  stream_params["tools"] = tools
                  stream_params["tool_choice"] = "auto"
              
              stream = await openai_client.chat.completions.create(**stream_params)

              assistant_response_content = ""
              tool_call_aggregators = {}  # {index: {"id": str, "name": str, "arguments": str}}
              completed_tool_calls = [] # [{"id": str, "type": "function", "function": {...}}]
              finish_reason = None

              async for chunk in stream:
                  delta = chunk.choices[0].delta if chunk.choices and chunk.choices[0] else None
                  if not delta: continue

                  if delta.content:
                      assistant_response_content += delta.content
                      await _send_sse(res, "text_delta", {**event_context, "content": delta.content})

                  if delta.tool_calls:
                      for tc_chunk in delta.tool_calls:
                          idx = tc_chunk.index
                          if idx not in tool_call_aggregators:
                              tool_call_aggregators[idx] = {"id": tc_chunk.id, "name": "", "arguments": ""}
                          if tc_chunk.id and not tool_call_aggregators[idx]["id"]: # Update if ID was None
                               tool_call_aggregators[idx]["id"] = tc_chunk.id
                          if tc_chunk.function:
                              if tc_chunk.function.name: tool_call_aggregators[idx]["name"] += tc_chunk.function.name
                              if tc_chunk.function.arguments: tool_call_aggregators[idx]["arguments"] += tc_chunk.function.arguments
                  
                  if chunk.choices[0].finish_reason:
                      finish_reason = chunk.choices[0].finish_reason
                      break # Exit stream processing once finish_reason is known
              
              # Finalize aggregated tool calls
              for idx in sorted(tool_call_aggregators.keys()):
                  agg = tool_call_aggregators[idx]
                  final_id = agg["id"] if agg["id"] else _generate_id(f"call_{idx}_") # Ensure ID
                  completed_tool_calls.append({
                      "id": final_id, "type": "function",
                      "function": {"name": agg["name"], "arguments": agg["arguments"]},
                  })

              # --- Append Assistant's Turn ---
              assistant_message = {"role": "assistant", "content": assistant_response_content or None}
              if completed_tool_calls:
                  assistant_message["tool_calls"] = completed_tool_calls
              current_messages.append(assistant_message)

              # --- Tool Execution Phase ---
              if finish_reason == "tool_calls" and completed_tool_calls:
                  for tool_call in completed_tool_calls:
                      tool_name = tool_call["function"]["name"]
                      tool_id = tool_call["id"]
                      tool_args_str = tool_call["function"]["arguments"]

                      await _send_sse(res, "tool_call_start", {
                          **event_context, "id": tool_id, "name": tool_name, 
                          "arguments_str": tool_args_str, "timestamp": int(time.time())
                      })
                      output, is_error = "", False
                      try:
                          args = json.loads(tool_args_str) if tool_args_str else {}
                          func = tool_functions.get(tool_name)
                          if not func: raise ValueError(f"Tool '{tool_name}' not found.")
                          
                          result = await func(**args) if asyncio.iscoroutinefunction(func) else func(**args)
                          output = str(result) if not isinstance(result, str) else result
                      except Exception as e:
                          output = f"Error in tool '{tool_name}': {type(e).__name__} - {str(e)}"
                          is_error = True
                      
                      await _send_sse(res, "tool_result", {
                          **event_context, "id": tool_id, "name": tool_name, 
                          "output": output, "is_error": is_error, "timestamp": int(time.time())
                      })
                      current_messages.append({
                          "role": "tool", "tool_call_id": tool_id, 
                          "name": tool_name, "content": output
                      })

                  # --- Second API Call after tools ---
                  final_stream = await openai_client.chat.completions.create(
                      model=model_name, messages=current_messages, stream=True
                  )
                  async for chunk in final_stream:
                      if chunk.choices and chunk.choices[0].delta and chunk.choices[0].delta.content:
                          await _send_sse(res, "text_delta", {
                              **event_context, "content": chunk.choices[0].delta.content,
                              "timestamp": int(time.time())
                          })
              
              await _send_sse(res, "stream_end", {**event_context, "status": "completed", "timestamp": int(time.time())})

          except Exception as e:
              error_message = f"{type(e).__name__}: {str(e)}"
              import traceback
              print(f"Error in stream_openai_with_tools: {error_message}\n{traceback.format_exc()}")
              try:
                  await _send_sse(res, "stream_error", {**event_context, "message": error_message, "timestamp": int(time.time())})
                  await _send_sse(res, "stream_end", {**event_context, "status": "failed", "timestamp": int(time.time())})
              except Exception as write_err: # If sending error itself fails
                  print(f"Critical: Failed to send error to stream: {write_err}")
          finally:
              if hasattr(res, 'aclose') and callable(res.aclose): await res.aclose()
              elif hasattr(res, 'end') and callable(res.end): # For sync compatibility if any
                  if asyncio.iscoroutinefunction(res.end): await res.end()
                  else: res.end()

      # --- Example Usage ---
      class MockResponseSSE: # For testing without a web framework
          async def write(self, data: str): print(data, end="")
          async def aclose(self): print("\n--- MOCK STREAM CLOSED ---")

      async def example_tool_weather(location: str, unit: str = "celsius"):
          """Simulates fetching weather. Can be async or sync."""
          await asyncio.sleep(0.1) # Simulate network latency
          return json.dumps({"location": location, "temperature": random.randint(0,30), "unit": unit})

      async def run_example():
          # IMPORTANT: Set OPENAI_API_KEY or initialize client directly
          try:
              client = AsyncOpenAI() # Expects OPENAI_API_KEY in env
          except Exception as e:
              print(f"OpenAI client init failed: {e}. Ensure OPENAI_API_KEY is set.")
              return

          mock_res = MockResponseSSE()
          user_messages = [{"role": "user", "content": "What's the weather in Tokyo and London?"}]
          example_tools = [{
              "type": "function", "function": {
                  "name": "get_weather", "description": "Get current weather for a location.",
                  "parameters": {"type": "object", "properties": {
                          "location": {"type": "string", "description": "City name"},
                          "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius"}
                      },"required": ["location"]
                  }}}]
          example_tool_fns = {"get_weather": example_tool_weather}

          print("Starting concise streaming example (output are SSE events)...\n")
          await stream_openai_with_tools(
              res=mock_res, messages=user_messages, tools=example_tools,
              tool_functions=example_tool_fns, openai_client=client,
              model_name="gpt-4o-mini", # Or "gpt-3.5-turbo" for simpler tool use
              thread_id="thread_example_concise_001"
          )

      if __name__ == "__main__":
          # To run this example:
          # 1. Ensure OPENAI_API_KEY environment variable is set.
          # 2. pip install openai asyncio
          # 3. Uncomment the line below.
          # asyncio.run(run_example())
          print("To run the example, uncomment `asyncio.run(run_example())` in the __main__ block.")
          print("Ensure your OPENAI_API_KEY is set as an environment variable.")
      ```

      ```go Go [expandable] theme={null}
      package main

      import (
      	"context"
      	"crypto/rand"
      	"encoding/hex"
      	"encoding/json"
      	"errors"
      	"fmt"
      	"io"
      	"log"
      	"net/http"
      	"os"
      	"strings"
      	"time"

      	openai "github.com/sashabaranov/go-openai"
      )

      // --- SSE Helper ---

      // EventData is a generic map for SSE data payloads
      type EventData map[string]interface{}

      func sendSSE(w http.ResponseWriter, flusher http.Flusher, eventType string, data EventData) error {
      	// Add a server-side timestamp to all events
      	data["server_timestamp_unix"] = time.Now().Unix()
      	jsonData, err := json.Marshal(data)
      	if err != nil {
      		log.Printf("Error marshalling SSE data: %v", err)
      		return fmt.Errorf("marshalling SSE data: %w", err)
      	}

      	if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", eventType, jsonData); err != nil {
      		log.Printf("Error writing SSE data to response: %v", err)
      		return fmt.Errorf("writing SSE event: %w", err)
      	}
      	if flusher != nil { // Ensure flusher is not nil before flushing
      		flusher.Flush()
      	}
      	return nil
      }

      // --- ID Generation ---

      func generateID(prefix string) string {
      	bytes := make([]byte, 8) // 8 bytes = 16 hex chars
      	if _, err := rand.Read(bytes); err != nil {
      		// Fallback for the rare case rand.Read fails
      		return fmt.Sprintf("%s%d", prefix, time.Now().UnixNano())
      	}
      	return prefix + hex.EncodeToString(bytes)
      }

      // --- Core Structs and Types ---

      // ToolFunction defines the signature for our Go tool functions
      type ToolFunction func(ctx context.Context, arguments json.RawMessage) (string, error)

      // StreamerConfig holds necessary configurations for the streamer
      type StreamerConfig struct {
      	OpenAIClient  *openai.Client
      	ModelName     string
      	Tools         []openai.Tool // OpenAI tool definitions
      	ToolFunctions map[string]ToolFunction
      }

      // AggregatedToolCall is used to piece together tool call information from stream deltas
      type AggregatedToolCall struct {
      	Index     int    // Original index from OpenAI, useful for ordering
      	ID        string // Tool call ID
      	Name      string // Function name
      	Arguments string // Accumulated arguments string
      }

      // --- HTTP Handler and Streaming Logic ---

      func streamOpenAIWithToolsHandler(config *StreamerConfig) http.HandlerFunc {
      	return func(w http.ResponseWriter, r *http.Request) {
      		ctx := r.Context() // Use request context for cancellation

      		w.Header().Set("Content-Type", "text/event-stream")
      		w.Header().Set("Cache-Control", "no-cache")
      		w.Header().Set("Connection", "keep-alive")
      		w.Header().Set("Access-Control-Allow-Origin", "*") // For local testing
      		w.Header().Set("Access-Control-Allow-Headers", "Content-Type") // Allow Content-Type header for POST

      		flusher, ok := w.(http.Flusher)
      		if !ok {
      			http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
      			return
      		}

      		runID := generateID("run_")
      		threadID := r.URL.Query().Get("thread_id") // Example: get thread_id from query
      		if threadID == "" {
      			threadID = generateID("thread_")
      		}

      		eventCtxBase := EventData{"run_id": runID, "thread_id": threadID}

      		var requestBody struct {
      			Messages []openai.ChatCompletionMessage `json:"messages"`
      		}
      		// Default message if parsing fails or body is empty/GET request
      		defaultUserContent := "What's the weather in Berlin and Paris?"
      		if r.Method == http.MethodPost && r.Body != nil && r.Body != http.NoBody {
      			decoder := json.NewDecoder(r.Body)
      			if err := decoder.Decode(&requestBody); err != nil && !errors.Is(err, io.EOF) { // Allow empty body for POST if not strictly required
      				log.Printf("Error decoding request body: %v", err)
      				sendSSE(w, flusher, "stream_error", EventData{**&eventCtxBase, "message": "Invalid request body: " + err.Error()})
      				sendSSE(w, flusher, "stream_end", EventData{**&eventCtxBase, "status": "failed"})
      				return
      			}
      			defer r.Body.Close()
      		}

      		if len(requestBody.Messages) == 0 {
      			// Use default content if no messages were decoded (e.g. GET request or empty POST body)
      			requestBody.Messages = []openai.ChatCompletionMessage{
      				{Role: openai.ChatMessageRoleUser, Content: defaultUserContent},
      			}
      			log.Printf("No messages in request body or GET request, using default: '%s'", defaultUserContent)
      		}


      		currentMessages := requestBody.Messages

      		sendSSE(w, flusher, "assistant_thinking", eventCtxBase)

      		var finalAssistantContent strings.Builder
      		toolCallAggregators := make(map[int]*AggregatedToolCall)
      		var finishReason openai.FinishReason

      		req := openai.ChatCompletionRequest{
      			Model:    config.ModelName,
      			Messages: currentMessages,
      			Stream:   true,
      		}
      		if len(config.Tools) > 0 {
      			req.Tools = config.Tools
      			req.ToolChoice = "auto"
      		}

      		stream, err := config.OpenAIClient.CreateChatCompletionStream(ctx, req)
      		if err != nil {
      			log.Printf("Error creating chat completion stream: %v", err)
      			sendSSE(w, flusher, "stream_error", EventData{**&eventCtxBase, "message": "Failed to start stream with OpenAI: " + err.Error()})
      			sendSSE(w, flusher, "stream_end", EventData{**&eventCtxBase, "status": "failed"})
      			return
      		}
      		defer stream.Close()

      		for {
      			response, streamErr := stream.Recv()
      			if errors.Is(streamErr, io.EOF) {
      				break
      			}
      			if streamErr != nil {
      				log.Printf("Error receiving stream data: %v", streamErr)
      				sendSSE(w, flusher, "stream_error", EventData{**&eventCtxBase, "message": "Stream error: " + streamErr.Error()})
      				sendSSE(w, flusher, "stream_end", EventData{**&eventCtxBase, "status": "failed"})
      				return
      			}

      			if len(response.Choices) == 0 {
      				continue
      			}
      			delta := response.Choices[0].Delta
      			if response.Choices[0].FinishReason != "" { // Capture finish reason if present in this chunk
      				finishReason = response.Choices[0].FinishReason
      			}


      			if delta.Content != "" {
      				finalAssistantContent.WriteString(delta.Content)
      				sendSSE(w, flusher, "text_delta", EventData{**&eventCtxBase, "content": delta.Content})
      			}

      			if len(delta.ToolCalls) > 0 {
      				for _, toolCallDelta := range delta.ToolCalls {
      					idx := 0
      					if toolCallDelta.Index != nil {
      						idx = *toolCallDelta.Index
      					}

      					if _, exists := toolCallAggregators[idx]; !exists {
      						toolCallAggregators[idx] = &AggregatedToolCall{
      							Index: idx,
      							ID:    toolCallDelta.ID,
      						}
      					}
      					if toolCallDelta.ID != "" && toolCallAggregators[idx].ID == "" {
      						toolCallAggregators[idx].ID = toolCallDelta.ID
      					}
      					if toolCallDelta.Function.Name != "" {
      						toolCallAggregators[idx].Name += toolCallDelta.Function.Name
      					}
      					if toolCallDelta.Function.Arguments != "" {
      						toolCallAggregators[idx].Arguments += toolCallDelta.Function.Arguments
      					}
      				}
      			}
      			if finishReason != "" { // If finish reason was set from this chunk, exit loop
      				break
      			}
      		}

      		assistantMessage := openai.ChatCompletionMessage{
      			Role:    openai.ChatMessageRoleAssistant,
      			Content: finalAssistantContent.String(),
      		}
      		var completedToolCalls []openai.ToolCall
      		if len(toolCallAggregators) > 0 {
      			for _, agg := range toolCallAggregators {
      				if agg.ID == "" {
      					agg.ID = generateID(fmt.Sprintf("call_%d_", agg.Index))
      				}
      				completedToolCalls = append(completedToolCalls, openai.ToolCall{
      					ID:   agg.ID,
      					Type: openai.ToolTypeFunction,
      					Function: openai.FunctionCall{
      						Name:      agg.Name,
      						Arguments: agg.Arguments,
      					},
      				})
      			}
      			assistantMessage.ToolCalls = completedToolCalls
      		}
      		currentMessages = append(currentMessages, assistantMessage)

      		if finishReason == openai.FinishReasonToolCalls && len(completedToolCalls) > 0 {
      			for _, tc := range completedToolCalls {
      				sendSSE(w, flusher, "tool_call_start", EventData{
      					**&eventCtxBase,
      					"id":            tc.ID,
      					"name":          tc.Function.Name,
      					"arguments_str": tc.Function.Arguments,
      				})

      				toolFunc, exists := config.ToolFunctions[tc.Function.Name]
      				var result string
      				var toolErr error
      				isError := false

      				if !exists {
      					result = fmt.Sprintf("Error: Tool '%s' not found.", tc.Function.Name)
      					toolErr = errors.New(result)
      					isError = true
      				} else {
      					rawArgs := json.RawMessage(tc.Function.Arguments)
      					result, toolErr = toolFunc(ctx, rawArgs) // Pass context to tool function
      					if toolErr != nil {
      						result = fmt.Sprintf("Error executing tool '%s': %v", tc.Function.Name, toolErr)
      						isError = true
      					}
      				}

      				sendSSE(w, flusher, "tool_result", EventData{
      					**&eventCtxBase,
      					"id":       tc.ID,
      					"name":     tc.Function.Name,
      					"output":   result,
      					"is_error": isError,
      				})
      				currentMessages = append(currentMessages, openai.ChatCompletionMessage{
      					Role:       openai.ChatMessageRoleTool,
      					ToolCallID: tc.ID,
      					Name:       tc.Function.Name,
      					Content:    result,
      				})
      			}

      			finalReq := openai.ChatCompletionRequest{
      				Model:    config.ModelName,
      				Messages: currentMessages,
      				Stream:   true,
      			}
      			finalStream, streamErr := config.OpenAIClient.CreateChatCompletionStream(ctx, finalReq)
      			if streamErr != nil {
      				log.Printf("Error creating final chat completion stream: %v", streamErr)
      				sendSSE(w, flusher, "stream_error", EventData{**&eventCtxBase, "message": "Failed to start final stream: " + streamErr.Error()})
      				sendSSE(w, flusher, "stream_end", EventData{**&eventCtxBase, "status": "failed"})
      				return
      			}
      			defer finalStream.Close()

      			for {
      				response, streamErr := finalStream.Recv()
      				if errors.Is(streamErr, io.EOF) {
      					break
      				}
      				if streamErr != nil {
      					log.Printf("Error receiving final stream data: %v", streamErr)
      					sendSSE(w, flusher, "stream_error", EventData{**&eventCtxBase, "message": "Final stream error: " + streamErr.Error()})
      					sendSSE(w, flusher, "stream_end", EventData{**&eventCtxBase, "status": "failed"})
      					return
      				}
      				if len(response.Choices) > 0 && response.Choices[0].Delta.Content != "" {
      					sendSSE(w, flusher, "text_delta", EventData{**&eventCtxBase, "content": response.Choices[0].Delta.Content})
      				}
      				if response.Choices[0].FinishReason != "" {
      					break
      				}
      			}
      		}
      		sendSSE(w, flusher, "stream_end", EventData{**&eventCtxBase, "status": "completed"})
      	}
      }

      // --- Example Tool Function ---
      func getWeather(ctx context.Context, arguments json.RawMessage) (string, error) {
      	select { // Simulate work and check for context cancellation
      	case <-time.After(100 * time.Millisecond):
      	case <-ctx.Done():
      		log.Printf("getWeather context cancelled")
      		return "", ctx.Err()
      	}

      	var params struct {
      		Location string `json:"location"`
      		Unit     string `json:"unit,omitempty"`
      	}
      	if err := json.Unmarshal(arguments, &params); err != nil {
      		return "", fmt.Errorf("invalid arguments format for getWeather: %w", err)
      	}
      	if params.Unit == "" {
      		params.Unit = "celsius"
      	}

      	temp := 20 // Default
      	condition := "partly cloudy"
      	if strings.Contains(strings.ToLower(params.Location), "berlin") {
      		temp = 15
      		condition = "cloudy"
      	} else if strings.Contains(strings.ToLower(params.Location), "paris") {
      		temp = 18
      		condition = "sunny"
      	} else if strings.Contains(strings.ToLower(params.Location), "london") {
      		temp = 12
      		condition = "rainy"
      	}


      	weatherData := map[string]interface{}{
      		"location":    params.Location,
      		"temperature": temp,
      		"unit":        params.Unit,
      		"condition":   condition,
      	}
      	resultBytes, err := json.Marshal(weatherData)
      	if err != nil {
      		return "", fmt.Errorf("failed to marshal weather data: %w", err)
      	}
      	log.Printf("Tool getWeather called for %s, returning: %s", params.Location, string(resultBytes))
      	return string(resultBytes), nil
      }

      // --- Main Application Setup ---
      func main() {
      	apiKey := os.Getenv("OPENAI_API_KEY")
      	if apiKey == "" {
      		log.Fatal("Error: OPENAI_API_KEY environment variable not set.")
      	}
      	client := openai.NewClient(apiKey)

      	tools := []openai.Tool{
      		{
      			Type: openai.ToolTypeFunction,
      			Function: &openai.FunctionDefinition{
      				Name:        "get_weather",
      				Description: "Get the current weather in a given location.",
      				Parameters: &openai.JSONSchemaDefinition{
      					Type: openai.JSONSchemaTypeObject,
      					Properties: map[string]*openai.JSONSchemaDefinition{
      						"location": {
      							Type:        openai.JSONSchemaTypeString,
      							Description: "The city, e.g., San Francisco",
      						},
      						"unit": {
      							Type: openai.JSONSchemaTypeString,
      							Enum: []string{"celsius", "fahrenheit"},
      						},
      					},
      					Required: []string{"location"},
      				},
      			},
      		},
      	}

      	toolFunctions := map[string]ToolFunction{
      		"get_weather": getWeather,
      	}

      	streamerConfig := &StreamerConfig{
      		OpenAIClient:  client,
      		ModelName:     openai.GPT4oMini, // Recommended: GPT4oMini, GPT4Turbo, or GPT3Dot5Turbo
      		Tools:         tools,
      		ToolFunctions: toolFunctions,
      	}

      	// Middleware for logging requests
      	loggingMiddleware := func(next http.Handler) http.Handler {
      		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      			start := time.Now()
      			log.Printf("--> %s %s", r.Method, r.URL.Path)
      			next.ServeHTTP(w, r)
      			log.Printf("<-- %s %s (%s)", r.Method, r.URL.Path, time.Since(start))
      		})
      	}
      	
      	mux := http.NewServeMux()
      	mux.HandleFunc("/v1/chat/stream-completions", streamOpenAIWithToolsHandler(streamerConfig))
      	
      	loggedMux := loggingMiddleware(mux)

      	port := os.Getenv("PORT")
      	if port == "" {
      		port = "8080"
      	}
      	log.Printf("Server starting on http://localhost:%s\n", port)
      	log.Printf("To test with POST and custom messages:\ncurl -N -X POST -H \"Content-Type: application/json\" -d '{\"messages\": [{\"role\": \"user\", \"content\": \"What is the weather in London?\"}]}' http://localhost:%s/v1/chat/stream-completions\n", port)
      	log.Printf("To test with GET (uses default question):\ncurl -N http://localhost:%s/v1/chat/stream-completions\n", port)

      	if err := http.ListenAndServe(":"+port, loggedMux); err != nil {
      		log.Fatalf("Failed to start server: %v", err)
      	}
      }
      ```
    </CodeGroup>
  </Step>
</Steps>

## Logging

When an agent calls a tool, it can log the tool call in its event stream. The tool call event follows this format:

```json theme={null}
{
  "id": "call_abc123",
  "object": "thread.run.step.delta",
  "thread_id": "thread_xyz789",
  "model": "agent-name",
  "choices": [{
    "delta": {
      "role": "assistant",
      "step_details": {
        "type": "tool_calls",
        "tool_calls": [{
          "id": "tool_call_123",
          "name": "get_weather",
          "args": {
            "location": "New York, NY",
            "unit": "celsius"
          }
        }]
      }
    }
  }]
}
```

## Tool Responses in Agent Connect

After a tool is executed, the result be returned in this format:

```json theme={null}
{
  "id": "resp_def456",
  "object": "thread.run.step.delta",
  "thread_id": "thread_xyz789",
  "model": "agent-name",
  "choices": [{
    "delta": {
      "role": "assistant",
      "step_details": {
        "type": "tool_response",
        "content": "{\"temperature\": 22, \"condition\": \"sunny\", \"humidity\": 45}",
        "name": "get_weather",
        "tool_call_id": "tool_call_123"
      }
    }
  }]
}
```
