Support tool for AzureChatOpenAI
Labels: name: auto:improvement, name: area: models

### Feature request

There is a new implementation of function call which I think isn't supported by langchain yet.

https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/function-calling

### Motivation

AzureChatOpenAI models can't be used by OpenAIFunctionAgent due to the implementation issue.

### Your contribution

I've implemented a workaround here. Hoping for a full solution.

```python
from langchain.chat_models import AzureChatOpenAI


class AzureChatOpenAIWithTooling(AzureChatOpenAI):
    """AzureChatOpenAI with a patch to support functions.

    Function calling: https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/function-calling

    Currently only a single function call is supported.
    If multiple function calls are returned by the model, only the first one is used.
    """

    def _generate(self, messages, stop=None, run_manager=None, stream=None, **kwargs):
        if "functions" in kwargs:
            kwargs["tools"] = [
                {"type": "function", "function": f} for f in kwargs.pop("functions")
            ]
        return super()._generate(messages, stop, run_manager, stream, **kwargs)

    def _create_message_dicts(self, messages, stop):
        dicts, params = super()._create_message_dicts(messages, stop)
        latest_call_id = {}
        for d in dicts:
            if "function_call" in d:
                # Record the ID for future use
                latest_call_id[d["function_call"]["name"]] = d["function_call"]["id"]
                # Convert back to tool call
                d["tool_calls"] = [
                    {
                        "id": d["function_call"]["id"],
                        "function": {
                            k: v for k, v in d["function_call"].items() if k != "id"
                        },
                        "type": "function",
                    }
                ]
                d.pop("function_call")

            if d["role"] == "function":
                # Renaming as tool
                d["role"] = "tool"
                d["tool_call_id"] = latest_call_id[d["name"]]

        return dicts, params

    def _create_chat_result(self, response):
        result = super()._create_chat_result(response)
        for generation in result.generations:
            if generation.message.additional_kwargs.get("tool_calls"):
                function_calls = [
                    {**t["function"], "id": t["id"]}
                    for t in generation.message.additional_kwargs.pop("tool_calls")
                ]
                # Only consider the first one.
                generation.message.additional_kwargs["function_call"] = function_calls[
                    0
                ]
        return result
```

Test code:

```python

def test_azure_chat_openai():
    from scripts.aoai_llm import AzureChatOpenAIWithTooling

    agent = OpenAIFunctionsAgent.from_llm_and_tools(
        llm=AzureChatOpenAIWithTooling(azure_deployment="gpt-35-turbo", api_version="2023-12-01-preview", temperature=0.),
        tools=[
            StructuredTool.from_function(get_current_weather)
        ],
    )

    action = agent.plan([], input="What's the weather like in San Francisco?")
    print(action)

    tool_output = get_current_weather(**action.tool_input)
    result = agent.plan([
        (action, tool_output)
    ], input="What's the weather like in San Francisco?")
    print(result)


# Example function hard coded to return the same weather
# In production, this could be your backend API or an external API
def get_current_weather(location: str, unit: str = "fahrenheit"):
    """Get the current weather in a given location"""
    if "tokyo" in location.lower():
        return json.dumps({"location": "Tokyo", "temperature": "10", "unit": unit})
    elif "san francisco" in location.lower():
        return json.dumps(
            {"location": "San Francisco", "temperature": "72", "unit": unit}
        )
    elif "paris" in location.lower():
        return json.dumps({"location": "Paris", "temperature": "22", "unit": unit})
    else:
        return json.dumps({"location": location, "temperature": "unknown"})
```

(Note: the original example to ask about weather in three countries simultaneously doesn't work here.)