Создание умного помощника по составлению маршрута путешествия с помощью LangChain, Google Maps API и Gradio (Часть 1)

Составление умного путеводителя с помощью LangChain, Google Maps API и Gradio (Часть 1)

Узнайте, как создать приложение, которое может вдохновить вашу следующую поездку на автомобиле

Эта статья является частью 1 из трехчастной серии, в которой мы создаем приложение для предложения путевых маршрутов с помощью OpenAI и Google API и отображаем его в простом интерфейсе, созданном с помощью gradio. В этой части мы начинаем с обсуждения инженерии запросов для этого проекта. Просто хотите посмотреть код? Найдите его здесь.

1. Мотивация

С момента запуска ChatGPT в конце 2022 года произошел взрыв интереса к большим языковым моделям (LLM) и их применению в потребительских продуктах, таких как чат-боты и поисковые системы. Меньше чем через год у нас есть доступ к множеству доступных в открытом доступе LLM-моделей из таких хабов моделей, как Hugging Face, сервисов для размещения моделей, таких как Lamini и платных API, таких как OpenAI и PaLM. Увлекательно и немного ошеломляет, как быстро развивается эта область, с новыми инструментами и парадигмами разработки, которые, кажется, возникают каждые несколько недель.

Здесь мы выберем всего лишь небольшую часть этого множества инструментов для создания полезного приложения, которое могло бы помочь нам в планировании путешествий. При планировании отпуска часто хочется получить рекомендации от тех, кто уже побывал там, и еще лучше видеть эти рекомендации на карте. В отсутствие такого совета, иногда я просто просматриваю Google Maps в области, которую хочу посетить, и выбираю несколько интересных мест. Может быть, этот процесс веселый, но он неэффективен и, вероятно, что-то упускает. Не было бы замечательно иметь инструмент, который мог бы давать вам множество рекомендаций при небольшом количестве предпочтений на высоком уровне?

Вот что именно мы будем пытаться создать: систему, которая может предлагать путевые маршруты с учетом некоторых предпочтений на высоком уровне, например “У меня есть 3 дня для изучения Сан-Франциско и я люблю художественные музеи”. Что касается запросов в поиске Google, изначальные функции и ChatGPT уже могут создавать творческие результаты для подобных запросов, но мы хотим пойти дальше и предоставить фактический маршрут с указанием времени путешествия и хорошей картой, чтобы помочь пользователю разобраться.

Вот что мы создадим: система для генерации рекомендаций по путешествию с простой картой, показывающей маршрут и пункты назначения, предоставленные LLM

Цель состоит не только в знакомстве с инструментами, необходимыми для создания системы подобной этой, а скорее в понимании их использования, но в процессе мы также узнаем немного о разработке запросов, оркестрации LLM с использованием LangChain, использовании API Google Maps для получения направлений и отображении результатов с помощью leafmap и gradio. Удивительно, насколько быстро эти инструменты позволяют создать ПОС для таких систем, но, как всегда, настоящие вызовы заключаются в оценке и управлении исключительными ситуациями. Инструмент, который мы создадим, далек от идеальности, и если кто-то заинтересован в помощи мне в его дальнейшей разработке, это было бы фантастически.

2. Стратегия запросов

Для этого проекта будут использоваться API OpenAI и Google PaLM. Вы можете получить ключи API, создав аккаунты здесь и здесь. На момент написания данной статьи, API Google имеет ограниченную общедоступность и есть список ожидания, однако доступ к нему можно получить всего за несколько дней.

Использование dotenv – это простой способ избежать необходимости копировать и вставлять ключи API в вашей среде разработки. После создания файла .env с следующими строками

OPENAI_API_KEY = {ваш ключ OpenAI}GOOGLE_PALM_API_KEY = {ваш ключ Google Palm API}

Мы можем использовать эту функцию для загрузки переменных, готовых для использования LangChain, например:

from dotenv import load_dotenv
from pathlib import Path

def load_secets():
    load_dotenv()
    env_path = Path(".") / ".env"
    load_dotenv(dotenv_path=env_path)
    open_ai_key = os.getenv("OPENAI_API_KEY")
    google_palm_key = os.getenv("GOOGLE_PALM_API_KEY")
    return {
        "OPENAI_API_KEY": open_ai_key,
        "GOOGLE_PALM_API_KEY": google_palm_key,
    }

