From c6c799fb23caad5bf86b0a8970f4d360649c70f8 Mon Sep 17 00:00:00 2001 From: Bob Lin Date: Mon, 4 Dec 2023 14:12:30 -0600 Subject: [PATCH] Add openai v2 adapter (#14063) ### Description Starting from [openai version 1.0.0](https://github.com/openai/openai-python/tree/17ac6779958b2b74999c634c4ea4c7b74906027a#module-level-client), the camel case form of `openai.ChatCompletion` is no longer supported and has been changed to lowercase `openai.chat.completions`. In addition, the returned object only accepts attribute access instead of index access: ```python import openai # optional; defaults to `os.environ['OPENAI_API_KEY']` openai.api_key = '...' # all client options can be configured just like the `OpenAI` instantiation counterpart openai.base_url = "https://..." openai.default_headers = {"x-foo": "true"} completion = openai.chat.completions.create( model="gpt-4", messages=[ { "role": "user", "content": "How do I output all files in a directory using Python?", }, ], ) print(completion.choices[0].message.content) ``` So I implemented a compatible adapter that supports both attribute access and index access: ```python In [1]: from langchain.adapters import openai as lc_openai ...: messages = [{"role": "user", "content": "hi"}] In [2]: result = lc_openai.chat.completions.create( ...: messages=messages, model="gpt-3.5-turbo", temperature=0 ...: ) In [3]: result.choices[0].message Out[3]: {'role': 'assistant', 'content': 'Hello! How can I assist you today?'} In [4]: result["choices"][0]["message"] Out[4]: {'role': 'assistant', 'content': 'Hello! How can I assist you today?'} In [5]: result = await lc_openai.chat.completions.acreate( ...: messages=messages, model="gpt-3.5-turbo", temperature=0 ...: ) In [6]: result.choices[0].message Out[6]: {'role': 'assistant', 'content': 'Hello! How can I assist you today?'} In [7]: result["choices"][0]["message"] Out[7]: {'role': 'assistant', 'content': 'Hello! How can I assist you today?'} In [8]: for rs in lc_openai.chat.completions.create( ...: messages=messages, model="gpt-3.5-turbo", temperature=0, stream=True ...: ): ...: print(rs.choices[0].delta) ...: print(rs["choices"][0]["delta"]) ...: {'role': 'assistant', 'content': ''} {'role': 'assistant', 'content': ''} {'content': 'Hello'} {'content': 'Hello'} {'content': '!'} {'content': '!'} In [20]: async for rs in await lc_openai.chat.completions.acreate( ...: messages=messages, model="gpt-3.5-turbo", temperature=0, stream=True ...: ): ...: print(rs.choices[0].delta) ...: print(rs["choices"][0]["delta"]) ...: {'role': 'assistant', 'content': ''} {'role': 'assistant', 'content': ''} {'content': 'Hello'} {'content': 'Hello'} {'content': '!'} {'content': '!'} ... ``` ### Twitter handle [lin_bob57617](https://twitter.com/lin_bob57617) --- .../integrations/adapters/openai-old.ipynb | 285 ++++++++++++++++++ docs/docs/integrations/adapters/openai.ipynb | 113 ++++--- libs/langchain/langchain/adapters/openai.py | 137 ++++++++- 3 files changed, 495 insertions(+), 40 deletions(-) create mode 100644 docs/docs/integrations/adapters/openai-old.ipynb diff --git a/docs/docs/integrations/adapters/openai-old.ipynb b/docs/docs/integrations/adapters/openai-old.ipynb new file mode 100644 index 0000000000000..fee3ab5a50169 --- /dev/null +++ b/docs/docs/integrations/adapters/openai-old.ipynb @@ -0,0 +1,285 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "700a516b", + "metadata": {}, + "source": [ + "# OpenAI Adapter(Old)\n", + "\n", + "**Please ensure OpenAI library is less than 1.0.0; otherwise, refer to the newer doc [OpenAI Adapter](./openai.ipynb).**\n", + "\n", + "A lot of people get started with OpenAI but want to explore other models. LangChain's integrations with many model providers make this easy to do so. While LangChain has it's own message and model APIs, we've also made it as easy as possible to explore other models by exposing an adapter to adapt LangChain models to the OpenAI api.\n", + "\n", + "At the moment this only deals with output and does not return other information (token counts, stop reasons, etc)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "6017f26a", + "metadata": {}, + "outputs": [], + "source": [ + "import openai\n", + "from langchain.adapters import openai as lc_openai" + ] + }, + { + "cell_type": "markdown", + "id": "b522ceda", + "metadata": {}, + "source": [ + "## ChatCompletion.create" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "1d22eb61", + "metadata": {}, + "outputs": [], + "source": [ + "messages = [{\"role\": \"user\", \"content\": \"hi\"}]" + ] + }, + { + "cell_type": "markdown", + "id": "d550d3ad", + "metadata": {}, + "source": [ + "Original OpenAI call" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "012d81ae", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'role': 'assistant', 'content': 'Hello! How can I assist you today?'}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = openai.ChatCompletion.create(\n", + " messages=messages, model=\"gpt-3.5-turbo\", temperature=0\n", + ")\n", + "result[\"choices\"][0][\"message\"].to_dict_recursive()" + ] + }, + { + "cell_type": "markdown", + "id": "db5b5500", + "metadata": {}, + "source": [ + "LangChain OpenAI wrapper call" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "c67a5ac8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'role': 'assistant', 'content': 'Hello! How can I assist you today?'}" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lc_result = lc_openai.ChatCompletion.create(\n", + " messages=messages, model=\"gpt-3.5-turbo\", temperature=0\n", + ")\n", + "lc_result[\"choices\"][0][\"message\"]" + ] + }, + { + "cell_type": "markdown", + "id": "034ba845", + "metadata": {}, + "source": [ + "Swapping out model providers" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "f7c94827", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'role': 'assistant', 'content': ' Hello!'}" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lc_result = lc_openai.ChatCompletion.create(\n", + " messages=messages, model=\"claude-2\", temperature=0, provider=\"ChatAnthropic\"\n", + ")\n", + "lc_result[\"choices\"][0][\"message\"]" + ] + }, + { + "cell_type": "markdown", + "id": "cb3f181d", + "metadata": {}, + "source": [ + "## ChatCompletion.stream" + ] + }, + { + "cell_type": "markdown", + "id": "f7b8cd18", + "metadata": {}, + "source": [ + "Original OpenAI call" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "fd8cb1ea", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'role': 'assistant', 'content': ''}\n", + "{'content': 'Hello'}\n", + "{'content': '!'}\n", + "{'content': ' How'}\n", + "{'content': ' can'}\n", + "{'content': ' I'}\n", + "{'content': ' assist'}\n", + "{'content': ' you'}\n", + "{'content': ' today'}\n", + "{'content': '?'}\n", + "{}\n" + ] + } + ], + "source": [ + "for c in openai.ChatCompletion.create(\n", + " messages=messages, model=\"gpt-3.5-turbo\", temperature=0, stream=True\n", + "):\n", + " print(c[\"choices\"][0][\"delta\"].to_dict_recursive())" + ] + }, + { + "cell_type": "markdown", + "id": "0b2a076b", + "metadata": {}, + "source": [ + "LangChain OpenAI wrapper call" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "9521218c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'role': 'assistant', 'content': ''}\n", + "{'content': 'Hello'}\n", + "{'content': '!'}\n", + "{'content': ' How'}\n", + "{'content': ' can'}\n", + "{'content': ' I'}\n", + "{'content': ' assist'}\n", + "{'content': ' you'}\n", + "{'content': ' today'}\n", + "{'content': '?'}\n", + "{}\n" + ] + } + ], + "source": [ + "for c in lc_openai.ChatCompletion.create(\n", + " messages=messages, model=\"gpt-3.5-turbo\", temperature=0, stream=True\n", + "):\n", + " print(c[\"choices\"][0][\"delta\"])" + ] + }, + { + "cell_type": "markdown", + "id": "0fc39750", + "metadata": {}, + "source": [ + "Swapping out model providers" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "68f0214e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'role': 'assistant', 'content': ' Hello'}\n", + "{'content': '!'}\n", + "{}\n" + ] + } + ], + "source": [ + "for c in lc_openai.ChatCompletion.create(\n", + " messages=messages,\n", + " model=\"claude-2\",\n", + " temperature=0,\n", + " stream=True,\n", + " provider=\"ChatAnthropic\",\n", + "):\n", + " print(c[\"choices\"][0][\"delta\"])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/integrations/adapters/openai.ipynb b/docs/docs/integrations/adapters/openai.ipynb index 8fd2c5214c767..0db8c7dbfbccf 100644 --- a/docs/docs/integrations/adapters/openai.ipynb +++ b/docs/docs/integrations/adapters/openai.ipynb @@ -7,6 +7,8 @@ "source": [ "# OpenAI Adapter\n", "\n", + "**Please ensure OpenAI library is version 1.0.0 or higher; otherwise, refer to the older doc [OpenAI Adapter(Old)](./openai-old.ipynb).**\n", + "\n", "A lot of people get started with OpenAI but want to explore other models. LangChain's integrations with many model providers make this easy to do so. While LangChain has it's own message and model APIs, we've also made it as easy as possible to explore other models by exposing an adapter to adapt LangChain models to the OpenAI api.\n", "\n", "At the moment this only deals with output and does not return other information (token counts, stop reasons, etc)." @@ -28,12 +30,12 @@ "id": "b522ceda", "metadata": {}, "source": [ - "## ChatCompletion.create" + "## chat.completions.create" ] }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 2, "id": "1d22eb61", "metadata": {}, "outputs": [], @@ -51,26 +53,29 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 3, "id": "012d81ae", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'role': 'assistant', 'content': 'Hello! How can I assist you today?'}" + "{'content': 'Hello! How can I assist you today?',\n", + " 'role': 'assistant',\n", + " 'function_call': None,\n", + " 'tool_calls': None}" ] }, - "execution_count": 15, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "result = openai.ChatCompletion.create(\n", + "result = openai.chat.completions.create(\n", " messages=messages, model=\"gpt-3.5-turbo\", temperature=0\n", ")\n", - "result[\"choices\"][0][\"message\"].to_dict_recursive()" + "result.choices[0].message.model_dump()" ] }, { @@ -83,26 +88,48 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 4, "id": "c67a5ac8", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'role': 'assistant', 'content': 'Hello! How can I assist you today?'}" + "{'role': 'assistant', 'content': 'Hello! How can I help you today?'}" ] }, - "execution_count": 17, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "lc_result = lc_openai.ChatCompletion.create(\n", + "lc_result = lc_openai.chat.completions.create(\n", " messages=messages, model=\"gpt-3.5-turbo\", temperature=0\n", ")\n", - "lc_result[\"choices\"][0][\"message\"]" + "\n", + "lc_result.choices[0].message # Attribute access" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "37a6e461-8608-47f6-ac45-12ad753c062a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'role': 'assistant', 'content': 'Hello! How can I help you today?'}" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lc_result[\"choices\"][0][\"message\"] # Also compatible with index access" ] }, { @@ -115,26 +142,26 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 6, "id": "f7c94827", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'role': 'assistant', 'content': ' Hello!'}" + "{'role': 'assistant', 'content': 'Hello! How can I assist you today?'}" ] }, - "execution_count": 19, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "lc_result = lc_openai.ChatCompletion.create(\n", + "lc_result = lc_openai.chat.completions.create(\n", " messages=messages, model=\"claude-2\", temperature=0, provider=\"ChatAnthropic\"\n", ")\n", - "lc_result[\"choices\"][0][\"message\"]" + "lc_result.choices[0].message" ] }, { @@ -142,7 +169,7 @@ "id": "cb3f181d", "metadata": {}, "source": [ - "## ChatCompletion.stream" + "## chat.completions.stream" ] }, { @@ -155,7 +182,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 7, "id": "fd8cb1ea", "metadata": {}, "outputs": [ @@ -163,25 +190,25 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'role': 'assistant', 'content': ''}\n", - "{'content': 'Hello'}\n", - "{'content': '!'}\n", - "{'content': ' How'}\n", - "{'content': ' can'}\n", - "{'content': ' I'}\n", - "{'content': ' assist'}\n", - "{'content': ' you'}\n", - "{'content': ' today'}\n", - "{'content': '?'}\n", - "{}\n" + "{'content': '', 'function_call': None, 'role': 'assistant', 'tool_calls': None}\n", + "{'content': 'Hello', 'function_call': None, 'role': None, 'tool_calls': None}\n", + "{'content': '!', 'function_call': None, 'role': None, 'tool_calls': None}\n", + "{'content': ' How', 'function_call': None, 'role': None, 'tool_calls': None}\n", + "{'content': ' can', 'function_call': None, 'role': None, 'tool_calls': None}\n", + "{'content': ' I', 'function_call': None, 'role': None, 'tool_calls': None}\n", + "{'content': ' assist', 'function_call': None, 'role': None, 'tool_calls': None}\n", + "{'content': ' you', 'function_call': None, 'role': None, 'tool_calls': None}\n", + "{'content': ' today', 'function_call': None, 'role': None, 'tool_calls': None}\n", + "{'content': '?', 'function_call': None, 'role': None, 'tool_calls': None}\n", + "{'content': None, 'function_call': None, 'role': None, 'tool_calls': None}\n" ] } ], "source": [ - "for c in openai.ChatCompletion.create(\n", + "for c in openai.chat.completions.create(\n", " messages=messages, model=\"gpt-3.5-turbo\", temperature=0, stream=True\n", "):\n", - " print(c[\"choices\"][0][\"delta\"].to_dict_recursive())" + " print(c.choices[0].delta.model_dump())" ] }, { @@ -194,7 +221,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 8, "id": "9521218c", "metadata": {}, "outputs": [ @@ -217,10 +244,10 @@ } ], "source": [ - "for c in lc_openai.ChatCompletion.create(\n", + "for c in lc_openai.chat.completions.create(\n", " messages=messages, model=\"gpt-3.5-turbo\", temperature=0, stream=True\n", "):\n", - " print(c[\"choices\"][0][\"delta\"])" + " print(c.choices[0].delta)" ] }, { @@ -233,7 +260,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 9, "id": "68f0214e", "metadata": {}, "outputs": [ @@ -241,14 +268,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'role': 'assistant', 'content': ' Hello'}\n", + "{'role': 'assistant', 'content': ''}\n", + "{'content': 'Hello'}\n", "{'content': '!'}\n", + "{'content': ' How'}\n", + "{'content': ' can'}\n", + "{'content': ' I'}\n", + "{'content': ' assist'}\n", + "{'content': ' you'}\n", + "{'content': ' today'}\n", + "{'content': '?'}\n", "{}\n" ] } ], "source": [ - "for c in lc_openai.ChatCompletion.create(\n", + "for c in lc_openai.chat.completions.create(\n", " messages=messages,\n", " model=\"claude-2\",\n", " temperature=0,\n", @@ -275,7 +310,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.11.5" } }, "nbformat": 4, diff --git a/libs/langchain/langchain/adapters/openai.py b/libs/langchain/langchain/adapters/openai.py index 8607468b81d23..0af759ebf5b08 100644 --- a/libs/langchain/langchain/adapters/openai.py +++ b/libs/langchain/langchain/adapters/openai.py @@ -25,6 +25,7 @@ SystemMessage, ToolMessage, ) +from langchain_core.pydantic_v1 import BaseModel from typing_extensions import Literal @@ -38,6 +39,29 @@ async def aenumerate( i += 1 +class IndexableBaseModel(BaseModel): + """Allows a BaseModel to return its fields by string variable indexing""" + + def __getitem__(self, item: str) -> Any: + return getattr(self, item) + + +class Choice(IndexableBaseModel): + message: dict + + +class ChatCompletions(IndexableBaseModel): + choices: List[Choice] + + +class ChoiceChunk(IndexableBaseModel): + delta: dict + + +class ChatCompletionChunk(IndexableBaseModel): + choices: List[ChoiceChunk] + + def convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage: """Convert a dictionary to a LangChain message. @@ -129,7 +153,7 @@ def convert_openai_messages(messages: Sequence[Dict[str, Any]]) -> List[BaseMess return [convert_dict_to_message(m) for m in messages] -def _convert_message_chunk_to_delta(chunk: BaseMessageChunk, i: int) -> Dict[str, Any]: +def _convert_message_chunk(chunk: BaseMessageChunk, i: int) -> dict: _dict: Dict[str, Any] = {} if isinstance(chunk, AIMessageChunk): if i == 0: @@ -148,6 +172,11 @@ def _convert_message_chunk_to_delta(chunk: BaseMessageChunk, i: int) -> Dict[str # This only happens at the end of streams, and OpenAI returns as empty dict if _dict == {"content": ""}: _dict = {} + return _dict + + +def _convert_message_chunk_to_delta(chunk: BaseMessageChunk, i: int) -> Dict[str, Any]: + _dict = _convert_message_chunk(chunk, i) return {"choices": [{"delta": _dict}]} @@ -262,3 +291,109 @@ def convert_messages_for_finetuning( for session in sessions if _has_assistant_message(session) ] + + +class Completions: + """Completion.""" + + @overload + @staticmethod + def create( + messages: Sequence[Dict[str, Any]], + *, + provider: str = "ChatOpenAI", + stream: Literal[False] = False, + **kwargs: Any, + ) -> ChatCompletions: + ... + + @overload + @staticmethod + def create( + messages: Sequence[Dict[str, Any]], + *, + provider: str = "ChatOpenAI", + stream: Literal[True], + **kwargs: Any, + ) -> Iterable: + ... + + @staticmethod + def create( + messages: Sequence[Dict[str, Any]], + *, + provider: str = "ChatOpenAI", + stream: bool = False, + **kwargs: Any, + ) -> Union[ChatCompletions, Iterable]: + models = importlib.import_module("langchain.chat_models") + model_cls = getattr(models, provider) + model_config = model_cls(**kwargs) + converted_messages = convert_openai_messages(messages) + if not stream: + result = model_config.invoke(converted_messages) + return ChatCompletions( + choices=[Choice(message=convert_message_to_dict(result))] + ) + else: + return ( + ChatCompletionChunk( + choices=[ChoiceChunk(delta=_convert_message_chunk(c, i))] + ) + for i, c in enumerate(model_config.stream(converted_messages)) + ) + + @overload + @staticmethod + async def acreate( + messages: Sequence[Dict[str, Any]], + *, + provider: str = "ChatOpenAI", + stream: Literal[False] = False, + **kwargs: Any, + ) -> ChatCompletions: + ... + + @overload + @staticmethod + async def acreate( + messages: Sequence[Dict[str, Any]], + *, + provider: str = "ChatOpenAI", + stream: Literal[True], + **kwargs: Any, + ) -> AsyncIterator: + ... + + @staticmethod + async def acreate( + messages: Sequence[Dict[str, Any]], + *, + provider: str = "ChatOpenAI", + stream: bool = False, + **kwargs: Any, + ) -> Union[ChatCompletions, AsyncIterator]: + models = importlib.import_module("langchain.chat_models") + model_cls = getattr(models, provider) + model_config = model_cls(**kwargs) + converted_messages = convert_openai_messages(messages) + if not stream: + result = await model_config.ainvoke(converted_messages) + return ChatCompletions( + choices=[Choice(message=convert_message_to_dict(result))] + ) + else: + return ( + ChatCompletionChunk( + choices=[ChoiceChunk(delta=_convert_message_chunk(c, i))] + ) + async for i, c in aenumerate(model_config.astream(converted_messages)) + ) + + +class Chat: + def __init__(self) -> None: + self.completions = Completions() + + +chat = Chat()