{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "aaf53a2d-2b29-4d83-bca8-aa6cf7d3a471",
      "metadata": {
        "id": "aaf53a2d-2b29-4d83-bca8-aa6cf7d3a471"
      },
      "source": [
        "# <center> 🤿 Погружение в Агентов LangChain  </center>\n",
        "### <center> 😎 Создаём \"суперагента\" 🕵️‍♂️ </center>"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "903691fb-7206-4e21-acad-e637c2c146b7",
      "metadata": {
        "jp-MarkdownHeadingCollapsed": true,
        "id": "903691fb-7206-4e21-acad-e637c2c146b7"
      },
      "source": [
        "### Оглавление ноутбука\n",
        "<img src='https://github.com/a-milenkin/LLM_practical_course/blob/main/images/ag2.webp?raw=1' align=\"right\" width=\"500\" height=\"428\" >\n",
        "<br>\n",
        "\n",
        "<p><font size=\"3\" face=\"Arial\" font-size=\"large\"><ul type=\"square\">\n",
        "    \n",
        "<li><a href=\"#p1\">🤖 Основные типы агентов и их суперспособности 🦾</a></li>\n",
        "<li><a href=\"#p2\">🚀 ReAct Agent 🔄</a></li><ul type=\"square\">\n",
        "<li><a href=\"#p2.1\">🔎 Инструмент поиск Google (SerpAPI) 🔎</a></li></ul>\n",
        "<li><a href=\"#p3\">💬 Structured Chat Agent 💬</a></li>\n",
        "<li><a href=\"#p4\">🙋🏻 Self-Ask with search Agent ❓</a></li>\n",
        "<li><a href=\"#p5\">😎 Агент, который всё помнит! 🫣</a></li>\n",
        "<li><a href=\"#p6\">🪛 OpenAI Tools Agent + RAG 🦞</a></li>\n",
        "<li><a href=\"#p7\">🙋🏻 Agent с sql базой 📊</a></li><ul type=\"square\">\n",
        "<li><a href=\"#p7.1\"> 🫂 SQL агент + python агент = super агент 😎</a></li></ul>\n",
        "<li><a href=\"#p8\">Задеплой Агента с помощью LangServe 🛋 </a></li>\n",
        "<li><a href=\"#p9\">🥁 Выводы и заключения ✅ </a></li>\n",
        "\n",
        "\n",
        "    \n",
        "</ul></font></p>"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "550b310f-8e4e-4a1f-8291-7c1d11d86031",
      "metadata": {
        "id": "550b310f-8e4e-4a1f-8291-7c1d11d86031"
      },
      "source": [
        "### 🧑‍🎓 В этом уроке более подробно погрузимся в возможности агентов с LangChain."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "fd014e50-ac2f-4f95-9b93-a32d86126e41",
      "metadata": {
        "id": "fd014e50-ac2f-4f95-9b93-a32d86126e41"
      },
      "source": [
        "# <center  id=\"p1\"> 🤖 Основные типы агентов и их суперспособности 🦾 </center>"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2679b3d0-e7a6-45fc-bc0d-d82391a4cb3e",
      "metadata": {
        "id": "2679b3d0-e7a6-45fc-bc0d-d82391a4cb3e"
      },
      "source": [
        "<div class=\"alert alert-info\">\n",
        "    \n",
        "📖 В `LangChain` представлены агенты 5 основных типов (`Agent Types`):\n",
        "* `ReAct (Reasoning and Acting)` - агенты такого типа создаются по умолчанию, работают с инструментами, принимающими один параметр на вход.\n",
        "* `Structured chat` - могут работать с инструментами, которые принимают много параметров на вход\n",
        "* `Self Ask with Search` - реализация техники промптинга `Self-Ask`, работает только с инструментами поиска\n",
        "* `XMLAgent, JSONAgent` - для работы с чат моделями в форматах `XML` и `JSON` (например, модели от `Anthropic`)\n",
        "* `OpenAI Tools Agents` - пришли на замену `OpenAIFunctions Agents` (OpenAI перестает их поддерживать). Могут вызывать сразу одну или несколько функций, тем самым сокращая время для получения ответа - работает с последними моделями от OpenAI."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "024f33ed-a522-40ef-a061-e17184cf380a",
      "metadata": {
        "id": "024f33ed-a522-40ef-a061-e17184cf380a"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "Рассмотрим подробнее первые три типа агентов. <br>\n",
        "Каждый агент \"под капотом\" содержит тот или иной промпт завернутый в удобную обертку агента `LangChain`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "2318c74b-038b-4d04-b3e5-0275d99c7811",
      "metadata": {
        "tags": [],
        "id": "2318c74b-038b-4d04-b3e5-0275d99c7811"
      },
      "outputs": [],
      "source": [
        "# !pip install langchain langchain-openai openai langchainhub google-search-results faiss-сpu langchain-experimental langserve -q"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "493f4994-0e2e-468c-adbf-916a3eceec38",
      "metadata": {
        "tags": [],
        "id": "493f4994-0e2e-468c-adbf-916a3eceec38"
      },
      "outputs": [],
      "source": [
        "import os\n",
        "from getpass import getpass"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "410165ef-1236-4cb5-8a7f-b74ed6554d84",
      "metadata": {
        "jupyter": {
          "source_hidden": true
        },
        "tags": [],
        "id": "410165ef-1236-4cb5-8a7f-b74ed6554d84"
      },
      "outputs": [],
      "source": [
        "# Для работы в колабе загрузите наш скрипт для использования ChatGPT с ключом курса, и необходимые файлы!\n",
        "# !mkdir ../data/\n",
        "# !wget -P ../data https://raw.githubusercontent.com/a-milenkin/LLM_practical_course/main/data/pizzeria.db\n",
        "# !wget https://raw.githubusercontent.com/a-milenkin/LLM_practical_course/main/notebooks/utils.py"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "d8c8b9c2-9f11-409c-933f-7c0d623cb3d2",
      "metadata": {
        "jupyter": {
          "source_hidden": true
        },
        "tags": [],
        "id": "d8c8b9c2-9f11-409c-933f-7c0d623cb3d2"
      },
      "outputs": [],
      "source": [
        "# # Если используете ключ от OpenAI, запустите эту ячейку\n",
        "# from langchain_openai import ChatOpenAI\n",
        "\n",
        "# # os.environ['OPENAI_API_KEY'] = \"Введите ваш OpenAI API ключ\"\n",
        "# os.environ['OPENAI_API_KEY'] = getpass(prompt='Введите ваш OpenAI API ключ')\n",
        "\n",
        "# # инициализируем языковую модель\n",
        "# llm = ChatOpenAI(temperature=0.0)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "8347a770-e986-433c-94fc-582aef674d51",
      "metadata": {
        "tags": [],
        "id": "8347a770-e986-433c-94fc-582aef674d51",
        "outputId": "0fce2454-7496-4c22-cba7-12c55e5fe41f"
      },
      "outputs": [
        {
          "name": "stdin",
          "output_type": "stream",
          "text": [
            "Введите ваш ключ, полученный в боте курса: ········\n"
          ]
        }
      ],
      "source": [
        "# Если используете ключ из курса, запустите эту ячейку\n",
        "from utils import ChatOpenAI\n",
        "\n",
        "# course_api_key= \"Введите ваш ключ, полученный в боте курса\"\n",
        "course_api_key = getpass(prompt=\"Введите ваш ключ, полученный в боте курса:\")\n",
        "\n",
        "# инициализируем языковую модель\n",
        "llm = ChatOpenAI(temperature=0.0, course_api_key=course_api_key)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8aece752-2a4e-4057-befc-fad3191e4bdd",
      "metadata": {
        "id": "8aece752-2a4e-4057-befc-fad3191e4bdd"
      },
      "source": [
        "# <center  id=\"p2\"> `ReAct` `Agent` - сначала придумай план, потом делай!🚀</center>\n",
        "\n",
        "<img src='https://github.com/a-milenkin/LLM_practical_course/blob/main/images/react_sch.png?raw=1' align=\"right\" width=\"550\" height=\"428\" >\n",
        "<br>\n",
        "<div class=\"alert alert-info\">\n",
        "    \n",
        "Этот тип агента реализуется на основе техники `ReAct Prompting` представленной командой GoogleResearch в 2022 году в статье [ReAct: Synergizing Reasoning and Acting in Language Models](https://arxiv.org/abs/2210.03629).\n",
        "\n",
        "**`ReAct`** объединила в себе два подхода:\n",
        "* **`Reasoning`** (техника Chain of Thoughts)\n",
        "* **`Acting`** (Action Plan Generation - генерация плана действий)\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d0c94c2a-ecde-45a5-9ebe-f8d9f18422fb",
      "metadata": {
        "id": "d0c94c2a-ecde-45a5-9ebe-f8d9f18422fb"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "#### Посмотрим как выглядит шаблон промпта ReAct агента в `LangChain`:\n",
        "```\n",
        "Answer the following questions as best you can. You have access to the\n",
        "following tools:\n",
        "{tools}\n",
        "Use the following format:\n",
        "Question: the input question you must answer\n",
        "Thought: you should always think about what to do\n",
        "Action: the action to take, should be one of [{tool_names}]\n",
        "Action Input: the input to the action\n",
        "Observation: the result of the action\n",
        "... (this Thought/Action/Action Input/Observation can repeat N times)\n",
        "Thought: I now know the final answer\n",
        "Final Answer: the final answer to the original input question\n",
        "Begin!\n",
        "Question: {input}\n",
        "Thought:{agent_scratchpad}\n",
        "```"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5e2b8035-71da-4fac-bb9d-2157f1809f22",
      "metadata": {
        "id": "5e2b8035-71da-4fac-bb9d-2157f1809f22"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "Шаблоны промптов, используемые в агентах (и не только) хранятся в [LangChain Hub](https://smith.langchain.com/hub). Туда, примерно так же, как на `GitHub`, можно загружать свои промпты и потом удобно ими пользоваться. 🤗"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "2190a01a-26ba-4f0d-9846-3a2caa2703d2",
      "metadata": {
        "id": "2190a01a-26ba-4f0d-9846-3a2caa2703d2",
        "outputId": "53863804-c77f-42e9-96c9-e1ad7175e26f"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "input_variables=['agent_scratchpad', 'input', 'tool_names', 'tools'] template='Answer the following questions as best you can. You have access to the following tools:\\n\\n{tools}\\n\\nUse the following format:\\n\\nQuestion: the input question you must answer\\nThought: you should always think about what to do\\nAction: the action to take, should be one of [{tool_names}]\\nAction Input: the input to the action\\nObservation: the result of the action\\n... (this Thought/Action/Action Input/Observation can repeat N times)\\nThought: I now know the final answer\\nFinal Answer: the final answer to the original input question\\n\\nBegin!\\n\\nQuestion: {input}\\nThought:{agent_scratchpad}'\n"
          ]
        }
      ],
      "source": [
        "from langchain import hub\n",
        "\n",
        "prompt = hub.pull(\"hwchase17/react\")\n",
        "print(prompt)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5c2c92f5-accf-42e4-b459-c0dc82e58922",
      "metadata": {
        "id": "5c2c92f5-accf-42e4-b459-c0dc82e58922"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "\n",
        "🤔 Видим шаблон промпта, который обсуждали выше и служебные переменные."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0fb03f05-140d-49d8-ba25-d33b83e91b8a",
      "metadata": {
        "id": "0fb03f05-140d-49d8-ba25-d33b83e91b8a"
      },
      "source": [
        "### <center  id=\"p2.1\"> 🚮 Добавим инструмент - Google-поиск (SerpAPI) 🔎</center>\n",
        "<div class=\"alert alert-info\">\n",
        "    \n",
        "Для использования поисковика Google по API необходимо: зарегистрироваться на сайте [serpapi.com](https://serpapi.com) и получить API-ключ.\n",
        "На бесплатном тарифе доступно 100 запросов в месяц."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "e22537f4-d088-4094-a5b3-373a814b0a95",
      "metadata": {
        "id": "e22537f4-d088-4094-a5b3-373a814b0a95"
      },
      "outputs": [],
      "source": [
        "import langchain\n",
        "from langchain.agents import AgentType, initialize_agent, load_tools, tool"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "2ab693ef-2f59-4240-a435-eb2b811aecef",
      "metadata": {
        "id": "2ab693ef-2f59-4240-a435-eb2b811aecef",
        "outputId": "c494743e-f5a6-4a92-89ca-5bfbed935b3a"
      },
      "outputs": [
        {
          "name": "stdin",
          "output_type": "stream",
          "text": [
            "Введите ваш ключ SerpApi: ········\n"
          ]
        }
      ],
      "source": [
        "# Добавляем полученный ключ в переменную среды\n",
        "os.environ[\"SERPAPI_API_KEY\"] = getpass(prompt=\"Введите ваш ключ SerpApi:\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "1381c1ec-4c64-4273-9508-44de894e2b96",
      "metadata": {
        "id": "1381c1ec-4c64-4273-9508-44de894e2b96",
        "outputId": "29ae2f95-fb26-4e2a-8463-1b26fa9679bf"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "\n",
            "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
            "\u001b[32;1m\u001b[1;3mI need to find the current temperature in Sochi.\n",
            "Action: Search\n",
            "Action Input: \"current temperature in Sochi\"\u001b[0m\n",
            "Observation: \u001b[36;1m\u001b[1;3m{'type': 'weather_result', 'temperature': '62', 'unit': 'Fahrenheit', 'precipitation': '3%', 'humidity': '60%', 'wind': '7 mph', 'location': 'Sochi, Russia', 'date': 'Monday 4:00 PM', 'weather': 'Mostly cloudy'}\u001b[0m\n",
            "Thought:\u001b[32;1m\u001b[1;3mThe current temperature in Sochi is 62 degrees Fahrenheit.\n",
            "Final Answer: The current temperature in Sochi is 62 degrees Fahrenheit.\u001b[0m\n",
            "\n",
            "\u001b[1m> Finished chain.\u001b[0m\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "{'input': 'Какая температура в Сочи?',\n",
              " 'output': 'The current temperature in Sochi is 62 degrees Fahrenheit.'}"
            ]
          },
          "execution_count": 11,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Легаси интерфейс и его недостатки\n",
        "tools = load_tools([\"serpapi\"], llm=llm)\n",
        "\n",
        "agent = initialize_agent(\n",
        "    tools,\n",
        "    llm,\n",
        "    agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, # Указывается тип агента, непонятно с каким промптом\n",
        "    handle_parsing_errors = True, # Агент не упадёт с ошибкой, если не распарсит ответ\n",
        "    verbose=True)\n",
        "#langchain.debug = True # включение Debug режима, для отслеживания рассуждений агента\n",
        "langchain.debug = False\n",
        "\n",
        "agent.invoke('Какая температура в Сочи?')"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6c79ee15-de4b-45cb-9cec-3dc44d43d8e7",
      "metadata": {
        "id": "6c79ee15-de4b-45cb-9cec-3dc44d43d8e7"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "В легаси интерфейсе, не было явно видно какой именно промпт используется для агента и откуда он берется? Новый интерфейс создания агентов позволяет явно указывать промпт используемый в работе агента. <br>\n",
        "Так же это открывает простой путь к кастомизации промптов для агентов, если дефолтные промпты не подходят для нашей конкретной задачи:\n",
        "* Берем стандартный промпт, изменяем его. Или пишем собственный с нуля.\n",
        "* Загружаем на `langchainhub`\n",
        "* При необходимости подгружаем с хаба и пользуемся."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "1acbf1c0-f0b9-41bc-a99f-664f03a21421",
      "metadata": {
        "id": "1acbf1c0-f0b9-41bc-a99f-664f03a21421",
        "outputId": "d6ce25c2-a50f-43fa-c81e-b47de89d2e4b"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "\n",
            "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
            "\u001b[32;1m\u001b[1;3mI need to find the current temperature in Sochi in Celsius.\n",
            "Action: Search\n",
            "Action Input: \"current temperature in Sochi in Celsius\"\u001b[0m\u001b[36;1m\u001b[1;3m{'type': 'weather_result', 'temperature': '17', 'unit': 'Celsius', 'precipitation': '2%', 'humidity': '60%', 'wind': '11 km/h', 'location': 'Sochi, Russia', 'date': 'Monday 4:00 PM', 'weather': 'Mostly cloudy'}\u001b[0m\u001b[32;1m\u001b[1;3mThe current temperature in Sochi is 17 degrees Celsius.\n",
            "Final Answer: 17 degrees Celsius.\u001b[0m\n",
            "\n",
            "\u001b[1m> Finished chain.\u001b[0m\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "{'input': 'Температура в Сочи? Ответь в Цельсиях',\n",
              " 'output': '17 degrees Celsius.'}"
            ]
          },
          "execution_count": 19,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "from langchain.agents import AgentExecutor, create_react_agent\n",
        "\n",
        "tools = load_tools([\"serpapi\"], llm=llm)\n",
        "langchain.debug = False\n",
        "\n",
        "# В новом интерфейсе явно задается используемый промпт\n",
        "prompt = hub.pull(\"hwchase17/react\")\n",
        "\n",
        "agent = create_react_agent(llm, tools, prompt)\n",
        "\n",
        "agent_executor = AgentExecutor(agent=agent,\n",
        "                               tools=tools,\n",
        "                               verbose=True,\n",
        "                               max_iterations=15, # максимальное количество итераций агента (15 по умолчанию)\n",
        "                               handle_parsing_errors=True)\n",
        "\n",
        "agent_executor.invoke({\"input\": \"Температура в Сочи? Ответь в Цельсиях\"})"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "7a89525d-37e9-452a-824d-d88c667961e9",
      "metadata": {
        "id": "7a89525d-37e9-452a-824d-d88c667961e9"
      },
      "outputs": [],
      "source": [
        "# Создадим свой tool, вычисляющий площадь треугольника\n",
        "from langchain.agents import tool\n",
        "\n",
        "@tool\n",
        "def compute_triangle_area(a, b, c):\n",
        "    \"\"\"Вычисляет площадь треугольника по длинам его сторон\"\"\"\n",
        "    s = (a + b + c) / 2\n",
        "    return (s * (s - a) * (s - b) * (s - c)) ** 0.5"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "7d9a5b5b-7420-43cc-80b2-fdf3943abd84",
      "metadata": {
        "id": "7d9a5b5b-7420-43cc-80b2-fdf3943abd84"
      },
      "outputs": [],
      "source": [
        "# Добавим его к поиску\n",
        "tools.append(compute_triangle_area)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "f66c562f-5fad-4ab3-8535-df385ae8b13c",
      "metadata": {
        "id": "f66c562f-5fad-4ab3-8535-df385ae8b13c",
        "outputId": "33feed4d-6bf4-489d-ad19-364867fef3fc"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "StructuredTool(name='compute_triangle_area', description='compute_triangle_area(a, b, c) - Вычисляет площадь треугольника по длинам его сторон', args_schema=<class 'pydantic.v1.main.compute_triangle_areaSchema'>, func=<function compute_triangle_area at 0x7f3431b288b0>)"
            ]
          },
          "execution_count": 23,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "tools[1]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "5bae43a8-8ee6-4882-a56a-efedcb8b0595",
      "metadata": {
        "id": "5bae43a8-8ee6-4882-a56a-efedcb8b0595"
      },
      "outputs": [],
      "source": [
        "agent = create_react_agent(llm, tools, prompt)\n",
        "\n",
        "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)\n",
        "\n",
        "agent_executor.invoke({\"input\": \"Посчитай площадь треугольника со сторонами 2, 2, 2\"})"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "035517e7-044e-4978-8436-c0849122f859",
      "metadata": {
        "id": "035517e7-044e-4978-8436-c0849122f859"
      },
      "source": [
        "<div class=\"alert alert-warning\">\n",
        "    \n",
        "Получим ошибку: `ValidationError` <br>\n",
        "\n",
        "<img src='https://github.com/a-milenkin/LLM_practical_course/blob/main/images/ag_err.png?raw=1'  width=\"808\" height=\"628\" >\n",
        "\n",
        "Это происходит из-за того, что `ReAct` агент не может применять `StructuredTool` - инструменты, принимающие на вход несколько параметров."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bc730c09-92d9-4673-b0e6-c8422e3472ad",
      "metadata": {
        "id": "bc730c09-92d9-4673-b0e6-c8422e3472ad"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "🤗 Для работы с такими инструментами применяется другой тип агента `Structured Chat Agent`."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "16cfd3a1-335a-46fe-8899-ad6be3f9d5db",
      "metadata": {
        "id": "16cfd3a1-335a-46fe-8899-ad6be3f9d5db"
      },
      "source": [
        "# <center id=\"p3\"> Structured Chat Agent 💬</center>\n",
        "\n",
        "<div class=\"alert alert-info\">\n",
        "    \n",
        "Не сильно отличается от `ReAct` агента - просто в промпт добавлены специальные инструкции и шаги для работы с `milti-input tools`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "65733e96-9f03-4b29-8f55-21cac1cbd486",
      "metadata": {
        "id": "65733e96-9f03-4b29-8f55-21cac1cbd486",
        "outputId": "85d84991-a53c-4403-9819-807d991723b7"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Respond to the human as helpfully and accurately as possible. You have access to the following tools:\n",
            "\n",
            "{tools}\n",
            "\n",
            "Use a json blob to specify a tool by providing an action key (tool name) and an action_input key (tool input).\n",
            "\n",
            "Valid \"action\" values: \"Final Answer\" or {tool_names}\n",
            "\n",
            "Provide only ONE action per $JSON_BLOB, as shown:\n",
            "\n",
            "```\n",
            "{{\n",
            "  \"action\": $TOOL_NAME,\n",
            "  \"action_input\": $INPUT\n",
            "}}\n",
            "```\n",
            "\n",
            "Follow this format:\n",
            "\n",
            "Question: input question to answer\n",
            "Thought: consider previous and subsequent steps\n",
            "Action:\n",
            "```\n",
            "$JSON_BLOB\n",
            "```\n",
            "Observation: action result\n",
            "... (repeat Thought/Action/Observation N times)\n",
            "Thought: I know what to respond\n",
            "Action:\n",
            "```\n",
            "{{\n",
            "  \"action\": \"Final Answer\",\n",
            "  \"action_input\": \"Final response to human\"\n",
            "}}\n",
            "\n",
            "Begin! Reminder to ALWAYS respond with a valid json blob of a single action. Use tools if necessary. Respond directly if appropriate. Format is Action:```$JSON_BLOB```then Observation\n"
          ]
        }
      ],
      "source": [
        "# Посмотрим на промпт Structured Chat агента, используемый по умолчанию\n",
        "prompt = hub.pull(\"hwchase17/structured-chat-agent\")\n",
        "print(prompt.messages[0].prompt.template)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "59e41d8a-caf5-4250-9f3b-289eeaea7c2c",
      "metadata": {
        "id": "59e41d8a-caf5-4250-9f3b-289eeaea7c2c"
      },
      "source": [
        "<div class=\"alert alert-info\">\n",
        "\n",
        "Видим, что по сути, промпт совпадает с промптом `ReAct` агента.\n",
        "    \n",
        "Посмотрим, как справится с задачей `structured_chat_agent`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "e1dd61c3-c616-457c-92e4-0660f5d07867",
      "metadata": {
        "id": "e1dd61c3-c616-457c-92e4-0660f5d07867",
        "outputId": "5fd00e90-8543-4aeb-9130-fc2781709857"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "\n",
            "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
            "\u001b[32;1m\u001b[1;3mThought: The user is asking to calculate the area of a triangle with side lengths of 2, 2, and 2.\n",
            "\n",
            "Action:\n",
            "```\n",
            "{\n",
            "  \"action\": \"compute_triangle_area\",\n",
            "  \"action_input\": {\n",
            "    \"a\": 2,\n",
            "    \"b\": 2,\n",
            "    \"c\": 2\n",
            "  }\n",
            "}\n",
            "```\u001b[0m\u001b[33;1m\u001b[1;3m1.7320508075688772\u001b[0m\u001b[32;1m\u001b[1;3mAction:\n",
            "```\n",
            "{\n",
            "  \"action\": \"Final Answer\",\n",
            "  \"action_input\": 1.7320508075688772\n",
            "}\n",
            "```\u001b[0m\n",
            "\n",
            "\u001b[1m> Finished chain.\u001b[0m\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "{'input': 'Посчитай площадь треугольника со сторонами 2, 2, 2',\n",
              " 'output': 1.7320508075688772}"
            ]
          },
          "execution_count": 27,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "from langchain.agents import create_structured_chat_agent\n",
        "\n",
        "agent = create_structured_chat_agent(llm, tools, prompt)\n",
        "\n",
        "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)\n",
        "\n",
        "agent_executor.invoke({\"input\": \"Посчитай площадь треугольника со сторонами 2, 2, 2\"})"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f6e21868-b9a2-40a8-909a-394c957cf2c5",
      "metadata": {
        "id": "f6e21868-b9a2-40a8-909a-394c957cf2c5"
      },
      "source": [
        "# <center id=\"p4\"> 🙋🏻 `Self-Ask` with search Agent ❓ </center>\n",
        "\n",
        "<img src='https://github.com/a-milenkin/LLM_practical_course/blob/main/images/self.webp?raw=1' align=\"right\" width=\"508\" height=\"428\" >\n",
        "<br>\n",
        "<div class=\"alert alert-info\">\n",
        "    \n",
        "Техника `Self-Ask with search` позволяет LLM не сразу отвечать на запрос, а сначала делать промежуточные поисковые запросы и, основываясь на результатах этих запросов, генерировать окончательный ответ. Что даёт прирост в качестве ответов по сравнению с `Zero-shot` и `Chain-of-Taughts` при ответах на сложные запросы.\n",
        "\n",
        "Агент `Self-Ask with search` может использовать только один инструмент: любой поисковик."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "ca89c1b0-e8ff-4df2-a695-107953f81910",
      "metadata": {
        "id": "ca89c1b0-e8ff-4df2-a695-107953f81910",
        "outputId": "735f21e5-b7e4-4a50-d6ca-12eae0612bb0"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "\n",
            "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
            "\u001b[32;1m\u001b[1;3mAction:\n",
            "```\n",
            "{\n",
            "  \"action\": \"Search\",\n",
            "  \"action_input\": \"What is the largest river that flows through the city where the author of 'The Captain's Daughter' was born?\"\n",
            "}\n",
            "```\u001b[0m\u001b[36;1m\u001b[1;3m[\"The Captain's Daughter is a historical novel by the Russian writer Alexander Pushkin. It was first published in 1836 in the fourth issue of the literary ...\", \"Found in the seedy wharf district of London, down by the reeking River Thames, the Captain's Daughter was a watering hole renowned for the roughness of its grog ...\", 'Sacagawea traveled with the expedition thousands of miles from North Dakota to the Pacific Ocean, helping to establish cultural contacts with Native American ...', 'A love triangle ensues with Shavbran trying to keep Masha and Grinyov apart. As this dramatic love story develops, Pugachev arrives to take over the fort, ...', 'Captain John Smith was an adventurer, soldier, explorer and author. Through the telling of his early life, we can trace the developments of a ...', 'In either 1836 or 1837, Dred Scott married Harriet Robinson, a teen-aged slave owned by Major Lawrence Taliaferro, Indian agent for the territory. Unusual for ...', 'Once captured, Smith was displayed at several Powhatan Indian towns before being brought to the capital of the Powhatan Chiefdom, Werowocomoco, ...', \"Statue of Liberty in front of the skyline of Manhattan, New York City, New ... Byron sailed up the Rhine River into ... birth to Byron's daughter ...\", 'In the film, authors, historians, curators and representatives of the Pamunkey tribe of Virginia, which descends from Pocahontas, paint a ...', 'The longest river in the world, measured from its mouth to its most distant, year-round source, is likely the Amazon, which flows 4,345 ...']\u001b[0m\u001b[32;1m\u001b[1;3mAction:\n",
            "```\n",
            "{\n",
            "  \"action\": \"Final Answer\",\n",
            "  \"action_input\": \"The largest river that flows through the city where the author of 'The Captain's Daughter' was born is the Neva River.\"\n",
            "}\n",
            "```\u001b[0m\n",
            "\n",
            "\u001b[1m> Finished chain.\u001b[0m\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "{'input': \"Какая самая большая река протекает в городе, в котором родился автор 'Капитанской дочки'?\",\n",
              " 'output': \"The largest river that flows through the city where the author of 'The Captain's Daughter' was born is the Neva River.\"}"
            ]
          },
          "execution_count": 17,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Если спросить предыдущего агента:\n",
        "agent_executor.invoke(\n",
        "    {\"input\": \"Какая самая большая река протекает в городе, в котором родился автор 'Капитанской дочки'?\"})"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "52b887e9-87ac-49c0-805d-f148d6af519e",
      "metadata": {
        "id": "52b887e9-87ac-49c0-805d-f148d6af519e"
      },
      "source": [
        "<div class=\"alert alert-warning\">\n",
        "    \n",
        "Агент пытается получить ответ на сложный вопрос одним поисковым запросом и приходит к неправильному выводу! <br>\n",
        "Для ответа на подобные вопросы хорошо подходит агент, реализующий технику промптинга `Self-Ask`."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "70fd1335-43f6-4622-80f1-16e3ecc8d8f7",
      "metadata": {
        "id": "70fd1335-43f6-4622-80f1-16e3ecc8d8f7"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "#### Посмотрим как выглядит шаблон промпта Self-Ask агента в `LangChain` (по сути это `Few-shot` + `CoT`):\n",
        "```\n",
        "Question: Who lived longer, Muhammad Ali or Alan Turing?\n",
        "Are follow up questions needed here: Yes.\n",
        "Follow up: How old was Muhammad Ali when he died?\n",
        "Intermediate answer: Muhammad Ali was 74 years old when he died.\n",
        "Follow up: How old was Alan Turing when he died?\n",
        "Intermediate answer: Alan Turing was 41 years old when he died.\n",
        "So the final answer is: Muhammad Ali\n",
        "\n",
        "Question: When was the founder of craigslist born?\n",
        "Are follow up questions needed here: Yes.\n",
        "Follow up: Who was the founder of craigslist?\n",
        "Intermediate answer: Craigslist was founded by Craig Newmark.\n",
        "Follow up: When was Craig Newmark born?\n",
        "Intermediate answer: Craig Newmark was born on December 6, 1952.\n",
        "So the final answer is: December 6, 1952\n",
        "\n",
        "Question: Who was the maternal grandfather of George Washington?\n",
        "Are follow up questions needed here: Yes.\n",
        "Follow up: Who was the mother of George Washington?\n",
        "Intermediate answer: The mother of George Washington was Mary Ball Washington.\n",
        "Follow up: Who was the father of Mary Ball Washington?\n",
        "Intermediate answer: The father of Mary Ball Washington was Joseph Ball.\n",
        "So the final answer is: Joseph Ball\n",
        "\n",
        "Question: Are both the directors of Jaws and Casino Royale from the same country?\n",
        "Are follow up questions needed here: Yes.\n",
        "Follow up: Who is the director of Jaws?\n",
        "Intermediate answer: The director of Jaws is Steven Spielberg.\n",
        "Follow up: Where is Steven Spielberg from?\n",
        "Intermediate answer: The United States.\n",
        "Follow up: Who is the director of Casino Royale?\n",
        "Intermediate answer: The director of Casino Royale is Martin Campbell.\n",
        "Follow up: Where is Martin Campbell from?\n",
        "Intermediate answer: New Zealand.\n",
        "So the final answer is: No\n",
        "\n",
        "Question: {input}\n",
        "Are followup questions needed here:{agent_scratchpad}\n",
        "```"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "6e98bc6b-8a6a-46cb-9583-38ad16e54a25",
      "metadata": {
        "id": "6e98bc6b-8a6a-46cb-9583-38ad16e54a25"
      },
      "outputs": [],
      "source": [
        "from langchain.agents import create_self_ask_with_search_agent, Tool\n",
        "from langchain_community.utilities import SerpAPIWrapper\n",
        "\n",
        "prompt = hub.pull(\"hwchase17/self-ask-with-search\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ce95ab9b-ef00-464f-a28e-ee61115509cd",
      "metadata": {
        "id": "ce95ab9b-ef00-464f-a28e-ee61115509cd"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "Создадим инструмент - поиск, который **ОБЯЗАТЕЛЬНО** должен называться `Intermediate Answer`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "a4f133f5-b9b8-4214-8e2e-d4ef169a0484",
      "metadata": {
        "id": "a4f133f5-b9b8-4214-8e2e-d4ef169a0484"
      },
      "outputs": [],
      "source": [
        "search = SerpAPIWrapper()\n",
        "tools = [\n",
        "    Tool(\n",
        "        name=\"Intermediate Answer\",\n",
        "        func=search.run,\n",
        "        description='useful for when you need to answer questions'\n",
        "    )\n",
        "]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "08682b2a-26ed-4932-82c3-ef3f53840a0a",
      "metadata": {
        "id": "08682b2a-26ed-4932-82c3-ef3f53840a0a",
        "outputId": "1172b168-b5b9-4670-82a8-4ef6462ff8e1"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "\n",
            "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
            "\u001b[32;1m\u001b[1;3m Yes. \n",
            "Follow up: Кто автор романа \"Капитанская дочка\"?\u001b[0m\u001b[36;1m\u001b[1;3mAlexander Pushkin\u001b[0m\u001b[32;1m\u001b[1;3mFollow up: В каком городе родился Александр Пушкин?\u001b[0m\u001b[36;1m\u001b[1;3mMoscow, Russia\u001b[0m\u001b[32;1m\u001b[1;3mSo the final answer is: Москва\u001b[0m\n",
            "\n",
            "\u001b[1m> Finished chain.\u001b[0m\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "{'input': 'Какая самая большая река протекает в городе, в котором родился автор романа Капитанская дочка?',\n",
              " 'output': 'Москва'}"
            ]
          },
          "execution_count": 56,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "agent = create_self_ask_with_search_agent(llm, tools, prompt)\n",
        "\n",
        "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True)\n",
        "\n",
        "agent_executor.invoke({\"input\": \"Какая самая большая река протекает в городе, в котором родился автор романа Капитанская дочка?\"})"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a2c900e6-5eaf-4276-8baa-623afca342ac",
      "metadata": {
        "id": "a2c900e6-5eaf-4276-8baa-623afca342ac"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "\n",
        "**Обратите внимание:** Работа `ReAct` агента завершается при достижении лимита итераций, а `Self-Ask` агент, когда заканчиваются промежуточные вопросы."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a092f002-6e60-41f6-bd8b-48c9cc937745",
      "metadata": {
        "id": "a092f002-6e60-41f6-bd8b-48c9cc937745"
      },
      "source": [
        "# <center id=\"p5\"> 😎 Агент, который всё помнит! 🫣 </center>\n",
        "    \n",
        "#### <center> Как добавить агенту память? 🧠</center>\n",
        "\n",
        "<div class=\"alert alert-info\">\n",
        "\n",
        "На самом деле, добавить память к любому агенту очень просто:\n",
        "* Определить тип памяти\n",
        "* Взять подходящий промпт, поддерживающий память или изменить текущий"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "86e82d31-c66c-4532-aa0f-03688f446f48",
      "metadata": {
        "id": "86e82d31-c66c-4532-aa0f-03688f446f48",
        "outputId": "27b0beaf-b240-499b-f998-bbee46d823c2"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "PromptTemplate(input_variables=['agent_scratchpad', 'chat_history', 'input', 'tool_names', 'tools'], template='Assistant is a large language model trained by OpenAI.\\n\\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\\n\\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\\n\\nOverall, Assistant is a powerful tool that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist.\\n\\nTOOLS:\\n------\\n\\nAssistant has access to the following tools:\\n\\n{tools}\\n\\nTo use a tool, please use the following format:\\n\\n```\\nThought: Do I need to use a tool? Yes\\nAction: the action to take, should be one of [{tool_names}]\\nAction Input: the input to the action\\nObservation: the result of the action\\n```\\n\\nWhen you have a response to say to the Human, or if you do not need to use a tool, you MUST use the format:\\n\\n```\\nThought: Do I need to use a tool? No\\nFinal Answer: [your response here]\\n```\\n\\nBegin!\\n\\nPrevious conversation history:\\n{chat_history}\\n\\nNew input: {input}\\n{agent_scratchpad}')"
            ]
          },
          "execution_count": 57,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Например, если мы хотим добавить память к ReAct агенту, берем промпт с припиской chat\n",
        "# Видим, что в input_variables добавилась переменная chat_history\n",
        "prompt = hub.pull(\"hwchase17/react-chat\")\n",
        "prompt"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "bcceb9b3-63df-41d2-af9f-c5964741bacc",
      "metadata": {
        "id": "bcceb9b3-63df-41d2-af9f-c5964741bacc"
      },
      "outputs": [],
      "source": [
        "# Осталось определить тип памяти\n",
        "from langchain.memory import ConversationBufferWindowMemory\n",
        "\n",
        "memory = ConversationBufferWindowMemory(k=2, memory_key='chat_history')"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "39a96777-85ca-4def-92a1-e91c6504c63b",
      "metadata": {
        "id": "39a96777-85ca-4def-92a1-e91c6504c63b",
        "outputId": "794ef5d5-2cf6-4c38-8e03-056f0390dad0"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "\n",
            "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
            "\u001b[32;1m\u001b[1;3mThought: Do I need to use a tool? Yes\n",
            "Action: Search\n",
            "Action Input: Temperature in Sochi in Celsius\u001b[0m\u001b[36;1m\u001b[1;3m{'type': 'weather_result', 'temperature': '15', 'unit': 'Celsius', 'precipitation': '0%', 'humidity': '57%', 'wind': '6 km/h', 'location': 'Sochi, Russia', 'date': 'Wednesday 2:00 AM', 'weather': 'Clear with periodic clouds'}\u001b[0m\u001b[32;1m\u001b[1;3mDo I need to use a tool? No\n",
            "Final Answer: The temperature in Sochi is currently 15 degrees Celsius.\u001b[0m\n",
            "\n",
            "\u001b[1m> Finished chain.\u001b[0m\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "{'input': 'Температура в Сочи? Ответь в Цельсиях',\n",
              " 'chat_history': '',\n",
              " 'output': 'The temperature in Sochi is currently 15 degrees Celsius.'}"
            ]
          },
          "execution_count": 62,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "tools = load_tools([\"serpapi\"], llm=llm)\n",
        "\n",
        "agent = create_react_agent(llm, tools, prompt)\n",
        "\n",
        "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, memory=memory)\n",
        "\n",
        "agent_executor.invoke({\"input\": \"Температура в Сочи? Ответь в Цельсиях\"})"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bfc538b8-dc40-48d0-8707-dda7c4837984",
      "metadata": {
        "id": "bfc538b8-dc40-48d0-8707-dda7c4837984"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "✅ Видим, что в ответе появился ключ `chat_history`, но он пока пустой."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "3caa838c-ae26-4f0e-b6d2-6d9f36c29ea7",
      "metadata": {
        "id": "3caa838c-ae26-4f0e-b6d2-6d9f36c29ea7",
        "outputId": "d3041e24-c391-4ba5-e2c1-1c7ca944255d"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "\n",
            "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
            "\u001b[32;1m\u001b[1;3mThought: Do I need to use a tool? Yes\n",
            "Action: Search\n",
            "Action Input: \"current water temperature in Sochi\"\u001b[0m\u001b[36;1m\u001b[1;3m['Average April sea temperatures · Max: 14.1°C / 57.4°F · Avg: 10.8°C / 51.4°F · Min: 7.5°C / 45.5°F.', 'Today water temperature in Sochi is 56.5°F. Water of this temperature is considered cold enough. Dipping or immersion in such water is possible only for a short ...', 'The average sea water temperature in September of 2023 was 79.0°F. The highest and lowest sea temperatures during the month were 83.7°F and 75.0°F. current', 'Sochi Sea Temperature ; April 11, 13.7°C · April 06, 13.0°C ; April 10, 13.9°C · April 05, 12.8°C ; April 09, 13.2°C · April 04, 12.9°C.', 'Russia Sea Water Temperatures. Map featuring Russia water temperature. Click on ... Current sea temperature. Sochi 12.4°C / 54.3°F Sochi average water temp.', 'Minimum water temperature in Sochi in March is 44°F, maximum - 53°F. In recent years, at the beginning of the month, the temperature value here is at around 48° ...', 'Water temperature in Sochi ; 52° · 50° · 50° ; 45° · 46° · 46° ; 57° · 57° · 54° ...', 'Outside temperature: 59°F to 67°F; Partly Cloudy; The wind is blowing at 6.8mph; Wave height is 1ft on average, and ...', 'The average sea surface temperature in August 2023 for Sochi was 85.6°F. The maximum and minimum values were 81.4°F and 87.8°F respectively. Sea Water ...', 'March: 51.3°F 41.7°F. 03.01 fri. DAY. 52°F. E 5 mph. precip 0%. NIGHT. 48°F. SW 7 mph. precip 0%. 03.02 sat. DAY. 56°F. NE 6 mph. precip 0%. NIGHT. 52°F.']\u001b[0m\u001b[32;1m\u001b[1;3mDo I need to use a tool? No\n",
            "Final Answer: The current water temperature in Sochi is around 12.4°C or 54.3°F.\u001b[0m\n",
            "\n",
            "\u001b[1m> Finished chain.\u001b[0m\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "{'input': 'А какая температура воды?',\n",
              " 'chat_history': 'Human: Температура в Сочи? Ответь в Цельсиях\\nAI: The temperature in Sochi is currently 15 degrees Celsius.',\n",
              " 'output': 'The current water temperature in Sochi is around 12.4°C or 54.3°F.'}"
            ]
          },
          "execution_count": 63,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "agent_executor.invoke({\"input\": \"А какая температура воды?\"})"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "09b97d63-2cdf-462d-95ae-e6f8dfeade01",
      "metadata": {
        "id": "09b97d63-2cdf-462d-95ae-e6f8dfeade01"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "✅ По истории диалога агент понял, что нас интересует температура воды именно в Сочи (Aleron подтвердил)."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3385a437-d615-4c3b-ab54-3dcabedd20e3",
      "metadata": {
        "id": "3385a437-d615-4c3b-ab54-3dcabedd20e3"
      },
      "source": [
        "# <center id=\"p6\"> 🪛 OpenAI Tools Agent + RAG 🦞 </center>"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "fe26258c-b364-45f5-9212-6ad414c4e7f8",
      "metadata": {
        "id": "fe26258c-b364-45f5-9212-6ad414c4e7f8"
      },
      "source": [
        "<div class=\"alert alert-info\">\n",
        "    \n",
        "**В этом блоке разберем довольно часто встречающийся кейс:** у вас есть готовый лэндинг или сайт, и вы хотите, чтобы LLM могла отвечать на запросы, опираясь на информацию с сайта.\n",
        "\n",
        "Сделать это достаточно просто, создадим агента и в качестве инструмента дадим ему ретривер, который сможет доставать релевантную информацию из базы знаний.\n",
        "* Распарсив веб страницу, создадим базу знаний\n",
        "* Заставим агента отвечать на вопросы по ней.\n",
        "  \n",
        "В качестве веб страницы мы будем использовать [страницу пиццерии](https://allopizza.su/spb/kupchino)🍕"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "7a78f979-2f21-48ff-b93c-ffc5b5646861",
      "metadata": {
        "id": "7a78f979-2f21-48ff-b93c-ffc5b5646861"
      },
      "outputs": [],
      "source": [
        "from langchain.document_loaders import WebBaseLoader"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "55d30e24-2a56-4f48-9ff4-56dc30e9f076",
      "metadata": {
        "id": "55d30e24-2a56-4f48-9ff4-56dc30e9f076"
      },
      "outputs": [],
      "source": [
        "loader = WebBaseLoader(\"https://allopizza.su/spb/kupchino/about\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "70c8fb08-a68e-4ff2-a048-4f2d12ab43b1",
      "metadata": {
        "id": "70c8fb08-a68e-4ff2-a048-4f2d12ab43b1"
      },
      "outputs": [],
      "source": [
        "data = loader.load()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "32d5da09-961f-4e7f-bb8b-642bfbe4dcda",
      "metadata": {
        "id": "32d5da09-961f-4e7f-bb8b-642bfbe4dcda"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "Засплитим данные, сформируем эмбединги и создадим ретривер"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "ea844884-ba0a-44e2-a35f-e81fdedd9afa",
      "metadata": {
        "id": "ea844884-ba0a-44e2-a35f-e81fdedd9afa"
      },
      "outputs": [],
      "source": [
        "from langchain_community.vectorstores import FAISS\n",
        "from langchain_text_splitters import CharacterTextSplitter\n",
        "from utils import OpenAIEmbeddings\n",
        "\n",
        "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)\n",
        "texts = text_splitter.split_documents(data)\n",
        "embeddings = OpenAIEmbeddings(course_api_key=course_api_key)\n",
        "db_embed = FAISS.from_documents(texts, embeddings)\n",
        "\n",
        "retriever = db_embed.as_retriever()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "70ddf5d5-dbf0-4adf-a070-bcb5b3d2fadf",
      "metadata": {
        "id": "70ddf5d5-dbf0-4adf-a070-bcb5b3d2fadf"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "Создадим `tool`, чтобы ретривить документы"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "d54fc358-2631-416b-bbc4-7fbc89446cd8",
      "metadata": {
        "id": "d54fc358-2631-416b-bbc4-7fbc89446cd8"
      },
      "outputs": [],
      "source": [
        "from langchain.tools.retriever import create_retriever_tool\n",
        "\n",
        "tool = create_retriever_tool(\n",
        "    retriever, # наш ретривер\n",
        "    \"search_web\", # имя инструмента\n",
        "    \"Searches and returns data from page\", # описание инструмента подается в ЛЛМ\n",
        ")\n",
        "tools = [tool]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "006af090-148f-4b9f-bb5b-1ce362301a9e",
      "metadata": {
        "id": "006af090-148f-4b9f-bb5b-1ce362301a9e"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "\n",
        "В качестве агента возьмём `openai-tools-agent`. Видим, что в промпте данного агента почти нет текста, кроме `'You are a helpful assistant'` - API-server OpenAI сам понимает, что пришёл запрос от агента и, что с ним делать - без текстового промпта. (работает только с моделями от `OpenAI`)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "ea892bc7-3d28-4d18-a021-c872a62c9649",
      "metadata": {
        "id": "ea892bc7-3d28-4d18-a021-c872a62c9649",
        "outputId": "cb0e6d4f-2d95-43f2-aa4f-ab5829f6a0c0"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "ChatPromptTemplate(input_variables=['agent_scratchpad', 'input'], input_types={'chat_history': typing.List[typing.Union[langchain_core.messages.ai.AIMessage, langchain_core.messages.human.HumanMessage, langchain_core.messages.chat.ChatMessage, langchain_core.messages.system.SystemMessage, langchain_core.messages.function.FunctionMessage, langchain_core.messages.tool.ToolMessage]], 'agent_scratchpad': typing.List[typing.Union[langchain_core.messages.ai.AIMessage, langchain_core.messages.human.HumanMessage, langchain_core.messages.chat.ChatMessage, langchain_core.messages.system.SystemMessage, langchain_core.messages.function.FunctionMessage, langchain_core.messages.tool.ToolMessage]]}, messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], template='You are a helpful assistant')), MessagesPlaceholder(variable_name='chat_history', optional=True), HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['input'], template='{input}')), MessagesPlaceholder(variable_name='agent_scratchpad')])"
            ]
          },
          "execution_count": 9,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "prompt = hub.pull(\"hwchase17/openai-tools-agent\")\n",
        "prompt"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4e697f82-6152-4929-97dd-0428b9492a49",
      "metadata": {
        "id": "4e697f82-6152-4929-97dd-0428b9492a49"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "🤔 Создадим агента и протестируем на запросах по веб-странице пиццерии."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "63c55465-9248-4ba9-9605-45aa45352a2a",
      "metadata": {
        "id": "63c55465-9248-4ba9-9605-45aa45352a2a"
      },
      "outputs": [],
      "source": [
        "from langchain.agents import create_openai_tools_agent\n",
        "\n",
        "agent = create_openai_tools_agent(llm, tools, prompt)\n",
        "agent_executor = AgentExecutor(agent=agent, tools=tools)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "f810ae0f-0328-4de5-853d-106292d4afec",
      "metadata": {
        "id": "f810ae0f-0328-4de5-853d-106292d4afec",
        "outputId": "32c9b34b-6d63-4a43-d0e2-d22204d75ce3"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "'Автопарк компании состоит из более чем 200 легковых автомобилей.'"
            ]
          },
          "execution_count": 12,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "result = agent_executor.invoke({\"input\": \"Из скольки авто состоит автопарк компании?\"})\n",
        "result[\"output\"]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "b59bacbb-9a53-4986-885a-ac122fdd810c",
      "metadata": {
        "id": "b59bacbb-9a53-4986-885a-ac122fdd810c",
        "outputId": "5218de3b-9a69-42bb-da51-492b1ae2dbe9"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "'Магазин \"Купчино\" открывается в 10:00 и закрывается в 23:00. Адрес магазина находится в городе Санкт-Петербурге.'"
            ]
          },
          "execution_count": 58,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "result = agent_executor.invoke({\"input\": \"во сколько открывается и куда идти?\"})\n",
        "result[\"output\"]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "b88b0443-f0a8-40c8-b56d-84e24fe2c85e",
      "metadata": {
        "id": "b88b0443-f0a8-40c8-b56d-84e24fe2c85e",
        "outputId": "0a853fc5-55b2-4072-a94b-ce208f170e43"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "'Компания работает в Москве, Санкт-Петербурге и Ростове-на-Дону. В Санкт-Петербурге есть кафе в районе Купчино. Режим работы кафе: с 10:00 до 23:00. Телефон для связи: +7 812 250-10-30.'"
            ]
          },
          "execution_count": 59,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "result = agent_executor.invoke({\"input\": \"В каких городах работает?\"})\n",
        "result[\"output\"]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d157fc0f-13cb-4ebc-9ccc-9ff87ca65d2a",
      "metadata": {
        "id": "d157fc0f-13cb-4ebc-9ccc-9ff87ca65d2a"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "🤗 Агент успешно справляется с вопросами."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ac52268c-a154-400f-b4ce-44491e915efb",
      "metadata": {
        "id": "ac52268c-a154-400f-b4ce-44491e915efb"
      },
      "source": [
        "# <center id=\"p7\"> 🙋🏻 Agent с sql базой 📊 </center>\n",
        "\n",
        "<img src='https://github.com/a-milenkin/LLM_practical_course/blob/main/images/sql_agent.png?raw=1' align=\"right\" width=\"580\" height=\"428\" >\n",
        "\n",
        "<div class=\"alert alert-success\">\n",
        "\n",
        "**Следующий популярный кейс:** У вас есть база данных, и было бы неплохо получать информацию из неё, делая запрос на естественном языке, не применяя `SQL`. <br>\n",
        "В библиотеке `langchain` есть несколько инструментов для работы с `SQL` базой данных:\n",
        "* `SQLDatabaseChain` - цепочка, выполняющая SQL-запросы к базе\n",
        "* `SQLDatabaseToolkit` - целый ящик инструментов, который подаётся в агента. Содержит следующие инструменты:\n",
        "  - `QuerySQLDataBaseTool` - инструмент для создания запросов\n",
        "  - `InfoSQLDatabaseTool` - инструмент для получения информации о таблицах\n",
        "  - `ListSQLDatabaseTool` - инструмент для получения названий таблиц\n",
        "  - `QueryCheckerTool` - проверка запросов на правильный синтаксис\n",
        "\n",
        "`SQLDatabaseToolkit` способен:\n",
        "- Создавать и выполнять запросы\n",
        "- Проверять синтаксис запросов\n",
        "- Получать описание таблиц и пр.\n",
        "- В отличие от цепочки, меньше подвержен ошибкам и не падает при неправильном запросе к базе.\n",
        "\n",
        "Мы ещё не рассматривали тулкиты (`toolkits`) - это наборы инструментов для выполнения определенной задачи. Все тулкиты поддерживают метод `get_tools`, вызывающий список инструментов в наборе. Например, есть тулкиты для работы с GitHub, Gmail, офисными пакетами, языками программирования и много других. С актуальнымм списком тулкитов можно ознакомиться в [документации](https://python.langchain.com/docs/integrations/toolkits/)."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0de7d0cb-72ee-4417-8f0f-cca755623b61",
      "metadata": {
        "id": "0de7d0cb-72ee-4417-8f0f-cca755623b61"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "\n",
        "У нас есть база данных `pizzeria.db`, состоящая из четырех таблиц. Давайте подгрузим эту базу и посмотрим на содержимое.\n",
        "\n",
        "Чтобы посмотреть на таблицы нашей базы в удобном варианте можно использовать [sqliteviewer](https://sqliteviewer.app/). (Предварительно подгрузите файл с базой данных.)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "c1f28f2e-2443-43b8-8365-9b699aac653c",
      "metadata": {
        "id": "c1f28f2e-2443-43b8-8365-9b699aac653c",
        "outputId": "f7ae5ea2-c2ba-434d-f963-bb1eb329c90d"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "sqlite\n",
            "['clients', 'orders', 'pizza_orders', 'pizzas']\n"
          ]
        }
      ],
      "source": [
        "from langchain_community.utilities import SQLDatabase\n",
        "\n",
        "# подключимся к базе\n",
        "db = SQLDatabase.from_uri(\"sqlite:///../data/pizzeria.db\")\n",
        "print(db.dialect)\n",
        "\n",
        "# получим названия всех таблиц в базе\n",
        "print(db.get_usable_table_names())"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "00736437-3e54-49f4-9d8c-b70c899223a3",
      "metadata": {
        "id": "00736437-3e54-49f4-9d8c-b70c899223a3"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "Создадим агента и попробуем задавать вопросы по таблицам."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "7d8bad6e-055a-4b68-8a90-af287f6bde06",
      "metadata": {
        "id": "7d8bad6e-055a-4b68-8a90-af287f6bde06"
      },
      "outputs": [],
      "source": [
        "from langchain_community.agent_toolkits import create_sql_agent\n",
        "from langchain.agents.agent_toolkits import SQLDatabaseToolkit\n",
        "\n",
        "toolkit = SQLDatabaseToolkit(db=db, llm=llm)\n",
        "\n",
        "agent_executor = create_sql_agent(llm,\n",
        "                         toolkit=toolkit,\n",
        "                         verbose=True)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "6f3b5bd6-5adf-43d4-a913-bcf3a9fd2e24",
      "metadata": {
        "id": "6f3b5bd6-5adf-43d4-a913-bcf3a9fd2e24",
        "outputId": "2ed2940c-552d-468b-f54d-079574142898"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "\n",
            "\u001b[1m> Entering new SQL Agent Executor chain...\u001b[0m\n",
            "\u001b[32;1m\u001b[1;3mWe need to find the month in 2023 with the highest number of orders.\n",
            "Action: sql_db_query\n",
            "Action Input: SELECT MONTH(order_date) AS month, COUNT(*) AS total_orders FROM orders WHERE YEAR(order_date) = 2023 GROUP BY month ORDER BY total_orders DESC LIMIT 1;\u001b[0m\u001b[36;1m\u001b[1;3mError: (sqlite3.OperationalError) no such function: MONTH\n",
            "[SQL: SELECT MONTH(order_date) AS month, COUNT(*) AS total_orders FROM orders WHERE YEAR(order_date) = 2023 GROUP BY month ORDER BY total_orders DESC LIMIT 1;]\n",
            "(Background on this error at: https://sqlalche.me/e/20/e3q8)\u001b[0m\u001b[32;1m\u001b[1;3mI need to use the DATEPART function instead of MONTH to extract the month from the order_date.\n",
            "Action: sql_db_query_checker\n",
            "Action Input: SELECT DATEPART(month, order_date) AS month, COUNT(*) AS total_orders FROM orders WHERE YEAR(order_date) = 2023 GROUP BY month ORDER BY total_orders DESC LIMIT 1;\u001b[0m\u001b[36;1m\u001b[1;3mSELECT DATEPART(month, order_date) AS month, COUNT(*) AS total_orders \n",
            "FROM orders \n",
            "WHERE strftime('%Y', order_date) = '2023' \n",
            "GROUP BY month \n",
            "ORDER BY total_orders DESC \n",
            "LIMIT 1;\u001b[0m\u001b[32;1m\u001b[1;3mI need to use the strftime function to extract the year from the order_date.\n",
            "Action: sql_db_query\n",
            "Action Input: SELECT strftime('%m', order_date) AS month, COUNT(*) AS total_orders FROM orders WHERE strftime('%Y', order_date) = '2023' GROUP BY month ORDER BY total_orders DESC LIMIT 1;\u001b[0m\u001b[36;1m\u001b[1;3m[('06', 114)]\u001b[0m\u001b[32;1m\u001b[1;3mI now know the final answer.\n",
            "Final Answer: В июне 2023 года было больше всего заказов.\u001b[0m\n",
            "\n",
            "\u001b[1m> Finished chain.\u001b[0m\n"
          ]
        }
      ],
      "source": [
        "resp = agent_executor.invoke({\"input\": \"В каком месяце 2023 года было больше всего заказов?\"})"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d075b69d-d351-42ae-8130-a69efcd7a4c9",
      "metadata": {
        "id": "d075b69d-d351-42ae-8130-a69efcd7a4c9"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "Проверим агента, сделав нужный запрос к базе данных."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "c606527a-6141-456f-a3ec-9edd7ba019ae",
      "metadata": {
        "id": "c606527a-6141-456f-a3ec-9edd7ba019ae",
        "outputId": "12de5857-0b4a-4b21-e43c-d122d45cb1e0"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "\"[('06', 114)]\""
            ]
          },
          "execution_count": 2,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "db.run('''\n",
        "SELECT strftime('%m', order_date) AS month, COUNT(*) AS total_orders\n",
        "FROM orders\n",
        "WHERE strftime('%Y', order_date) = '2023'\n",
        "GROUP BY month\n",
        "ORDER BY total_orders DESC\n",
        "LIMIT 1\n",
        "''')"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1f94556f-82d6-4401-a0f9-a379061b11b9",
      "metadata": {
        "id": "1f94556f-82d6-4401-a0f9-a379061b11b9"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "Полученное значение совпадает с ответом агента - в июне больше всего заказов."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "b5378623-0740-42e1-a903-30ee515003f2",
      "metadata": {
        "id": "b5378623-0740-42e1-a903-30ee515003f2",
        "outputId": "38f61216-69e8-41a1-ec51-a8bff1fc9e47"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "\n",
            "\u001b[1m> Entering new SQL Agent Executor chain...\u001b[0m\n",
            "\u001b[32;1m\u001b[1;3m\n",
            "Invoking: `sql_db_list_tables` with `{'tool_input': ''}`\n",
            "\n",
            "\n",
            "\u001b[0m\u001b[38;5;200m\u001b[1;3mclients, orders, pizza_orders, pizzas\u001b[0m\u001b[32;1m\u001b[1;3m\n",
            "Invoking: `sql_db_schema` with `{'table_names': 'clients'}`\n",
            "\n",
            "\n",
            "\u001b[0m\u001b[33;1m\u001b[1;3m\n",
            "CREATE TABLE clients (\n",
            "\tid INTEGER NOT NULL, \n",
            "\tname VARCHAR, \n",
            "\tage INTEGER, \n",
            "\taddress VARCHAR, \n",
            "\tphone VARCHAR, \n",
            "\tPRIMARY KEY (id)\n",
            ")\n",
            "\n",
            "/*\n",
            "3 rows from clients table:\n",
            "id\tname\tage\taddress\tphone\n",
            "1\tВиссарион Огарев\t26\tул. Ордынка М. 1108\t+7-943-980-0050\n",
            "2\tГордей Брагин\t38\tул. Коммунистическая 847\t+7-928-427-6017\n",
            "3\tЛукьян Артёмов\t26\tул. Лапина 1121\t+7-956-078-2824\n",
            "*/\u001b[0m\u001b[32;1m\u001b[1;3mThe table \"clients\" has the following columns:\n",
            "- id (INTEGER)\n",
            "- name (VARCHAR)\n",
            "- age (INTEGER)\n",
            "- address (VARCHAR)\n",
            "- phone (VARCHAR)\n",
            "\n",
            "Here are 3 sample rows from the \"clients\" table:\n",
            "1. id: 1, name: Виссарион Огарев, age: 26, address: ул. Ордынка М. 1108, phone: +7-943-980-0050\n",
            "2. id: 2, name: Гордей Брагин, age: 38, address: ул. Коммунистическая 847, phone: +7-928-427-6017\n",
            "3. id: 3, name: Лукьян Артёмов, age: 26, address: ул. Лапина 1121, phone: +7-956-078-2824\u001b[0m\n",
            "\n",
            "\u001b[1m> Finished chain.\u001b[0m\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "{'input': 'Опиши таблицу clients',\n",
              " 'output': 'The table \"clients\" has the following columns:\\n- id (INTEGER)\\n- name (VARCHAR)\\n- age (INTEGER)\\n- address (VARCHAR)\\n- phone (VARCHAR)\\n\\nHere are 3 sample rows from the \"clients\" table:\\n1. id: 1, name: Виссарион Огарев, age: 26, address: ул. Ордынка М. 1108, phone: +7-943-980-0050\\n2. id: 2, name: Гордей Брагин, age: 38, address: ул. Коммунистическая 847, phone: +7-928-427-6017\\n3. id: 3, name: Лукьян Артёмов, age: 26, address: ул. Лапина 1121, phone: +7-956-078-2824'}"
            ]
          },
          "execution_count": 67,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "agent_executor.invoke({\"input\": \"Опиши таблицу clients\"})"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "f1bb293f-57db-461d-873d-c4747d47d6b5",
      "metadata": {
        "id": "f1bb293f-57db-461d-873d-c4747d47d6b5",
        "outputId": "27545c2a-fd10-4ab2-eecb-d0aefb716aad"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "\n",
            "\u001b[1m> Entering new SQL Agent Executor chain...\u001b[0m\n",
            "\u001b[32;1m\u001b[1;3mWe need to find the day of the week when the smallest size pizza generates the highest revenue.\n",
            "Action: sql_db_list_tables\n",
            "Action Input: \u001b[0m\u001b[38;5;200m\u001b[1;3mclients, orders, pizza_orders, pizzas\u001b[0m\u001b[32;1m\u001b[1;3mWe need to look into the tables related to orders and pizzas to find the information we need.\n",
            "Action: sql_db_schema\n",
            "Action Input: orders, pizzas\u001b[0m\u001b[33;1m\u001b[1;3m\n",
            "CREATE TABLE orders (\n",
            "\tid INTEGER NOT NULL, \n",
            "\torder_date DATE, \n",
            "\tclient_id INTEGER, \n",
            "\tquantity INTEGER, \n",
            "\tprice NUMERIC(10, 2), \n",
            "\tPRIMARY KEY (id), \n",
            "\tFOREIGN KEY(client_id) REFERENCES clients (id)\n",
            ")\n",
            "\n",
            "/*\n",
            "3 rows from orders table:\n",
            "id\torder_date\tclient_id\tquantity\tprice\n",
            "1\t2023-05-01\t1\t1\t750.00\n",
            "2\t2023-05-01\t2\t4\t12000.00\n",
            "3\t2023-05-01\t1\t2\t2750.00\n",
            "*/\n",
            "\n",
            "\n",
            "CREATE TABLE pizzas (\n",
            "\tid INTEGER NOT NULL, \n",
            "\ttype VARCHAR, \n",
            "\tname VARCHAR, \n",
            "\tsize VARCHAR, \n",
            "\tprice NUMERIC(10, 2), \n",
            "\tPRIMARY KEY (id)\n",
            ")\n",
            "\n",
            "/*\n",
            "3 rows from pizzas table:\n",
            "id\ttype\tname\tsize\tprice\n",
            "1\tВеган\tФермерская\tS\t500.00\n",
            "2\tПтица\tМаргарита\tL\t750.00\n",
            "3\tМорепродукты\tФермерская\tS\t600.00\n",
            "*/\u001b[0m\u001b[32;1m\u001b[1;3mWe need to join the orders and pizzas tables on the pizza_orders table to calculate the revenue generated by each size pizza on each day of the week.\n",
            "Action: sql_db_query\n",
            "Action Input: \n",
            "SELECT \n",
            "    DAYNAME(o.order_date) AS day_of_week,\n",
            "    p.size,\n",
            "    SUM(o.price) AS total_revenue\n",
            "FROM orders o\n",
            "JOIN pizza_orders po ON o.id = po.order_id\n",
            "JOIN pizzas p ON po.pizza_id = p.id\n",
            "GROUP BY day_of_week, p.size\n",
            "ORDER BY total_revenue DESC\u001b[0m\u001b[36;1m\u001b[1;3mError: (sqlite3.OperationalError) no such function: DAYNAME\n",
            "[SQL: SELECT \n",
            "    DAYNAME(o.order_date) AS day_of_week,\n",
            "    p.size,\n",
            "    SUM(o.price) AS total_revenue\n",
            "FROM orders o\n",
            "JOIN pizza_orders po ON o.id = po.order_id\n",
            "JOIN pizzas p ON po.pizza_id = p.id\n",
            "GROUP BY day_of_week, p.size\n",
            "ORDER BY total_revenue DESC]\n",
            "(Background on this error at: https://sqlalche.me/e/20/e3q8)\u001b[0m\u001b[32;1m\u001b[1;3mWe need to use a different function to get the day of the week from the date.\n",
            "Action: sql_db_query_checker\n",
            "Action Input: \n",
            "SELECT \n",
            "    strftime('%w', o.order_date) AS day_of_week,\n",
            "    p.size,\n",
            "    SUM(o.price) AS total_revenue\n",
            "FROM orders o\n",
            "JOIN pizza_orders po ON o.id = po.order_id\n",
            "JOIN pizzas p ON po.pizza_id = p.id\n",
            "GROUP BY day_of_week, p.size\n",
            "ORDER BY total_revenue DESC\u001b[0m\u001b[36;1m\u001b[1;3mSELECT \n",
            "    strftime('%w', o.order_date) AS day_of_week,\n",
            "    p.size,\n",
            "    SUM(o.price) AS total_revenue\n",
            "FROM orders o\n",
            "JOIN pizza_orders po ON o.id = po.order_id\n",
            "JOIN pizzas p ON po.pizza_id = p.id\n",
            "GROUP BY day_of_week, p.size\n",
            "ORDER BY total_revenue DESC;\u001b[0m\u001b[32;1m\u001b[1;3mWe now have the correct query to find the day of the week when the smallest size pizza generates the highest revenue.\n",
            "Action: sql_db_query\n",
            "Action Input: \n",
            "SELECT \n",
            "    strftime('%w', o.order_date) AS day_of_week,\n",
            "    p.size,\n",
            "    SUM(o.price) AS total_revenue\n",
            "FROM orders o\n",
            "JOIN pizza_orders po ON o.id = po.order_id\n",
            "JOIN pizzas p ON po.pizza_id = p.id\n",
            "WHERE p.size = 'S'\n",
            "GROUP BY day_of_week\n",
            "ORDER BY total_revenue DESC\u001b[0m\u001b[36;1m\u001b[1;3m[('1', 'S', 1843950), ('0', 'S', 1232000), ('2', 'S', 511100), ('6', 'S', 115625), ('3', 'S', 14900)]\u001b[0m\u001b[32;1m\u001b[1;3mThe day of the week when the smallest size pizza generates the highest revenue is Monday.\n",
            "Final Answer: Monday\u001b[0m\n",
            "\n",
            "\u001b[1m> Finished chain.\u001b[0m\n"
          ]
        }
      ],
      "source": [
        "# зададим еще один вопрос, требующий информацию из трех таблиц\n",
        "resp = agent_executor.invoke({\"input\": \"В какой день недели пиццы наименьшего размера приносят наибольшую выручку?\"})"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "1966e42b-d588-432d-b286-53de9fd9442b",
      "metadata": {
        "id": "1966e42b-d588-432d-b286-53de9fd9442b",
        "outputId": "d1e3ebc0-cf6b-4582-e846-a1346abd031b"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "\"[('1', 'S', 1843950), ('0', 'S', 1232000), ('2', 'S', 511100), ('6', 'S', 115625), ('3', 'S', 14900)]\""
            ]
          },
          "execution_count": 17,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "db.run('''\n",
        "SELECT\n",
        "    strftime('%w', o.order_date) AS day_of_week,\n",
        "    p.size,\n",
        "    SUM(o.price) AS total_revenue\n",
        "FROM orders o\n",
        "JOIN pizza_orders po ON o.id = po.order_id\n",
        "JOIN pizzas p ON po.pizza_id = p.id\n",
        "WHERE p.size = 'S'\n",
        "GROUP BY day_of_week\n",
        "ORDER BY total_revenue DESC\n",
        "''')"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4fd2fcc8-fd44-4ad2-ad9c-701834881170",
      "metadata": {
        "id": "4fd2fcc8-fd44-4ad2-ad9c-701834881170"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "Ответы совпадают, наибольшая выручка для маленьких пицц - в понедельник."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "35da7223-5c26-4ab3-8373-1b61906dbd56",
      "metadata": {
        "id": "35da7223-5c26-4ab3-8373-1b61906dbd56"
      },
      "source": [
        "### <center id=\"p7.1\"> 🫂 SQL агент + python агент 🐍 = super агент 😎 </center>"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bcfe4c1a-7e6b-43c6-b7aa-fe489c2166b5",
      "metadata": {
        "id": "bcfe4c1a-7e6b-43c6-b7aa-fe489c2166b5"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "Теперь попробуем заставить агентов решить следущую задачу - попросим сделать запрос к sql базе данных, и на основе полученных данных построить график.\n",
        "\n",
        "Для этого мы создадим двух агентов: `PythonAgent` и `SQLAgent`. И эти агенты будут инструментами для еще одного верхнеуровнего агента."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "726ae409-bfe6-4334-9ba1-fc68be74950c",
      "metadata": {
        "id": "726ae409-bfe6-4334-9ba1-fc68be74950c"
      },
      "outputs": [],
      "source": [
        "from langchain_experimental.agents.agent_toolkits.python.base import create_python_agent\n",
        "from langchain_experimental.tools.python.tool import PythonREPLTool"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "5051c4fd-d941-48c8-9964-6edc19ed03ef",
      "metadata": {
        "id": "5051c4fd-d941-48c8-9964-6edc19ed03ef"
      },
      "outputs": [],
      "source": [
        "# создаем пайтон агента\n",
        "agent_model = create_python_agent(\n",
        "    llm=llm,\n",
        "    tool=PythonREPLTool(),\n",
        "    verbose=True\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "dd6b99ad-083b-40b9-8609-213702d12771",
      "metadata": {
        "id": "dd6b99ad-083b-40b9-8609-213702d12771"
      },
      "outputs": [],
      "source": [
        "# создаем sql агента\n",
        "agent_executor_sql = create_sql_agent(llm, db=db, agent_type=\"openai-tools\", verbose=True)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "157d6280-ec89-4557-94d8-0f860ba81c9e",
      "metadata": {
        "id": "157d6280-ec89-4557-94d8-0f860ba81c9e"
      },
      "outputs": [],
      "source": [
        "from langchain.agents import Tool\n",
        "# оба агента станут инструментами для верхнеуровнего агента\n",
        "tools_mix=[\n",
        "        Tool(\n",
        "            name=\"PythonAgent\",\n",
        "            func=agent_model.run,\n",
        "            description=\"\"\"Useful to run python commands\"\"\",\n",
        "        ),\n",
        "        Tool(\n",
        "            name=\"SQLAgent\",\n",
        "            func=agent_executor_sql.run,\n",
        "            description=\"\"\"Useful to query sql tables\"\"\",\n",
        "        ),\n",
        "    ]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "33122762-3064-41a9-b963-a44ee3bb1dd3",
      "metadata": {
        "id": "33122762-3064-41a9-b963-a44ee3bb1dd3"
      },
      "outputs": [],
      "source": [
        "# верхнеуровневый агент\n",
        "prompt = hub.pull(\"hwchase17/openai-tools-agent\")\n",
        "agent_mix = create_openai_tools_agent(\n",
        "    tools=tools_mix,\n",
        "    llm=llm,\n",
        "    prompt=prompt,\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "ce833e1d-b9db-4cb4-b5f2-b39ab22aeae8",
      "metadata": {
        "id": "ce833e1d-b9db-4cb4-b5f2-b39ab22aeae8"
      },
      "outputs": [],
      "source": [
        "agent_executor = AgentExecutor(agent=agent_mix, tools=tools_mix, verbose=True,\n",
        "                              handle_parsing_errors=True)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "40c4b230-a68c-4130-a4fa-65bbc35177c0",
      "metadata": {
        "scrolled": true,
        "id": "40c4b230-a68c-4130-a4fa-65bbc35177c0",
        "outputId": "5d418806-4562-4c69-b33a-7d45ccd0f6c3"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "\n",
            "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
            "\u001b[32;1m\u001b[1;3m\n",
            "Invoking: `SQLAgent` with `SELECT DATE(created_at) as date, COUNT(*) as orders_count FROM orders GROUP BY DATE(created_at) LIMIT 10;`\n",
            "\n",
            "\n",
            "\u001b[0m\n",
            "\n",
            "\u001b[1m> Entering new SQL Agent Executor chain...\u001b[0m\n",
            "\u001b[32;1m\u001b[1;3m\n",
            "Invoking: `sql_db_list_tables` with `{'tool_input': ''}`\n",
            "\n",
            "\n",
            "\u001b[0m\u001b[38;5;200m\u001b[1;3mclients, orders, pizza_orders, pizzas\u001b[0m\u001b[32;1m\u001b[1;3m\n",
            "Invoking: `sql_db_schema` with `{'table_names': 'orders'}`\n",
            "\n",
            "\n",
            "\u001b[0m\u001b[33;1m\u001b[1;3m\n",
            "CREATE TABLE orders (\n",
            "\tid INTEGER NOT NULL, \n",
            "\torder_date DATE, \n",
            "\tclient_id INTEGER, \n",
            "\tquantity INTEGER, \n",
            "\tprice NUMERIC(10, 2), \n",
            "\tPRIMARY KEY (id), \n",
            "\tFOREIGN KEY(client_id) REFERENCES clients (id)\n",
            ")\n",
            "\n",
            "/*\n",
            "3 rows from orders table:\n",
            "id\torder_date\tclient_id\tquantity\tprice\n",
            "1\t2023-05-01\t1\t1\t750.00\n",
            "2\t2023-05-01\t2\t4\t12000.00\n",
            "3\t2023-05-01\t1\t2\t2750.00\n",
            "*/\u001b[0m\u001b[32;1m\u001b[1;3m\n",
            "Invoking: `sql_db_query` with `{'query': 'SELECT DATE(order_date) as date, COUNT(*) as orders_count FROM orders GROUP BY DATE(order_date) LIMIT 10;'}`\n",
            "\n",
            "\n",
            "\u001b[0m\u001b[36;1m\u001b[1;3m[('2023-05-01', 6), ('2023-05-02', 4), ('2023-05-03', 1), ('2023-05-06', 3), ('2023-05-07', 6), ('2023-05-08', 6), ('2023-05-09', 5), ('2023-05-10', 2), ('2023-05-13', 3), ('2023-05-14', 6)]\u001b[0m\u001b[32;1m\u001b[1;3mThe query returned the number of orders placed on each date:\n",
            "\n",
            "- On 2023-05-01, there were 6 orders.\n",
            "- On 2023-05-02, there were 4 orders.\n",
            "- On 2023-05-03, there was 1 order.\n",
            "- On 2023-05-06, there were 3 orders.\n",
            "- On 2023-05-07, there were 6 orders.\n",
            "- On 2023-05-08, there were 6 orders.\n",
            "- On 2023-05-09, there were 5 orders.\n",
            "- On 2023-05-10, there were 2 orders.\n",
            "- On 2023-05-13, there were 3 orders.\n",
            "- On 2023-05-14, there were 6 orders.\u001b[0m\n",
            "\n",
            "\u001b[1m> Finished chain.\u001b[0m\n",
            "\u001b[33;1m\u001b[1;3mThe query returned the number of orders placed on each date:\n",
            "\n",
            "- On 2023-05-01, there were 6 orders.\n",
            "- On 2023-05-02, there were 4 orders.\n",
            "- On 2023-05-03, there was 1 order.\n",
            "- On 2023-05-06, there were 3 orders.\n",
            "- On 2023-05-07, there were 6 orders.\n",
            "- On 2023-05-08, there were 6 orders.\n",
            "- On 2023-05-09, there were 5 orders.\n",
            "- On 2023-05-10, there were 2 orders.\n",
            "- On 2023-05-13, there were 3 orders.\n",
            "- On 2023-05-14, there were 6 orders.\u001b[0m\u001b[32;1m\u001b[1;3m\n",
            "Invoking: `PythonAgent` with `import matplotlib.pyplot as plt\n",
            "\n",
            "# Data for the first 10 days\n",
            "dates = ['2023-05-01', '2023-05-02', '2023-05-03', '2023-05-06', '2023-05-07', '2023-05-08', '2023-05-09', '2023-05-10', '2023-05-13', '2023-05-14']\n",
            "orders_count = [6, 4, 1, 3, 6, 6, 5, 2, 3, 6]\n",
            "\n",
            "plt.figure(figsize=(10, 6))\n",
            "plt.bar(dates, orders_count, color='skyblue')\n",
            "plt.xlabel('Date')\n",
            "plt.ylabel('Number of Orders')\n",
            "plt.title('Number of Orders for the First 10 Days')\n",
            "plt.xticks(rotation=45)\n",
            "plt.tight_layout()\n",
            "plt.show()`\n",
            "\n",
            "\n",
            "\u001b[0m\n",
            "\n",
            "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
            "\u001b[32;1m\u001b[1;3mWe need to visualize the data using a bar chart with matplotlib.\n",
            "Action: Python_REPL\n",
            "Action Input:\n",
            "```python\n",
            "import matplotlib.pyplot as plt\n",
            "\n",
            "# Data for the first 10 days\n",
            "dates = ['2023-05-01', '2023-05-02', '2023-05-03', '2023-05-06', '2023-05-07', '2023-05-08', '2023-05-09', '2023-05-10', '2023-05-13', '2023-05-14']\n",
            "orders_count = [6, 4, 1, 3, 6, 6, 5, 2, 3, 6]\n",
            "\n",
            "plt.figure(figsize=(10, 6))\n",
            "plt.bar(dates, orders_count, color='skyblue')\n",
            "plt.xlabel('Date')\n",
            "plt.ylabel('Number of Orders')\n",
            "plt.title('Number of Orders for the First 10 Days')\n",
            "plt.xticks(rotation=45)\n",
            "plt.tight_layout()\n",
            "plt.show()\n",
            "```\u001b[0m"
          ]
        },
        {
          "data": {
            "image/png": "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lixRs2bNVKNGDb300ksKCAjQlClT9MADD+jnn39W7dq1ve6zTZs2KlOmjEaNGnXN86ePHDmiunXr6vz58+rdu7fy5MmjadOmqUWLFpo1a5b+8Y9/6L777tNnn32mJ554Qo0bN1anTp2umce3334rSVddLygoSB06dFBkZKR++eUXNWrUyHPblClTdPHiRT311FMKDg5W7ty5FR0drQcffFD58uXTiBEjlJCQoJdeekkFChRIdt8jR47UsGHD1LZtW3Xv3l3Hjh3T22+/rfvuu08bNmzw2lN/4sQJNWvWTO3atVPHjh1VoEABHT161PNYgwYNUs6cObVv3z7Nnj37ms/5s88+04cffqjVq1fro48+kiTVrVtX0uU9ptOmTVPr1q313HPP6ddff9Xo0aM9RwJcafv27Wrfvr3+9a9/qUePHipbtmyKjzd06FCdOXNGf/75p958801JUrZs2bzWGTNmjAICAvT888/rzJkzGjdunB5//HH9+uuvnnV83a5Scv78eR0/ftxrWY4cOa5Zdnv27Kl8+fJp+PDhio2NlSQ9/fTTmjVrlp599llVqFBBJ06c0PLly7Vt2zZVr149Vc/5Rm3YsEGhoaEqX7681/Kk579hw4brvnh1LU888YQ+/PBDLVq0SI0bN5YkffXVVzp//ryeeeYZ5cmTR6tXr9bbb7+tP//8U1999ZWky3vOe/XqpenTp6tatWpe9zl9+nQ1aNBARYoU0aVLl9SkSRPFxcXp3//+twoWLKiDBw9q/vz5On36tHLkyHHDswOAXzAAAMdMmTLFSDJr1qwxu3fvNkFBQaZ3796e2+vXr28qVqzo+Xzv3r1GkpkyZUqy+5JkXnrpJc/nL730kpFknnrqKc+yhIQEEx4eblwulxkzZoxn+alTp0yWLFlM586dPcuWLl1qJJkiRYqYmJgYz/L//Oc/RpJ56623jDHGuN1uU6ZMGdOkSRPjdrs9650/f96ULFnSNG7cONlM7du3T1U+ffv2NZLMzz//7Fl29uxZU7JkSVOiRAmTmJjo9fx79ep13fts1aqVkWROnTp11XVmz55tJJmJEycaY/6Xe1hYmDl69Giy+wsJCTF//PGHZ9nWrVtNYGCgufK/2X379pnAwEAzcuRIr6+Pjo42QUFBXsvr169vJJn333/fa905c+Z4thdfde7c2YSGhnot27hxo5Fkunfv7rX8+eefN5LMkiVLPMuKFy9uJJmFCxem6vEefvhhU7x48WTLk7ar8uXLm7i4OM/yt956y0gy0dHRxhjftquUJH3PUvpYunSp5zlduc0n/Tzee++9JiEhwev+cuTIcd3t62rPOTWu9bUPP/ywueOOO5Itj42NNZLMoEGDrnnfST93x44dS/H2U6dOGUnmH//4h2fZ+fPnk603evRo43K5vLb19u3bm8KFC3v9LK5fv97r99SGDRuMJPPVV19dc04AyKg4vBwA0ok77rjDs8fp8OHDaXa/3bt39/w7MDBQNWvWlDFG3bp18yzPmTOnypYtqz179iT7+k6dOil79uyez1u3bq1ChQrpv//9ryRp48aN2rlzpzp06KATJ07o+PHjOn78uGJjY9WwYUP99NNPcrvdXvf59NNPp2r2//73v6pdu7bXXrxs2bLpqaee0r59+zyHd/vi7NmzkuT1nP4u6baYmBiv5Y8++qjnIlzS5UPvo6Ki1KpVKxUrVsyzvHz58mrSpInX186ePVtut1tt27b1ZHT8+HEVLFhQZcqU0dKlS73WDw4OVteuXb2WJe0Jnz9/vuLj41P5jK8u6XvYv39/r+XPPfecJHkdxi9dPiLj78/rRnXt2tXr/PJ69epJkmcbvJHtKiVPPfWUvv/+e6+PiIiIa35Njx49kp0jnTNnTv366686dOiQr0/1pl24cCHF889DQkI8t9+MpD3yST8bkrzO1Y+NjdXx48dVt25dGWO8Difv1KmTDh065LX9Tp8+XVmyZNGjjz4qSZ492VFRUTp//vxNzQoA/ojSDQDpyIsvvqiEhITrntvtiyvLoHT5D+CQkBCvCyolLT916lSyry9TpozX5y6XS6VLl9a+ffskXT5XU7p8cbJ8+fJ5fXz00UeKi4tLdt5myZIlUzX7H3/8keIhzEmH2f7xxx+pup8rJRXqKwvG312tmP997mPHjunChQvJMpKUbO6dO3fKGKMyZcoky2nbtm06evSo1/pFihRJdtGz+vXr69FHH1VkZKTy5s2rli1b3tRbPv3xxx8KCAhQ6dKlvZYXLFhQOXPmTJZvar9vqfH37TJXrlyS5NkGb2S7SkmZMmXUqFEjr4+kx7qalJ7nuHHjtHnzZhUtWlS1a9fWiBEjUnyRyoYsWbKk+D2+ePGi5/abce7cOUne2/v+/fvVpUsX5c6dW9myZVO+fPlUv359SfLKvXHjxipUqJCmT58u6fK5+jNnzlTLli0991eyZEn1799fH330kfLmzasmTZpo0qRJnM8N4LbBOd0AkI7ccccd6tixoz788EMNGjQo2e1Xu0BYYmLiVe8zpasaX+1Kx+YG3p86aW/ja6+9pqpVq6a4zt/PbXXyitfly5fX3LlztWnTpmTnryfZtGmTJKlChQpey29mbrfbLZfLpe+++y7F/FOTkcvl0qxZs7Rq1Sp9++23nrd8Gj9+vFatWnXD5xBf68Jz15vpRl1vG7yR7SqtpPQ827Ztq3r16mnOnDlatGiRXnvtNY0dO1azZ89Ws2bNrMyRpFChQlq6dKmMMV7fq6QjYm72GhBJV01PevElMTFRjRs31smTJzVw4ECVK1dOoaGhOnjwoLp06eJ1hEFgYKA6dOigyZMn691339Uvv/yiQ4cOJbti/Pjx49WlSxfNmzdPixYtUu/evTV69GitWrXKc7E6AMioKN0AkM68+OKL+vzzzzV27NhktyXtoTt9+rTX8hvZ45taSXsckxhjtGvXLlWpUkWSVKpUKUlSWFiY10XH0kLx4sW1ffv2ZMt///13z+2+at68uUaPHq1PP/00xdKdmJioGTNmKFeuXLrnnnuueV/58uVTlixZkmUkKdncpUqVkjFGJUuW1J133unz3Fe6++67dffdd2vkyJGaMWOGHn/8cX3xxRdepxKkRvHixeV2u7Vz506vi3QdOXJEp0+fvqF8k6S2yF+Nze3qRhUqVEg9e/ZUz549dfToUVWvXl0jR470lO6bfc5XU7VqVX300Ufatm2b1wtBSRedu9qLEqn12WefSZLn1IHo6Gjt2LFD06ZN87rg4JVXN79Sp06dNH78eH377bf67rvvlC9fvhRPQ6hcubIqV66sF198UStWrNA999yj999/X6+++upNzQ8A6R2HlwNAOlOqVCl17NhRH3zwgf766y+v28LCwpQ3b1799NNPXsvfffdda/N8+umnXodiz5o1S4cPH/YUjRo1aqhUqVJ6/fXXPYepXunYsWM3/NgPPfSQVq9e7fWWSLGxsfrwww9VokSJZHuiU6Nu3bpq1KiRpkyZovnz5ye7fejQodqxY4cGDBhw3T27gYGBatKkiebOnav9+/d7lm/btk1RUVFe6/7zn/9UYGCgIiMjkx1RYIzRiRMnrjv7qVOnkn1tUuG6kUPMH3roIUnShAkTvJa/8cYbkqSHH37Y5/tMEhoaelOHD9vcrnyVmJiY7Lnkz59fhQsX9sr9Zp/z1bRs2VKZMmXy+jk3xuj9999XkSJFPFeivxEzZszQRx99pDp16qhhw4aS/ncUwpXbmjFGb731Vor3UaVKFVWpUkUfffSRvv76a7Vr105BQf/brxMTE6OEhASvr6lcubICAgJu+NQIAPAn7OkGgHRo6NCh+uyzz7R9+3ZVrFjR67bu3btrzJgx6t69u2rWrKmffvpJO3bssDZL7ty5de+996pr1646cuSIJkyYoNKlS6tHjx6SpICAAH300Udq1qyZKlasqK5du6pIkSI6ePCgli5dqrCwMM/bdPlq0KBBmjlzppo1a6bevXsrd+7cmjZtmvbu3auvv/5aAQE39trxp59+qoYNG6ply5bq0KGD6tWrp7i4OM2ePVvLli3TY489phdeeCFV9xUZGamFCxeqXr166tmzpxISEvT222+rYsWKnsPUpcsvprz66qsaPHiw9u3bp1atWil79uzau3ev5syZo6eeekrPP//8NR9r2rRpevfdd/WPf/xDpUqV0tmzZzV58mSFhYV5CrQvIiIi1LlzZ3344Yc6ffq06tevr9WrV2vatGlq1aqV7r//fp/vM0mNGjX05Zdfqn///qpVq5ayZcumRx55JNVfb3O78tXZs2cVHh6u1q1bKyIiQtmyZdMPP/ygNWvWeL2/va/PedOmTfrmm28kSbt27dKZM2c8e30jIiI8XxseHq6+ffvqtddeU3x8vGrVqqW5c+fq559/1vTp0696qP7fzZo1S9myZdOlS5d08OBBRUVF6ZdfflFERITnbcAkqVy5cipVqpSef/55HTx4UGFhYfr6669TvOZDkk6dOnm2378fWr5kyRI9++yzatOmje68804lJCTos88+U2BgoOdiawCQoTlxyXQAwGVXvmXY33Xu3NlI8nrLMGMuv5VPt27dTI4cOUz27NlN27ZtzdGjR6/6lmF/f5uglN46ypjkb0+W9NZOM2fONIMHDzb58+c3WbJkMQ8//LDXWwYl2bBhg/nnP/9p8uTJY4KDg03x4sVN27ZtzeLFi68707Xs3r3btG7d2uTMmdOEhISY2rVrm/nz5ydbT6l8y7AkZ8+eNSNGjDAVK1Y0WbJkMdmzZzf33HOPmTp1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ly/nytBXca1crzx01Bjj+Y902LBhGjdunFauXKmsWbM6NXq6d71tNjg4WMWKFZMkz4sbu3fvVrVq1RQUxK/Hq0kp1xEjRqhixYqaM2eOHnjgAX355ZeqXLmyOnfuLElasWKFChcuzPZ6DdfbXgMCAuR2u5UjRw716NFDUVFROnDggIoWLer06One37OtVq2aXn75ZVWoUEGFChXyvGh88uRJz1X3Fy1apHz58ilv3rwOT59+pZRrZGSkmjRporVr1+rMmTNKTExU1apVJV1+n964uDiVKVNGkrgmwTXs3LlTH3/8sbp16+bJKzExUeXKldP8+fPVoEEDGWPUs2dPNWrUSNLld5MJDw/nhaJrSCnXvytQoICk/72g+dtvv6lChQqU7uu4WrZJO+aMMbr77ruVKVMmLViwQFWrVlWJEiU0depUlSpVysHJbxHH9rHDrxw4cMAULFjQ9OvXz6xatcq89dZbpkmTJiZnzpxm5cqVxpj/HQYdHx9vjDHm6aefNnny5DFbtmxxbO70LjW5Xrp0yRhjzLx588y9995rhg8fboKDg826deucHD3d82WbNebydjt06FBToEABs3XrVqfGTveulmuOHDnMTz/9ZIwxZuPGjaZmzZqmbNmypkyZMubhhx82OXPmNBs2bHB2+HTM1+11zZo1xuVymS+//NKpkf3GtbbZpGz37Nlj8ubNax555BHz73//2zz99NMmV65cyQ7hxf9cK9eff/452fonTpwwQ4YMMQUKFDC7d+92YGL/sWvXLpM/f34TFhZm+vbta3bt2mWMuXw4dNLfWBs3bjTVq1c31atXN5UqVTItWrQwYWFhbLPXcLVcryY2NtYMGTLE5MuXz2zevPkWTemfrpVt0mH8X3zxhalTp47n9NMkV572l5FRupEqixcvNjVq1DAnTpzwLNu1a5dp3769yZo1q1m/fr0x5n/nFo4fP964XC7PcqQstbkaY8w333xjXC6XyZs3b7JfWEgutdm63W7z5Zdfmnbt2pkiRYqwzV7HtXLNkiWLWb16tTHGmP3795sFCxaYF154wbzzzjtm+/btTo3sF1K7vSYkJHj+gOnfvz8vEKXC9bbZNWvWGGOMWbdunWnatKmpX7++ad++PX9kX8f1cr3y74Lo6GjzwgsvmPz58/M79jrOnTtnOnToYNq3b28iIyNNtWrVzLPPPpti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AIAM6oEHHlBERIRmz54tSQoICNDEiRO1ZcsWTZs2TUuWLNGAAQMkSXXr1tWECRMUFhamw4cP6/Dhw3r++eclSc8++6xWrlypL774Qps2bVKbNm3UtGlT7dy507HnBgCAv3AZY4zTQwAAgBvXpUsXnT59WnPnzk12W7t27bRp0yZt3bo12W2zZs3S008/rePHj0u6fE533759dfr0ac86+/fv1x133KH9+/ercOHCnuWNGjVS7dq1NWrUqDR/PgAAZCRBTg8AAADsMcbI5XJJkn744QeNHj1av//+u2JiYpSQkKCLFy/q/Pnzypo1a4pfHx0drcTERN15551ey+Pi4pQnTx7r8wMA4O8o3QAAZGDbtm1TyZIltW/fPjVv3lzPPPOMRo4cqdy5c2v58uXq1q2bLl26dNXSfe7cOQUGBmrdunUKDAz0ui1btmy34ikAAODXKN0AAGRQS5YsUXR0tPr166d169bJ7XZr/PjxCgi4fEmX//znP17rZ86cWYmJiV7LqlWrpsTERB09elT16tW7ZbMDAJBRULoBAMgA4uLi9NdffykxMVFHjhzRwoULNXr0aDVv3lydOnXS5s2bFR8fr7fffluPPPKIfvnlF73//vte91GiRAmdO3dOixcvVkREhLJmzao777xTjz/+uDp16qTx48erWrVqOnbsmBYvXqwqVaro4YcfdugZAwDgH7h6OQAAGcDChQtVqFAhlShRQk2bNtXSpUs1ceJEzZs3T4GBgYqIiNAbb7yhsWPHqlKlSpo+fbpGjx7tdR9169bV008/rccee0z58uXTuHHjJElTpkxRp06d9Nxzz6ls2bJq1aqV1qxZo2LFijnxVAEA8CtcvRwAAAAAAEvY0w0AAAAAgCWUbgAAAAAALKF0AwAAAABgCaUbAAAAAABLKN0AAAAAAFhC6QYAAAAAwBJKNwAAAAAAllC6AQAAAACwhNINAAAAAIAllG4AAAAAACyhdAMAAAAAYAmlGwAAAAAAS/4Puvd/19bvlDwAAAAASUVORK5CYII=",
            "text/plain": [
              "<Figure size 1000x600 with 1 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "Observation: \u001b[36;1m\u001b[1;3m\u001b[0m\n",
            "Thought:\u001b[32;1m\u001b[1;3mThe bar chart has been displayed with the data for the first 10 days.\n",
            "Final Answer: The bar chart has been displayed successfully.\u001b[0m\n",
            "\n",
            "\u001b[1m> Finished chain.\u001b[0m\n",
            "\u001b[36;1m\u001b[1;3mThe bar chart has been displayed successfully.\u001b[0m\u001b[32;1m\u001b[1;3mГрафик с количеством заказов за первые 10 дней был успешно построен.\u001b[0m\n",
            "\n",
            "\u001b[1m> Finished chain.\u001b[0m\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "{'input': 'Возьми из таблицы orders данные о количестве заказов в день. И построй график для ппервых 10 дней.',\n",
              " 'output': 'График с количеством заказов за первые 10 дней был успешно построен.'}"
            ]
          },
          "execution_count": 85,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "agent_executor.invoke({'input':'Возьми из таблицы orders данные о количестве заказов в день. И построй график для первых 10 дней.'})"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "fc6a61bc-a135-4020-a98c-e6ca31dab7ec",
      "metadata": {
        "id": "fc6a61bc-a135-4020-a98c-e6ca31dab7ec"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "🤩 Видим, что sql агент сделал правильный запрос к базе данных, а python агент построил по этим данным график."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "39a7dc5f-22d8-4c5b-88a5-e331689eed9b",
      "metadata": {
        "id": "39a7dc5f-22d8-4c5b-88a5-e331689eed9b"
      },
      "source": [
        "# <center id=\"p8\"> 📦 Задеплой Агента с помощью `LangServe`. 🚀</center>\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ce97dd89-22cb-43a4-9203-d655efac421d",
      "metadata": {
        "id": "ce97dd89-22cb-43a4-9203-d655efac421d"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "\n",
        "Представим, что у нас есть настроеный работающий агент, и мы хотим, чтобы другие люди тоже могли им воспользоваться.\n",
        "Самое время задеплоить его на сервере и дать ощутить пользу от него другим людям.\n",
        "\n",
        "Фрэймворк `LangServe` позволяет деплоить Langchain-цепи, агентов и любые `runnables` (протокол кастомных цепей), как `REST API`-сервисы и даже делает для них простую веб-обертку (`playground`).\n",
        "\n",
        "`LangServe` интегрирован с `FastAPI` ([документация](https://fastapi.tiangolo.com/)) и для валидации данных использует `pydantiс` ([документация](https://docs.pydantic.dev/latest/))."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "16390df2-69e7-4d93-a369-e37e5d5acd10",
      "metadata": {
        "id": "16390df2-69e7-4d93-a369-e37e5d5acd10"
      },
      "source": [
        "<div class=\"alert alert-info\">\n",
        "    \n",
        "В качестве сервера будем использовать код из ячейки ниже.\n",
        "Чтобы запустить сервер локально нужно сохранить код ячейки в питоновский файл `.py` и вызвать через командную строку командой `python файл.py`.\n",
        "\n",
        "Сервер будет слушать запросы по адресу `\"0.0.0.0:8501\"` (в нашем случае).\n",
        "У нас будет 2 эндпоинта `/joke` и `/rag_agent`.\n",
        "* По первому у нас будет доступ к цепочке, которая будет выдавать шутки на определенную тему (тему мы подадим post-запросом с клиента).\n",
        "* По второму - знакомый нам RAG-agent для ответов на вопросы о пиццерии."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "c89877d3-0f39-4e80-9449-4ebdbc024521",
      "metadata": {
        "id": "c89877d3-0f39-4e80-9449-4ebdbc024521",
        "outputId": "5e9408af-bbcc-4747-8cd1-f362fa92ed7e"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Overwriting server.py\n"
          ]
        }
      ],
      "source": [
        "%%writefile server.py\n",
        "#!/usr/bin/env python\n",
        "\n",
        "from fastapi import FastAPI\n",
        "from utils import ChatOpenAI, OpenAIEmbeddings\n",
        "from typing import Any\n",
        "\n",
        "from langserve import add_routes\n",
        "from langchain.pydantic_v1 import BaseModel\n",
        "from langchain.document_loaders import WebBaseLoader\n",
        "from langchain import hub\n",
        "from langchain.prompts import ChatPromptTemplate\n",
        "from langchain_community.vectorstores import FAISS\n",
        "from langchain_text_splitters import CharacterTextSplitter\n",
        "from langchain.tools.retriever import create_retriever_tool\n",
        "from langchain.agents import AgentExecutor, create_openai_tools_agent\n",
        "\n",
        "class Input(BaseModel):\n",
        "    input: str\n",
        "\n",
        "\n",
        "class Output(BaseModel):\n",
        "    output: Any\n",
        "\n",
        "def cut_output(output):\n",
        "    return output['output']\n",
        "\n",
        "\n",
        "loader = WebBaseLoader(\"https://allopizza.su/spb/kupchino/about\")\n",
        "data = loader.load()\n",
        "\n",
        "course_api_key = ''# ключ курса (если используем ключи из курса)\n",
        "llm = ChatOpenAI(temperature=0.0, course_api_key=course_api_key)\n",
        "\n",
        "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)\n",
        "texts = text_splitter.split_documents(data)\n",
        "embeddings = OpenAIEmbeddings(course_api_key=course_api_key)\n",
        "db_embed = FAISS.from_documents(texts, embeddings)\n",
        "retriever = db_embed.as_retriever()\n",
        "\n",
        "tool = create_retriever_tool(\n",
        "    retriever, # наш ретривер\n",
        "    \"search_web\", # имя инструмента\n",
        "    \"Searches and returns data from page\", # описание инструмента подается в ЛЛМ\n",
        ")\n",
        "\n",
        "prompt = hub.pull(\"hwchase17/openai-tools-agent\")\n",
        "\n",
        "agent = create_openai_tools_agent(llm, [tool], prompt)\n",
        "agent_executor = AgentExecutor(agent=agent, tools=[tool])\n",
        "\n",
        "app = FastAPI(\n",
        "    title=\"LangChain Server\",\n",
        "    version=\"1.0\",\n",
        "    description=\"A simple api server using Langchain's Runnable interfaces\",\n",
        ")\n",
        "\n",
        "add_routes(\n",
        "    app,\n",
        "    agent_executor.with_types(input_type=Input, output_type=Output) | cut_output,\n",
        "    path=\"/rag_agent\",  # эндпоинт для rag агента\n",
        ")\n",
        "\n",
        "prompt2 = ChatPromptTemplate.from_template(\"tell me a joke about {topic}\")\n",
        "add_routes(\n",
        "    app,\n",
        "    prompt2 | llm,\n",
        "    path=\"/joke\",  # эндпоинт для цепочки\n",
        ")\n",
        "\n",
        "\n",
        "\n",
        "if __name__ == \"__main__\":\n",
        "    import uvicorn\n",
        "\n",
        "    uvicorn.run(app, host=\"0.0.0.0\", port=8501)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "10fbe4e4-dad0-49a5-8393-774b8fdbbfff",
      "metadata": {
        "id": "10fbe4e4-dad0-49a5-8393-774b8fdbbfff"
      },
      "source": [
        "<div class=\"alert alert-success\">\n",
        "    \n",
        "В ячейках ниже - код клиента.\n",
        "Мы посылаем POST-запрос на нужный адрес.  "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "4ef39020-617c-4638-a149-8403a482d110",
      "metadata": {
        "id": "4ef39020-617c-4638-a149-8403a482d110",
        "outputId": "2939f84f-97c5-4f12-a03b-7d152be27488"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "{'output': {'content': 'Why did the robot go on a diet? Because it had a byte problem!',\n",
              "  'additional_kwargs': {},\n",
              "  'response_metadata': {'token_usage': {'completion_tokens': 16,\n",
              "    'prompt_tokens': 13,\n",
              "    'total_tokens': 29},\n",
              "   'model_name': 'gpt-3.5-turbo',\n",
              "   'system_fingerprint': 'fp_3b956da36b',\n",
              "   'finish_reason': 'stop',\n",
              "   'logprobs': None},\n",
              "  'type': 'ai',\n",
              "  'name': None,\n",
              "  'id': 'run-e2c75e33-432f-4d3f-820b-9c8f38a900b1-0',\n",
              "  'example': False},\n",
              " 'metadata': {'run_id': '5d41242a-19da-4e84-a336-b1b49a36dcdc',\n",
              "  'feedback_tokens': []}}"
            ]
          },
          "execution_count": 28,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Проверяем \"шуточную\" цепочку\n",
        "import requests\n",
        "\n",
        "response = requests.post(\n",
        "    \"http://0.0.0.0:8501/joke/invoke\",\n",
        "    json={'input': {'topic': 'robot'}}\n",
        ")\n",
        "response.json()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "a4fe4dfc-3802-495d-8aef-802865fe659b",
      "metadata": {
        "id": "a4fe4dfc-3802-495d-8aef-802865fe659b",
        "outputId": "cc8d9214-5467-45e0-ff94-b37b9fdaf22e"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "{'output': {'output': 'Автопарк компании состоит из более чем 200 легковых автомобилей.'},\n",
              " 'metadata': {'run_id': '79d2a9fb-d8e0-4af6-b006-6953635fd31f',\n",
              "  'feedback_tokens': []}}"
            ]
          },
          "execution_count": 29,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Проверяем RAG-агента\n",
        "response = requests.post(\n",
        "    \"http://0.0.0.0:8501/rag_agent/invoke\",\n",
        "    json={'input': {'input':'Из скольки авто состоит автопарк компании?'}}\n",
        ")\n",
        "response.json()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2e68e2e3-1a79-42f6-8fc3-4d6369656669",
      "metadata": {
        "id": "2e68e2e3-1a79-42f6-8fc3-4d6369656669"
      },
      "source": [
        "# <center id=\"p9\"> 🥁 Выводы и заключения ✅</center>\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a4067ae7-d9ba-4167-8e4b-fdaaf92516b8",
      "metadata": {
        "id": "a4067ae7-d9ba-4167-8e4b-fdaaf92516b8"
      },
      "source": [
        "<div class=\"alert alert-info\">\n",
        "\n",
        "* В этом уроке разобрались в основных типах агентов в langchain:\n",
        "  -  `ReAct` (`Reasoning` and `Acting`) - агенты по умолчанию\n",
        "  -  `Structured chat` - `milti-input tools`\n",
        "  -  `Self Ask with search` - `SelfAsk` с инструментами поиска\n",
        "  -  `OpenAI-tools` - для моделей от OpenAI\n",
        "* Узнали, что инструменты можно выдавать агентам не по одному, а целыми ящиками (`toolkits`).\n",
        "* Научились подключать агентам память, базу знаний (`RAG`), базу данных `SQL`.\n",
        "* Cобрали супер-агента из нескольких и даже задеплоили всё это добро на сервер средствами `LangServe`.\n",
        "* И в очередной раз убедились, что в основе всех методов лежат грамотно составленные промпты!"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "cv",
      "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.10.12"
    },
    "colab": {
      "provenance": []
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}