Теперь, как мы должны разработать запросы для услуги туристического агента? Пользователь может ввести любой текст, поэтому мы сначала хотим определить, является ли его запрос допустимым. Мы определенно хотим обнаруживать любые запросы, содержащие вредоносный контент, такие как запросы на составление маршрутов с злонамеренными намерениями.

Мы также хотим отфильтровать вопросы, не связанные с путешествиями – несомненно, LLM может предоставить ответ на такие вопросы, но они выходят за рамки этого проекта. Наконец, мы также хотим определять запросы, которые неразумны, например “Я хочу полететь на Луну” или “Я хочу сделать трехдневную автомобильную поездку из Нью-Йорка в Токио”. Получив неразумный запрос, было бы замечательно, если бы модель могла объяснить, почему он неразумный, и предложить изменение, которое поможет.

Однажды проверив запрос, мы можем переходить к предоставлению предлагаемого маршрута, который в идеале должен содержать конкретные адреса путевых точек, чтобы их можно было отправить в API-сервис карт или направлений, такой как Google Maps.

Маршрут должен быть удобочитаемым для человека, с достаточными деталями, чтобы пользователь мог использовать его как самостоятельное предложение. Большие LLM, активированные инструкциями, такие как ChatGPT, кажутся отлично подходящими для предоставления таких ответов, но нам нужно убедиться, что адреса путевых точек извлекаются единообразным образом.

Итак, здесь есть три отдельных этапа:

  1. Проверить запрос
  2. Сформировать маршрут
  3. Извлечь путевые точки в формате, понятном для API Google Maps

Возможно, мы можем разработать запрос, который может выполнять все три этапа за один вызов, но для упрощения процесса отладки мы разделим их на три вызова LLM, по одному для каждой части.

К счастью, класс PydanticOutputParser в LangChain действительно может помочь здесь, предоставляя набор готовых запросов, которые побуждают LLM форматировать свои ответы в соответствии с определенной схемой вывода.

3. Запрос на валидацию

Давайте взглянем на запрос на валидацию, который мы можем оформить в виде шаблонного класса, чтобы сделать его более удобным для содержания и внесения изменений в разные версии.

from langchain.prompts.chat import (
    ChatPromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
)
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

class Validation(BaseModel):
    plan_is_valid: str = Field(
        description="This field is 'yes' if the plan is feasible, 'no' otherwise"
    )
    updated_request: str = Field(description="Your update to the plan")

class ValidationTemplate(object):
    def __init__(self):
        self.system_template = """      
You are a travel agent who helps users make exciting travel plans.      
The user's request will be denoted by four hashtags. Determine if the user's      
request is reasonable and achievable within the constraints they set.      
A valid request should contain the following:      
- A start and end location      
- A trip duration that is reasonable given the start and end location      
- Some other details, like the user's interests and/or preferred mode of transport      
Any request that contains potentially harmful activities is not valid, regardless of what      
other details are provided.      
If the request is not valid, set      
plan_is_valid = 0 and use your travel expertise to update the request to make it valid,      
keeping your revised request shorter than 100 words.      
If the request seems reasonable, then set plan_is_valid = 1 and      
don't revise the request.      
{format_instructions}    
"""

        self.human_template = """      
####{query}####    
"""

        self.parser = PydanticOutputParser(pydantic_object=Validation)
        self.system_message_prompt = SystemMessagePromptTemplate.from_template(
            self.system_template,
            partial_variables={
                "format_instructions": self.parser.get_format_instructions()
            },
        )
        self.human_message_prompt = HumanMessagePromptTemplate.from_template(
            self.human_template, input_variables=["query"]
        )
        self.chat_prompt = ChatPromptTemplate.from_messages(
            [self.system_message_prompt, self.human_message_prompt]
        )

Наш класс Validation содержит определения выходной схемы для запроса, который будет JSON-объектом с двумя ключами plan_is_valid и updated_request. Внутри ValidationTemplate мы используем полезные шаблонные классы LangChain для построения нашего приглашения и также создаем объект парсера с PydanicOutputParser. Это преобразует код Pydantic в Validation в набор инструкций, которые могут быть переданы в LLM вместе с запросом. Затем мы можем включить ссылку на эти инструкции формата в системный шаблон. Каждый раз, когда вызывается API, мы хотим отправить и system_message_prompt, и human_message_prompt в LLM, поэтому мы объединяем их вместе в chat_prompt.

