Чат LangChain с пользовательскими инструментами, функциями и памятью

CoderStudio, 11.07.2023 16:16
Чат LangChain с пользовательскими инструментами, функциями и памятью
Фото Barn Images на Unsplash

В этой статье мы рассмотрим, как можно создать простое веб-приложение для чата, которое взаимодействует с частным REST API, использует функции OpenAI и разговорную память .

Мы снова будем использовать фреймворк LangChain , который предоставляет отличную инфраструктуру для взаимодействия с Large Language Models (LLM).

Агент, который мы опишем в этой заметке, будет использовать эти инструменты:

  • Wikipedia с LangChain's WikipediaAPIWrapper .
  • DuckDuckGo Search с LangChain's DuckDuckGoSearchAPIWrapper
  • Pubmed с PubMedAPIWrapper
  • Математическая цепочка LLM с помощью LLMMathChain
  • Events API с собственной реализацией, о которой мы расскажем позже.

Агент будет иметь два пользовательских интерфейса:

  • Tweb-клиент на базе Streamlit .
  • Интерфейс командной строки

Функции OpenAI

Одна из основных проблем при работе с ответами от LLM типа ChatGPT заключается в том, что ответы не являются полностью предсказуемыми. При попытке разобрать ответы могут возникнуть небольшие отклонения в выводе, которые делают разбор программными средствами ошибочным. Поскольку агенты LangChain передают пользовательский ввод в LLM и ожидают, что он направит вывод определенному инструменту (или функции), агенты должны уметь анализировать предсказуемый вывод.

Для решения этой проблемы 13 июня 2023 года OpenAI представил функцию "Вызов функций", которая позволяет разработчикам описывать вывод JSON, описывающий, какие функции (на языке LangChain: инструменты) следует вызвать на основе определенного входного сигнала.

Подход, изначально используемый агентами LangChain - например, агентом ZERO_SHOT_REACT_DESCRIPTION, - использует оперативную инженерию для маршрутизации сообщений к нужным инструментам. Такой подход потенциально менее точен и медленнее, чем подход с использованием функций OpenAI.

На момент написания этого блога модели, поддерживающие данную функцию, следующие:

  • gpt-4-0613
  • gpt-3.5-turbo-0613 (сюда входит gpt-3.5-turbo-16k-0613, который мы использовали для чат-агента вашей игровой площадки)

Вот несколько примеров, приведенных OpenAI, о том, как работает вызов функций:

Преобразование запросов типа "Напишите Ане, чтобы узнать, не хочет ли она выпить кофе в следующую пятницу" в вызов функции send_email(to: string, body: string), или "Какая погода в Бостоне?" в get_current_weather(location: string, unit: 'celsius' | 'fahrenheit').

Внутренние инструкции о функции и ее параметрах вставляются в системное сообщение.

Конечной точкой API является:

POST https://api.openai.com/v1/chat/completions .

Детали API низкого уровня можно найти здесь:

OpenAI Platform

Агентский цикл

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

Цикл агента
Цикл агента

Пользовательские и LangChain инструменты

Агент LangChain использует инструменты (соответствует функциям OpenAPI). LangChain (версия 0.0.220) поставляется с множеством инструментов, позволяющих подключаться к различным платным и бесплатным сервисам или взаимодействиям, например, таким как:

  • arxiv (бесплатно)
  • azure_cognitive_services
  • bing_search
  • brave_search
  • ddg_search
  • управление_файлами
  • gmail
  • google_places
  • google_search
  • google_serper
  • graphql
  • взаимодействие с людьми
  • jira
  • json
  • метафора_поиск
  • офис365
  • openapi
  • XY- 53-WZ
  • playwright
  • powerbi
  • pubmed
  • python
  • запросы
  • scenexplain
  • searx_search
  • shell
  • sleep
  • spark_sql
  • база данных sql_
  • steamship_image_generation
  • vectorstore
  • википедия (бесплатно)
  • wolfram_alpha
  • youtube
  • zapier

В этом блоге мы покажем, как можно создать пользовательский инструмент для доступа к пользовательскому REST API.

Разговорная память

Этот тип памяти может пригодиться, когда необходимо вспомнить элементы из предыдущих вводов. Например, если вы спросите "Кто такой Альберт Эйнштейн?", а затем спросите "Кто был его наставником?", то разговорная память поможет агенту вспомнить, что "его" относится к "Альберту Эйнштейну".

Вот документация LangChain по памяти .

Агент с функциями, пользовательским инструментом и памятью

Наш агент можно найти в Git-репозитории:

