{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from dotenv import load_dotenv, find_dotenv\n",
    "_ = load_dotenv(find_dotenv())\n",
    "openai_api_key = os.environ[\"OPENAI_API_KEY\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Describe the functions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "functions = [\n",
    "    {\n",
    "        \"name\": \"get_item_info\",\n",
    "        \"description\": \"Get name and price of a menu item of the chinese restaurant\",\n",
    "        \"parameters\": {\n",
    "            \"type\": \"object\",\n",
    "            \"properties\": {\n",
    "                \"item_name\": {\n",
    "                    \"type\": \"string\",\n",
    "                    \"description\": \"The name of the menu item, e.g. Chop-Suey\",\n",
    "                },\n",
    "            },\n",
    "            \"required\": [\"item_name\"],\n",
    "        },\n",
    "    },\n",
    "    {\n",
    "        \"name\": \"place_order\",\n",
    "        \"description\": \"Place an order for a menu item from the restaurant\",\n",
    "        \"parameters\": {\n",
    "            \"type\": \"object\",\n",
    "            \"properties\": {\n",
    "                \"item_name\": {\n",
    "                    \"type\": \"string\",\n",
    "                    \"description\": \"The name of the item you want to order, e.g. Chop-Suey\",\n",
    "                },\n",
    "                \"quantity\": {\n",
    "                    \"type\": \"integer\",\n",
    "                    \"description\": \"The number of items you want to order\",\n",
    "                    \"minimum\": 1\n",
    "                },\n",
    "                \"address\": {\n",
    "                    \"type\": \"string\",\n",
    "                    \"description\": \"The address where the food should be delivered\",\n",
    "                },\n",
    "            },\n",
    "            \"required\": [\"item_name\", \"quantity\", \"address\"],\n",
    "        },\n",
    "    }\n",
    "]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Option 1: The LangChain Solution"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We use create_openai_fn_chain to create a chain that handles the OpenAI Functions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.chat_models import ChatOpenAI\n",
    "from langchain.prompts import PromptTemplate\n",
    "from langchain.chains.openai_functions import create_openai_fn_chain"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/juliocolomer/.pyenv/versions/3.11.4/envs/venv021524/lib/python3.11/site-packages/langchain_core/_api/deprecation.py:117: LangChainDeprecationWarning: The class `langchain_community.chat_models.openai.ChatOpenAI` was deprecated in langchain-community 0.0.10 and will be removed in 0.2.0. An updated version of the class exists in the langchain-openai package and should be used instead. To use it run `pip install -U langchain-openai` and import as `from langchain_openai import ChatOpenAI`.\n",
      "  warn_deprecated(\n",
      "/Users/juliocolomer/.pyenv/versions/3.11.4/envs/venv021524/lib/python3.11/site-packages/langchain_core/_api/deprecation.py:117: LangChainDeprecationWarning: The function `create_openai_fn_chain` was deprecated in LangChain 0.1.1 and will be removed in 0.2.0. Use create_openai_fn_runnable instead.\n",
      "  warn_deprecated(\n"
     ]
    }
   ],
   "source": [
    "llm = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0)\n",
    "\n",
    "template = \"\"\"You are an AI chatbot having a conversation \n",
    "with a human.\n",
    "\n",
    "Human: {human_input}\n",
    "AI: \"\"\"\n",
    "\n",
    "prompt = PromptTemplate(\n",
    "    input_variables=[\"human_input\"], \n",
    "    template=template\n",
    ")\n",
    "\n",
    "chain = create_openai_fn_chain(\n",
    "    functions, \n",
    "    llm, \n",
    "    prompt, \n",
    "    verbose=True\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As we can see below, this app appears to work well:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/juliocolomer/.pyenv/versions/3.11.4/envs/venv021524/lib/python3.11/site-packages/langchain_core/_api/deprecation.py:117: LangChainDeprecationWarning: The function `run` was deprecated in LangChain 0.1.0 and will be removed in 0.2.0. Use invoke instead.\n",
      "  warn_deprecated(\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "\n",
      "\u001b[1m> Entering new LLMChain chain...\u001b[0m\n",
      "Prompt after formatting:\n",
      "\u001b[32;1m\u001b[1;3mYou are an AI chatbot having a conversation \n",
      "with a human.\n",
      "\n",
      "Human: How much does Chop-Suey cost?\n",
      "AI: \u001b[0m\n",
      "\n",
      "\u001b[1m> Finished chain.\u001b[0m\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'arguments': {'item_name': 'Chop-Suey'}, 'name': 'get_item_info'}"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "chain.run(\"How much does Chop-Suey cost?\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "\n",
      "\u001b[1m> Entering new LLMChain chain...\u001b[0m\n",
      "Prompt after formatting:\n",
      "\u001b[32;1m\u001b[1;3mYou are an AI chatbot having a conversation \n",
      "with a human.\n",
      "\n",
      "Human: I want to order two Chop-Suey to 321 Street\n",
      "AI: \u001b[0m\n",
      "\n",
      "\u001b[1m> Finished chain.\u001b[0m\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'arguments': {'item_name': 'Chop-Suey',\n",
       "  'quantity': 2,\n",
       "  'address': '321 Street'},\n",
       " 'name': 'place_order'}"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "chain.run(\"I want to order two Chop-Suey to 321 Street\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Problem: this app breaks if the user makes an off-topic question"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "# chain.run(\"How old did my grandfather get?\") # Not-so-good error message."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Option 2: Solution using the OpenAI API"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "#!pip install openai"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openai\n",
    "\n",
    "def chat(query):\n",
    "    response = openai.chat.completions.create(\n",
    "        model=\"gpt-3.5-turbo-0125\",\n",
    "        messages=[{\"role\": \"user\", \"content\": query}],\n",
    "        functions=functions\n",
    "    )\n",
    "    message = response.choices[0].message\n",
    "    return message"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ChatCompletionMessage(content=None, role='assistant', function_call=FunctionCall(arguments='{\"item_name\":\"Chop-Suey\",\"quantity\":2,\"address\":\"321 Street\"}', name='place_order'), tool_calls=None)"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "chat(\"I want to order two Chop-Suey to 321 Street\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ChatCompletionMessage(content=\"I'm an AI assistant and I can provide you with information. The current Pope is Pope Francis.\", role='assistant', function_call=None, tool_calls=None)"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "chat(\"Who is the current Pope?\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ChatCompletionMessage(content=None, role='assistant', function_call=FunctionCall(arguments='{\"address\":\"321 Street\",\"item_name\":\"Chop-Suey\",\"quantity\":2}', name='place_order'), tool_calls=None)"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "message = chat(\"I want to order two Chop-Suey to 321 Street\")\n",
    "message"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'openai.types.chat.chat_completion_message.ChatCompletionMessage'>\n",
      "function call needed!\n"
     ]
    }
   ],
   "source": [
    "print(type(message))\n",
    "\n",
    "if (hasattr(message, 'function_call')): # not a real dictionary\n",
    "    print(\"function call needed!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Let's use a dictionary as a fake database to check full functionality"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "fake_db = {\n",
    "    \"items\": {\n",
    "        \"Chop-Suey\": {\"price\": 15.00, \"ingredients\": [\"chop\", \"suey\", \"cheese\"]},\n",
    "        \"Lo-Main\": {\"price\": 10.00, \"ingredients\": [\"lo\", \"main\", \"basil\"]},\n",
    "        \"Chin-Gun\": {\"price\": 12.50, \"ingredients\": [\"chin\", \"gunu\", \"tomato sauce\"]},\n",
    "        \"Won-Ton\": {\"price\": 11.00, \"ingredients\": [\"won\", \"ton\", \"mushrooms\"]},\n",
    "    },\n",
    "    \"orders\": []\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Let's create the functions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_item_info(item_name):\n",
    "    item = fake_db[\"items\"].get(item_name)\n",
    "    \n",
    "    if not item:\n",
    "        return f\"No information available for item: {item_name}\"\n",
    "\n",
    "    return {\"name\": item_name, \"price\": item[\"price\"], \"ingredients\": item[\"ingredients\"]}\n",
    "\n",
    "def place_order(item_name, quantity, address):\n",
    "    if item_name not in fake_db[\"items\"]:\n",
    "        return f\"We don't have {item_name}!\"\n",
    "    \n",
    "    if quantity < 1:\n",
    "        return \"You must order at least one item.\"\n",
    "    \n",
    "    order_id = len(fake_db[\"orders\"]) + 1\n",
    "    order = {\n",
    "        \"order_id\": order_id,\n",
    "        \"item_name\": item_name,\n",
    "        \"quantity\": quantity,\n",
    "        \"address\": address,\n",
    "        \"total_price\": fake_db[\"items\"][item_name][\"price\"] * quantity\n",
    "    }\n",
    "\n",
    "    fake_db[\"orders\"].append(order)\n",
    "    \n",
    "    return f\"Order placed successfully! Your order ID is {order_id}. Total price is ${order['total_price']}.\"\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Now let's check the App"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ChatCompletionMessage(content=None, role='assistant', function_call=FunctionCall(arguments='{\"item_name\":\"Chop-Suey\",\"quantity\":2,\"address\":\"321 Street\"}', name='place_order'), tool_calls=None)"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "message = chat(\"I want to order two Chop-Suey to 321 Street\")\n",
    "message"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "place_order\n",
      "{'item_name': 'Chop-Suey', 'quantity': 2, 'address': '321 Street'}\n"
     ]
    }
   ],
   "source": [
    "import json\n",
    "\n",
    "print(message.function_call.name)\n",
    "print(json.loads(message.function_call.arguments))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Order placed successfully! Your order ID is 1. Total price is $30.0.\n"
     ]
    }
   ],
   "source": [
    "function_name = message.function_call.name\n",
    "arguments = json.loads(message.function_call.arguments)\n",
    "\n",
    "response = place_order(**arguments)\n",
    "print(response)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Let´s improve the app making it dynamic (the LLM can continue the conversation after the order has been placed)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openai\n",
    "import json\n",
    "\n",
    "class ChatBot:\n",
    "    \n",
    "    def __init__(self, database):\n",
    "        self.fake_db = database\n",
    "        \n",
    "    def chat(self, query):\n",
    "        initial_response = self.make_openai_request(query)\n",
    "        \n",
    "        message = initial_response.choices[0].message\n",
    "        \n",
    "        if (hasattr(message, 'function_call') & (message.function_call != None)):\n",
    "            function_name = message.function_call.name\n",
    "            arguments = json.loads(message.function_call.arguments)\n",
    "            function_response = getattr(self, function_name)(**arguments)\n",
    "            \n",
    "            follow_up_response = self.make_follow_up_request(query, message, function_name, function_response)\n",
    "            return follow_up_response.choices[0].message.content\n",
    "        else:\n",
    "            return message.content\n",
    "    \n",
    "    def make_openai_request(self, query):\n",
    "        response = openai.chat.completions.create(\n",
    "            model=\"gpt-3.5-turbo-0613\",\n",
    "            messages=[{\"role\": \"user\", \"content\": query}],\n",
    "            functions=functions\n",
    "        )\n",
    "        return response\n",
    "\n",
    "    def make_follow_up_request(self, query, initial_message, function_name, function_response):\n",
    "        response = openai.chat.completions.create(\n",
    "            model=\"gpt-3.5-turbo-0613\",\n",
    "            messages=[\n",
    "                {\"role\": \"user\", \"content\": query},\n",
    "                initial_message,\n",
    "                {\n",
    "                    \"role\": \"function\",\n",
    "                    \"name\": function_name,\n",
    "                    \"content\": function_response,\n",
    "                },\n",
    "            ],\n",
    "        )\n",
    "        return response\n",
    "\n",
    "    def place_order(self, item_name, quantity, address):\n",
    "        if item_name not in self.fake_db[\"items\"]:\n",
    "            return f\"We don't have {item_name}!\"\n",
    "        \n",
    "        if quantity < 1:\n",
    "            return \"You must order at least one item.\"\n",
    "        \n",
    "        order_id = len(self.fake_db[\"orders\"]) + 1\n",
    "        order = {\n",
    "            \"order_id\": order_id,\n",
    "            \"item_name\": item_name,\n",
    "            \"quantity\": quantity,\n",
    "            \"address\": address,\n",
    "            \"total_price\": self.fake_db[\"items\"][item_name][\"price\"] * quantity\n",
    "        }\n",
    "\n",
    "        self.fake_db[\"orders\"].append(order)\n",
    "        \n",
    "        return f\"Order placed successfully! Your order ID is {order_id}. Total price is ${order['total_price']}.\"\n",
    "\n",
    "    def get_item_info(self, item_name):\n",
    "        if item_name in self.fake_db[\"items\"]:\n",
    "            item = self.fake_db[\"items\"][item_name]\n",
    "            return f\"Item: {item['name']}, Price: ${item['price']}\"\n",
    "        else:\n",
    "            return f\"We don't have information about {item_name}.\"\n",
    "\n",
    "database = {\n",
    "    \"items\": {\n",
    "        \"Chop-Suey\": {\n",
    "            \"name\": \"Chop-Suey\",\n",
    "            \"price\": 15.0\n",
    "        },\n",
    "        \"Lo-Mein\": {\n",
    "            \"name\": \"Lo-Mein\",\n",
    "            \"price\": 12.0\n",
    "        }\n",
    "    },\n",
    "    \"orders\": []\n",
    "}\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Let's check the app"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [],
   "source": [
    "bot = ChatBot(database=database)\n",
    "response = bot.chat(\"I want to order two Chop-Suey to 321 Street\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Your order for two Chop-Suey to be delivered at 321 Street has been successfully placed. The order ID is 2, and the total price is $30.0.'"
      ]
     },
     "execution_count": 30,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Let's ask for a type of food that is not in the menu"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'I apologize, but it seems that we do not have spring rolls available. Is there anything else you would like to order?'"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = bot.chat(\"I want to order one spring roll to 321 Street\")\n",
    "response"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'items': {'Chop-Suey': {'name': 'Chop-Suey', 'price': 15.0}, 'Lo-Mein': {'name': 'Lo-Mein', 'price': 12.0}}, 'orders': [{'order_id': 1, 'item_name': 'Chop-Suey', 'quantity': 2, 'address': '321 Street', 'total_price': 30.0}, {'order_id': 2, 'item_name': 'Chop-Suey', 'quantity': 2, 'address': '321 Street', 'total_price': 30.0}]}\n"
     ]
    }
   ],
   "source": [
    "print(database)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Let's ask an off-topic question"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'The current Pope is Pope Francis.'"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response= bot.chat(\"Who is the current Pope?\")\n",
    "response"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## As you see, the app does not break now."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