Поскольку это на самом деле не приложение для чат-бота (хотя его можно сделать таким!), мы могли бы просто поместить как системные, так и человеческие шаблоны в одну строку и получить тот же ответ.

Теперь мы можем создать класс Agent, который использует LangChain для вызова LLM API с вышеопределенным шаблоном. Здесь мы используем ChatOpenAI, но его можно заменить на GooglePalm, если вам это больше нравится.

Обратите внимание, что мы также используем здесь LLMChain и SequentialChain из Langchain, хотя мы делаем только один вызов LLM. Это, вероятно, избыточно, но может быть полезным для расширяемости в будущем, если мы захотели бы добавить еще один вызов, например, к API модерации OpenAI, перед выполнением цепи проверки.

import openaiimport loggingimport time# для Palmfrom langchain.llms import GooglePalm# для OpenAIfrom langchain.chat_models import ChatOpenAIfrom langchain.chains import LLMChain, SequentialChainlogging.basicConfig(level=logging.INFO)class Agent(object):    def __init__(        self,        open_ai_api_key,        model="gpt-3.5-turbo",        temperature=0,        debug=True,    ):        self.logger = logging.getLogger(__name__)        self.logger.setLevel(logging.INFO)        self._openai_key = open_ai_api_key        self.chat_model = ChatOpenAI(model=model, temperature=temperature, openai_api_key=self._openai_key)        self.validation_prompt = ValidationTemplate()        self.validation_chain = self._set_up_validation_chain(debug)    def _set_up_validation_chain(self, debug=True):              # создаем цепочку валидации        validation_agent = LLMChain(            llm=self.chat_model,            prompt=self.validation_prompt.chat_prompt,            output_parser=self.validation_prompt.parser,            output_key="validation_output",            verbose=debug,        )                # добавляем в последовательную цепочку         overall_chain = SequentialChain(            chains=[validation_agent],            input_variables=["query", "format_instructions"],            output_variables=["validation_output"],            verbose=debug,        )        return overall_chain    def validate_travel(self, query):        self.logger.info("Валидация запроса")        t1 = time.time()        self.logger.info(            "Вызываем валидацию (модель {}) для входных данных пользователя".format(                self.chat_model.model_name            )        )        validation_result = self.validation_chain(            {                "query": query,                "format_instructions": self.validation_prompt.parser.get_format_instructions(),            }        )        validation_test = validation_result["validation_output"].dict()        t2 = time.time()        self.logger.info("Время выполнения запроса на проверку: {}".format(round(t2 - t1, 2)))        return validation_test

Для запуска примера мы можем использовать следующий код. Установка debug=True активирует режим отладки LangChain, который печатает процесс текста запроса на его пути через различные классы LangChain при обращении к LLM.

secrets = load_secets()travel_agent = Agent(open_ai_api_key=secrets[OPENAI_API_KEY],debug=True)query = """        Я хочу сделать 5-дневное автомобильное путешествие от Кейптауна до Претории в Южной Африке.        Я хочу посетить удаленные места с видами на горы.        """travel_agent.validate_travel(query)

Этот запрос кажется разумным, поэтому мы получим результат вот такой:

INFO:__main__:Валидация запросаINFO:__main__:Вызываем валидацию (модель gpt-3.5-turbo) для входных данных пользователяINFO:__main__:Время выполнения запроса на проверку: 1,08{'plan_is_valid': 'да', 'updated_request': ''}

Сейчас мы проведем тест, изменив запрос на менее разумный, например

query = """        Хочу пройти пешком от Кейптауна до Претории в Южной Африке.        Хочу посетить удаленные места с видом на горы.        """

Время ответа увеличивается, поскольку ChatGPT пытается объяснить, почему запрос недействителен, и поэтому генерирует больше токенов.