GitHub - gilfernandes/chat_functions: Простое приложение для чата на игровой площадке, которое взаимодействует с функциями OpenAI с помощью памяти и пользовательских инструментов.

Для того чтобы приложение заработало, сначала установите Conda .

Затем создайте следующее окружение и установите следующие библиотеки:

conda activate langchain_streamlit
pip install langchain
pip install prompt_toolkit
pip install wikipedia
pip install arxiv
pip install python-dotenv
pip install streamlit
pip install openai
pip install duckduckgo-search

Затем создайте файл .env с этим содержимым:

OPENAI_API_KEY=<key>

Затем можно запустить версию агента в командной строке, используя:

python .\agent_cli.py

А версию Streamlit можно запустить с помощью этой команды на порту 8080:

streamlit run ./agent_streamlit.py --server.port 8080

Вот несколько скриншотов инструмента:

Использование пользовательского агента
Использование пользовательского агента

В пользовательском интерфейсе видно, какой инструмент (функция OpenAI) используется и какие входные данные были ему отправлены.

Использование инструмента Arxiv
Использование инструмента Arxiv
Использование калькулятора
Использование калькулятора
Использование Википедии
Использование Википедии

Детали кода

Код состоит из следующих основных частей:

Настройка агента

В следующем фрагменте кода необходимо настроить агента с его инструментальной памятью:

def setup_agent() -> AgentExecutor:
    """
    Sets up the tools for a function based chain.
    We have here the following tools:
    - wikipedia
    - duduckgo
    - calculator
    - arxiv
    - events (a custom tool)
    - pubmed
    """
    cfg = Config()
    duckduck_search = DuckDuckGoSearchAPIWrapper()
    wikipedia = WikipediaAPIWrapper()
    pubmed = PubMedAPIWrapper()
    events = tools_wrappers.EventsAPIWrapper()
    events.doc_content_chars_max=5000
    llm_math_chain = LLMMathChain.from_llm(llm=cfg.llm, verbose=False)
    arxiv = ArxivAPIWrapper()
    tools = [
        Tool(
            name="Search",
            func=duckduck_search.run,
            description="useful for when you need to answer questions about current events. You should ask targeted questions"
        ),
        Tool(
            name="Calculator",
            func=llm_math_chain.run,
            description="useful for when you need to answer questions about math"
        ),
        Tool(
            name="Wikipedia",
            func=wikipedia.run,
            description="useful when you need an answer about encyclopedic general knowledge"
        ),
        Tool(
            name="Arxiv",
            func=arxiv.run,
            description="useful when you need an answer about encyclopedic general knowledge"
        ),
        # This is the custom tool. Note that the OpenAPI Function parameters are inferred via analysis of the `events.run`` method
        StructuredTool.from_function(
            func=events.run,
            name="Events",
            description="useful when you need an answer about meditation related events in the united kingdom"
        ),
        StructuredTool.from_function(
            func=pubmed.run, 
            name='PubMed',
            description='Useful tool for querying medical publications'
        )
    ]
    agent_kwargs, memory = setup_memory()

    return initialize_agent(
        tools, 
        cfg.llm, 
        agent=AgentType.OPENAI_FUNCTIONS, 
        verbose=False, 
        agent_kwargs=agent_kwargs,
        memory=memory
    )

В этой функции происходит настройка памяти:

def setup_memory() -> Tuple[Dict, ConversationBufferMemory]:
    """
    Sets up memory for the open ai functions agent.
    :return a tuple with the agent keyword pairs and the conversation memory.
    """
    agent_kwargs = {
        "extra_prompt_messages": [MessagesPlaceholder(variable_name="memory")],
    }
    memory = ConversationBufferMemory(memory_key="memory", return_messages=True)

    return agent_kwargs, memory

И в этом режиме агент находится:

class Config():
    """
    Contains the configuration of the LLM.
    """
    model = 'gpt-3.5-turbo-16k-0613'
    llm = ChatOpenAI(temperature=0, model=model)

Это пользовательский инструмент, вызывающий простой REST API:

import requests
import urllib.parse

from typing import Dict, Optional
from pydantic import BaseModel, Extra

