How to add structured tools / functions with multiple inputs 
Labels: name: area: memory, name: area: agent, name: auto:improvement, name: area: models

Currently, there is no support for agents that have both:
1) Conversational history
2) Structured tool chat (functions with multiple inputs/parameters)



#3700 mentions this as well but it was not resolved, `AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION` is zero_shot, and essentially has [no memory](https://stackoverflow.com/questions/76906469/langchain-zero-shot-react-agent-uses-memory-or-not). The langchain docs for [structured tool chat](https://python.langchain.com/docs/modules/agents/agent_types/structured_chat) the agent have a sense of memory through creating one massive input prompt. Still, this agent was performing much worse as #3700 mentions and other agents do not support multi input tools, even after creating [custom tools](https://python.langchain.com/docs/modules/agents/tools/custom_tools).


MY SOLUTION:

1) Use ConversationBufferMemory to keep track of chat history.
2) Convert these messages to a format OpenAI wants for their API.
3) Use the OpenAI chat completion endpoint, that has support for function calling

Usage: `chatgpt_function_response(user_prompt)`

- Dynamo db and session id stuff comes from the [docs](https://python.langchain.com/docs/integrations/memory/dynamodb_chat_message_history)
- `memory.py` handles getting the chat history for a particular session (can be interpreted as a user). We use ConversationBufferMemory as we usually would and add a helper method to convert the ConversationBufferMemory to a [format that OpenAI wants](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb)
- `core.py` handles the main functionality with a user prompt. We add the user's prompt to the message history, and get the message history in the OpenAI format. We use the chat completion endpoint as normal, and add the function response call to the message history as an AI message.
- `functions.py` is also how we would normally use the chat completions API, also described [here](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb)

`memory.py`
```

import logging
from typing import List
import boto3
from langchain.memory import ConversationBufferMemory
from langchain.memory.chat_message_histories import DynamoDBChatMessageHistory
from langchain.schema.messages import SystemMessage
from langchain.adapters.openai import convert_message_to_dict

TABLE_NAME = "your table name"

# if using dynamodb
session = boto3.session.Session(
    aws_access_key_id="",
    aws_secret_access_key="",
    region_name="",
)


def get_memory(session_id: str):
    """Get a conversation buffer with chathistory saved to dynamodb

    Returns:
        ConversationBufferMemory: A memory object with chat history saved to dynamodb
    """

    # Define the necessary components with the dynamodb endpoint
    message_history = DynamoDBChatMessageHistory(
        table_name=TABLE_NAME,
        session_id=session_id,
        boto3_session=session,
    )
    # if you want to add a system prompt
    if len(message_history.messages) == 0:
        message_history.add_message(SystemMessage(content="whatever system prompt"))

    memory = ConversationBufferMemory(
        memory_key="chat_history", chat_memory=message_history, return_messages=True
    )

    logging.info(f"Memory: {memory}")

    return memory


def convert_message_buffer_to_openai(memory: ConversationBufferMemory) -> List[dict]:
    """Convert a message buffer to a list of messages that OpenAI can understand

    Args:
        memory (ConversationBufferMemory): A memory object with chat history saved to dynamodb

    Returns:
        List[dict]: A list of messages that OpenAI can understand
    """
    messages = []
    for message in memory.buffer_as_messages:
        messages.append(convert_message_to_dict(message))

    return messages

```

`core.py`

```
def _handle_function_call(response: dict) -> str:
    response_message = response["message"]

    function_name = response_message["function_call"]["name"]
    function_to_call = function_names[function_name]
    function_args = json.loads(response_message["function_call"]["arguments"])

    function_response = function_to_call(**function_args)
    return function_response


def chatgpt_response(prompt, model=MODEL, session_id: str = SESSION_ID) -> str:
    memory = get_memory(session_id)
    memory.chat_memory.add_user_message(prompt)

    messages = convert_message_buffer_to_openai(memory)

    logging.info(f"Memory: {messages}")
    response = openai.ChatCompletion.create(
        model=model,
        messages=messages,
    )

    answer = response["choices"][0]["message"]["content"]

    memory.chat_memory.add_ai_message(answer)
    return answer


def chatgpt_function_response(
    prompt: str,
    functions=function_descriptions,
    model=MODEL,
    session_id: str = SESSION_ID,
) -> str:
    memory = get_memory(session_id)
    memory.chat_memory.add_user_message(prompt)

    messages = convert_message_buffer_to_openai(memory)

    logging.info(f"Memory for function response: {messages}")

    response = openai.ChatCompletion.create(
        model=model,
        messages=messages,
        functions=functions,
    )["choices"][0]

    if response["finish_reason"] == "function_call":
        answer = _handle_function_call(response)

    else:
        answer = response["message"]["content"]

    memory.chat_memory.add_ai_message(answer)
    return answer

```

`functions.py`

```
def create_reminder(
    task: str, days: int, hours: int, minutes: int
) -> str:
    return 'whatever'


function_names = {
    "create_reminder": create_reminder,
}


function_descriptions = [
    {
        "name": "create_reminder",
        "description": "This function handles the logic for creating a reminder for a "
        "generic task at a given date and time.",
        "parameters": {
            "type": "object",
            "properties": {
                "task": {
                    "type": "string",
                    "description": "The task to be reminded of, such as 'clean the "
                    "house'",
                },
                "days": {
                    "type": "integer",
                    "description": "The number of days from now to be reminded",
                },
                "hours": {
                    "type": "integer",
                    "description": "The number of hours from now to be reminded",
                },
                "minutes": {
                    "type": "integer",
                    "description": "The number of minutes from now to be reminded",
                },
            },
            "required": ["task", "days", "hours", "minutes"],
        },
    },
]
```