INFO:__main__:Проверка запросаINFO:__main__:Вызов проверки (модель gpt-3.5-turbo) пользовательских данныхINFO:__main__:Время проверки запроса: 4.12{'plan_is_valid': 'no', 'updated_request': 'Walking from Cape Town to Pretoria in South Africa is not ...' a

4. Запрос на маршрут

Если запрос является действительным, он может перейти к следующей стадии, которая является запросом на маршрут. Здесь мы хотим, чтобы модель вернула детальный предлагаемый план путешествия, который должен быть в форме маркированного списка с адресами пунктов назначения и некоторыми советами о том, что делать в каждом месте. Это действительно главная “генеративная” часть проекта, и есть много способов создать хороший запрос, чтобы получить хорошие результаты здесь. Наш ItineraryTemplate выглядит следующим образом

class ItineraryTemplate(object):    def __init__(self):        self.system_template = """      Ты - туристический агент, который помогает пользователям создавать захватывающие планы путешествия.      Запрос пользователя будет обозначен четырьмя символами решетки. Преобразуйте      запрос пользователя в подробный маршрут, описывающий места,      которые они должны посетить, и то, что они должны сделать.      Постарайтесь включить конкретный адрес каждого места.      Помните учет предпочтений пользователя и ограничения времени,      и предложите план, который был бы интересным и выполнимым исходя из их условий.      Верните маршрут в виде маркированного списка с ясно указанными начальным и конечным пунктами.      Обязательно укажите тип транспорта поездки.      Если не указаны конкретное начальное и конечное местоположение, выберите те, которые считаете подходящими, и укажите конкретные адреса.      Ваш вывод должен представлять собой только список и ничего более.    """        self.human_template = """      ####{query}####    """        self.system_message_prompt = SystemMessagePromptTemplate.from_template(            self.system_template,        )        self.human_message_prompt = HumanMessagePromptTemplate.from_template(            self.human_template, input_variables=["query"]        )        self.chat_prompt = ChatPromptTemplate.from_messages(            [self.system_message_prompt, self.human_message_prompt]        )

Обратите внимание, что здесь нет необходимости в парсере Pydantic, поскольку мы хотим, чтобы вывод был строкой, а не объектом JSON.

Чтобы использовать это, мы можем добавить новую цепочку LLMChain в класс Agent, который выглядит следующим образом

        travel_agent = LLMChain(            llm=self.chat_model,            prompt=self.itinerary_prompt.chat_prompt,            verbose=debug,            output_key="agent_suggestion",        )

Мы не установили аргумент max_tokens при создании экземпляра chat_model здесь, что позволяет модели определить длину своего вывода. Особенно с GPT4 это может занять довольно много времени (в некоторых случаях более 30 секунд). Интересно, что время ответа PaLM значительно короче.

5. Запрос о местах промежуточного пункта

Использование запроса о маршруте может дать нам хороший список пунктов промежуточного пункта, например, такой

- День 1:  - Начало путешествия в Беркли, Калифорния  - Поездка в Редвуд Национальный Парк, Калифорния (1111 Second St, Crescent City, CA 95531)  - Исследование красивых секвойевых лесов и наслаждение природой  - Поездка в Юрику, Калифорния (531 2nd St, Eureka, CA 95501)  - Наслаждение местной кухней и исследование очаровательного города  - Остановка ночью в Юрике, Калифорния- День 2:  - Начало путешествия в Юрику, Калифорния  - Поездка в Кратер Лейк Национальный Парк, Орегон (Crater Lake National Park, OR 97604)  - Восхищение удивительным голубым озером и походы по живописным тропам  - Поездка в Бенд, Орегон (Bend, OR 97701)  - Разнообразие местной кухни и исследование активного города  - Остановка ночью в Бенде, Орегон- День 3:  - Начало путешествия в Бенд, Орегон  - Поездка в Национальный Парк Маунт Рейнир, Вашингтон (55210 238th Ave E, Ashford, WA 98304)  - Наслаждение захватывающими видами на горы и походы по тропам  - Поездка в Такому, Вашингтон (Tacoma, WA 98402)  - Попробовать вкусные блюда и исследование достопримечательностей города  - Остановка ночью в Такоме, Вашингтон- День 4:  - Начало путешествия в Такому, Вашингтон  - Поездка в Олимпийский Национальный Парк, Вашингтон (3002 Mount Angeles Rd, Port Angeles, WA 98362)  - Исследование разнообразных экосистем парка и наслаждение естественной красотой  - Поездка в Сиэтл, Вашингтон (Seattle, WA 98101)  - Ознакомление с активной кулинарной сценой и посещение популярных достопримечательностей  - Остановка ночью в Сиэтле, Вашингтон- День 5:  - Начало путешествия в Сиэтл, Вашингтон  - Исследование большего количества достопримечательностей города и наслаждение местной кухней  - Закончить путешествие в Сиэтле, Вашингтон

Теперь нам необходимо извлечь адреса путевых точек, чтобы перейти к следующему шагу, который будет состоять в их отображении на карте и вызове API направлений Google Maps для получения маршрутов между ними.

Для этого мы снова сделаем другой вызов LLM и снова используем PydanicOutputParser, чтобы убедиться, что наш вывод отформатирован правильно. Чтобы понять формат здесь, полезно кратко рассмотреть, что мы хотим делать на следующем этапе этого проекта (описано в части 2). Мы будем делать вызов к Python API Google Maps, который выглядит как

import googlemapsgmaps = googlemaps.Client(key=google_maps_api_key)directions_result = gmaps.directions(            start,            end,            waypoints=waypoints,            mode=transit_type,            units="metric",            optimize_waypoints=True,            traffic_model="best_guess",            departure_time=start_time,)

Где start и end – это адреса в виде строк, а waypoints – это список адресов, которые нужно посетить по пути.

Наши запрошенные схемы для извлечения путевых точек поэтому выглядят следующим образом

class Trip(BaseModel):    start: str = Field(description="начальное местоположение поездки")    end: str = Field(description="конечное местоположение поездки")    waypoints: List[str] = Field(description="список путевых точек")    transit: str = Field(description="способ передвижения")

Это позволит нам подключить выводы вызова LLM к вызову направлений.

Для этого промпта я обнаружил, что добавление примера с одним выстрелом действительно помогло модели соответствовать желаемому выводу. Тонкая настройка меньшей, открытой LLM для извлечения списков путевых точек, используя эти результаты от ChatGPT/PaLM, может быть интересным проектом-побочкой здесь.

class MappingTemplate(object):    def __init__(self):        self.system_template = """      Вы - агент, который превращает подробные планы путешествия в простой список мест.      Маршрут будет обозначен четырьмя знаками хэштега. Преобразуйте его в      список мест, которые они должны посетить. Постарайтесь указать конкретный адрес каждого места.      Ваш вывод должен всегда содержать начальную и конечную точку поездки, а также может содержать список      путевых точек. Он также должен включать способ передвижения. Количество путевых точек не может превышать 20.      Если вы не можете определить способ передвижения, сделайте лучшую догадку, исходя из места поездки.      Например:      ####      Itinerary for a 2-day driving trip within London:      - Day 1:        - Start at Buckingham Palace (The Mall, London SW1A 1AA)        - Visit the Tower of London (Tower Hill, London EC3N 4AB)        - Explore the British Museum (Great Russell St, Bloomsbury, London WC1B 3DG)        - Enjoy shopping at Oxford Street (Oxford St, London W1C 1JN)        - End the day at Covent Garden (Covent Garden, London WC2E 8RF)      - Day 2:        - Start at Westminster Abbey (20 Deans Yd, Westminster, London SW1P 3PA)        - Visit the Churchill War Rooms (Clive Steps, King Charles St, London SW1A 2AQ)        - Explore the Natural History Museum (Cromwell Rd, Kensington, London SW7 5BD)        - End the trip at the Tower Bridge (Tower Bridge Rd, London SE1 2UP)      #####      Output:      Start: Buckingham Palace, The Mall, London SW1A 1AA      End: Tower Bridge, Tower Bridge Rd, London SE1 2UP      Waypoints: ["Tower of London, Tower Hill, London EC3N 4AB", "British Museum, Great Russell St, Bloomsbury, London WC1B 3DG", "Oxford St, London W1C 1JN", "Covent Garden, London WC2E 8RF","Westminster, London SW1A 0AA", "St. James's Park, London", "Natural History Museum, Cromwell Rd, Kensington, London SW7 5BD"]      Transit: driving      Транзит может быть только одним из следующих вариантов: "driving", "train", "bus" или "flight".      {format_instructions}    """        self.human_template = """      ####{agent_suggestion}####    """        self.parser = PydanticOutputParser(pydantic_object=Trip)        self.system_message_prompt = SystemMessagePromptTemplate.from_template(            self.system_template,            partial_variables={                "format_instructions": self.parser.get_format_instructions()            },        )        self.human_message_prompt = HumanMessagePromptTemplate.from_template(            self.human_template, input_variables=["agent_suggestion"]        )        self.chat_prompt = ChatPromptTemplate.from_messages(            [self.system_message_prompt, self.human_message_prompt]        )

Теперь добавим в класс Agent новый метод, который может вызывать LLM с помощью ItineraryTemplate и MappingTemplate последовательно с использованием SequentialChain

def _set_up_agent_chain(self, debug=True):      # настроим LLMChain для получения маршрута в виде строки    travel_agent = LLMChain(            llm=self.chat_model,            prompt=self.itinerary_prompt.chat_prompt,            verbose=debug,            output_key="agent_suggestion",        )        # настроим LLMChain для извлечения точек пути в виде объекта JSON    parser = LLMChain(            llm=self.chat_model,            prompt=self.mapping_prompt.chat_prompt,            output_parser=self.mapping_prompt.parser,            verbose=debug,            output_key="mapping_list",        )         # цепочка overall_chain позволяет нам вызывать travel_agent и parser    # последовательно с помощью маркированных выходных данных.    overall_chain = SequentialChain(            chains=[travel_agent, parser],            input_variables=["query", "format_instructions"],            output_variables=["agent_suggestion", "mapping_list"],            verbose=debug,        )    return overall_chain

Для совершения этих вызовов мы можем использовать следующий код

agent_chain = travel_agent._set_up_agent_chain()mapping_prompt = MappingTemplate()agent_result = agent_chain(                {                    "query": query,                    "format_instructions": mapping_prompt.parser.get_format_instructions(),                }            )trip_suggestion = agent_result["agent_suggestion"]waypoints_dict = agent_result["mapping_list"].dict()

Адреса в waypoints_dict должны быть достаточно отформатированы для использования с Google Maps, но их также можно преобразовать в геокоды, чтобы снизить вероятность ошибок при вызове API направлений. Словарь точек маршрута должен выглядеть примерно так.

{'start': 'Berkeley, CA', 'end': 'Seattle, WA', 'waypoints': ['Redwood National and State Parks, 1111 Second St, Crescent City, CA 95531', 'Crater Lake National Park, Crater Lake National Park, OR 97604', 'Mount Rainier National Park, 55210 238th Ave E, Ashford, WA 98304', 'Olympic National Park, 3002 Mount Angeles Rd, Port Angeles, WA 98362'], 'transit': 'driving'}

6. Сбор всех частей вместе

Теперь у нас есть возможность использовать LLM для проверки туристического запроса, создания детального маршрута и извлечения точек пути в виде объекта JSON, который может быть передан ниже по потоку данных. Вы увидите, что в коде почти все это функциональность обрабатывается классом Agent, который создается внутри TravelMapperBase и используется следующим образом

travel_agent = Agent(   open_ai_api_key=openai_api_key,   google_palm_api_key=google_palm_api_key,   debug=verbose,)itinerary, list_of_places, validation = travel_agent.suggest_travel(query)

Использование LangChain позволяет очень легко заменить используемый LLM. Для PALM нам просто нужно объявить

from langchain.llms import GooglePalmAgent.chat_model = GooglePalm(   model_name="models/text-bison-001",   temperature=0,   google_api_key=google_palm_api_key,)

А для OpenAI мы можем использовать ChatOpenAI или OpenAI, как описано в приведенных выше разделах.

Теперь мы готовы перейти к следующему этапу: как преобразовать список мест в набор указаний и отобразить их на карте для показа пользователю? Об этом будет рассказано во второй части этой трехчастной серии.

Спасибо за чтение! Пожалуйста, не стесняйтесь изучать полный код здесь https://github.com/rmartinshort/travel_mapper. Буду благодарен за любые предложения по улучшению или расширению функциональности!