В этой статье мы рассмотрим, как можно создать простое веб-приложение для чата, которое взаимодействует с частным REST API, использует функции OpenAI и разговорную память .
Мы снова будем использовать фреймворк LangChain , который предоставляет отличную инфраструктуру для взаимодействия с Large Language Models (LLM).
Агент, который мы опишем в этой заметке, будет использовать эти инструменты:
Агент будет иметь два пользовательских интерфейса:
Одна из основных проблем при работе с ответами от LLM типа ChatGPT заключается в том, что ответы не являются полностью предсказуемыми. При попытке разобрать ответы могут возникнуть небольшие отклонения в выводе, которые делают разбор программными средствами ошибочным. Поскольку агенты LangChain передают пользовательский ввод в LLM и ожидают, что он направит вывод определенному инструменту (или функции), агенты должны уметь анализировать предсказуемый вывод.
Для решения этой проблемы 13 июня 2023 года OpenAI представил функцию "Вызов функций", которая позволяет разработчикам описывать вывод JSON, описывающий, какие функции (на языке LangChain: инструменты) следует вызвать на основе определенного входного сигнала.
Подход, изначально используемый агентами LangChain - например, агентом ZERO_SHOT_REACT_DESCRIPTION, - использует оперативную инженерию для маршрутизации сообщений к нужным инструментам. Такой подход потенциально менее точен и медленнее, чем подход с использованием функций OpenAI.
На момент написания этого блога модели, поддерживающие данную функцию, следующие:
Вот несколько примеров, приведенных 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 использует инструменты (соответствует функциям OpenAPI). LangChain (версия 0.0.220) поставляется с множеством инструментов, позволяющих подключаться к различным платным и бесплатным сервисам или взаимодействиям, например, таким как:
В этом блоге мы покажем, как можно создать пользовательский инструмент для доступа к пользовательскому 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) используется и какие входные данные были ему отправлены.
Код состоит из следующих основных частей:
В следующем фрагменте кода необходимо настроить агента с его инструментальной памятью:
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 агента использует простой цикл 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, которое отображает пользовательский интерфейс, показанный выше:
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-функции для создания агента, описанного в этом блоге.