class EventsAPIWrapper(BaseModel):
    """Wrapper around a custom API used to fetch public event information.

    There is no need to install any package to get this to work.
    """

    offset: int = 0
    limit: int = 10
    filter_by_country: str="United Kingdom"
    doc_content_chars_max: int = 4000

    class Config:
        """Configuration for this pydantic object."""

        extra = Extra.forbid

    def run(self, query: str) -> str:
        """Run Events search and get page summaries."""
        encoded_query =  urllib.parse.quote_plus(query)
        encoded_filter_by_country =  urllib.parse.quote_plus(self.filter_by_country)
        response = requests.get(f"https://events.brahmakumaris.org/events-rest/event-search-v2?search = {encoded_query}" + 
                     f"&limit=10&offset = {self.offset}&filterByCountry = {encoded_filter_by_country}&includeDescription=true")
        if response.status_code >= 200 and response.status_code < 300:
            json = response.json()
            summaries = [self._formatted_event_summary(e) for e in json['events']]
            return "\n\n".join(summaries)[: self.doc_content_chars_max]
        else:
            return f"Failed to call events API with status code {response.status_code}"

    @staticmethod
    def _formatted_event_summary(event: Dict) -> Optional[str]:
        return (f"Event: {event['name']}\n" + 
                f"Start: {event['startDate']} {event['startTime']}\n" + 
                f"End: {event['endDate']} {event['endTime']}\n" +
                f"Venue: {event['venueAddress']} {event['postalCode']} {event['locality']} {event['countryName']}\n" +
                f"Event Description: {event['description']}\n" +
                f"Event URL: https://brahmakumaris.uk/event/?id = {event['id']}\n"
        )

Цикл CLI агента

Цикл CLI агента использует простой цикл while:

from prompt_toolkit import HTML, prompt, PromptSession
from prompt_toolkit.history import FileHistory

from langchain.input import get_colored_text
from dotenv import load_dotenv
from langchain.agents import AgentExecutor

import langchain
from callbacks import AgentCallbackHandler

load_dotenv()
from chain_setup import setup_agent

langchain.debug = True

if __name__ == "__main__":

    agent_executor: AgentExecutor = setup_agent()
    
    session = PromptSession(history=FileHistory(".agent-history-file"))
    while True:
        question = session.prompt(
            HTML("<b>Type <u>Your question</u></b>  ('q' to exit): ")
        )
        if question.lower() == 'q':
            break
        if len(question) == 0:
            continue
        try:
            print(get_colored_text("Response: >>> ", "green"))
            print(get_colored_text(agent_executor.run(question, callbacks=[AgentCallbackHandler()]), "green"))
        except Exception as e:
            print(get_colored_text(f"Failed to process {question}", "red"))
            print(get_colored_text(f"Error {e}", "red"))

Приложение Streamlit API

Это приложение Streamlit, которое отображает пользовательский интерфейс, показанный выше:

import streamlit as st
from dotenv import load_dotenv

from langchain.agents import AgentExecutor

import callbacks

load_dotenv()

from chain_setup import setup_agent

QUESTION_HISTORY: str = 'question_history'

def init_stream_lit():
    title="Chat Functions Introduction"
    st.set_page_config(page_title=title, layout="wide")
    agent_executor: AgentExecutor = prepare_agent()
    st.header(title)
    if QUESTION_HISTORY not in st.session_state:
        st.session_state[QUESTION_HISTORY] = []
    intro_text()
    simple_chat_tab, historical_tab = st.tabs(["Simple Chat", "Session History"])
    with simple_chat_tab:
        user_question = st.text_input("Your question")
        with st.spinner('Please wait ...'):
            try:
                response = agent_executor.run(user_question, callbacks=[callbacks.StreamlitCallbackHandler(st)])
                st.write(f"{response}")
                st.session_state[QUESTION_HISTORY].append((user_question, response))
            except Exception as e:
                st.error(f"Error occurred: {e}")
    with historical_tab:
        for q in st.session_state[QUESTION_HISTORY]:
            question = q[0]
            if len(question) > 0:
                st.write(f"Q: {question}")
                st.write(f"A: {q[1]}")


def intro_text():
    with st.expander("Click to see application info:"):
        st.write(f"""Ask questions about:
- [Wikipedia](https://www.wikipedia.org/) Content
- Scientific publications ([pubmed](https://pubmed.ncbi.nlm.nih.gov) and [arxiv](https://arxiv.org))
- Mathematical calculations
- Search engine content ([DuckDuckGo](https://duckduckgo.com/))
- Meditation related events (Custom Tool)
    """)
        
@st.cache_resource()
def prepare_agent() -> AgentExecutor:
    return setup_agent()


if __name__ == "__main__":
    init_stream_lit()

Сводка

Функции OpenAI теперь можно легко использовать с LangChain. И это, похоже, гораздо лучший метод (более быстрый и точный) для создания агентов по сравнению с подходами, основанными на подсказках.

Функции OpenAI также могут быть легко интегрированы с памятью и пользовательскими инструментами. Нет никаких оправданий, чтобы не использовать OpenAI-функции для создания агента, описанного в этом блоге.