diff --git a/docs/docs/integrations/adapters/openai-old.ipynb b/docs/docs/integrations/adapters/openai-old.ipynb new file mode 100644 index 00000000000000..fee3ab5a501694 --- /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 8fd2c5214c767f..0db8c7dbfbccff 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 8607468b81d236..0af759ebf5b087 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()