diff --git a/docs/docs/integrations/chat/perplexity.ipynb b/docs/docs/integrations/chat/perplexity.ipynb new file mode 100644 index 00000000000..f8f66c8d0c7 --- /dev/null +++ b/docs/docs/integrations/chat/perplexity.ipynb @@ -0,0 +1,229 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "a016701c", + "metadata": {}, + "source": [ + "---\n", + "sidebar_label: Perplexity\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "bf733a38-db84-4363-89e2-de6735c37230", + "metadata": {}, + "source": [ + "# ChatPerplexity\n", + "\n", + "This notebook covers how to get started with Perplexity chat models." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d4a7c55d-b235-4ca4-a579-c90cc9570da9", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T11:25:00.590587Z", + "start_time": "2024-01-19T11:25:00.127293Z" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from langchain_community.chat_models import ChatPerplexity\n", + "from langchain_core.prompts import ChatPromptTemplate" + ] + }, + { + "cell_type": "markdown", + "id": "97a8ce3a", + "metadata": {}, + "source": [ + "The code provided assumes that your PPLX_API_KEY is set in your environment variables. If you would like to manually specify your API key and also choose a different model, you can use the following code:\n", + "\n", + "```python\n", + "chat = ChatPerplexity(temperature=0, pplx_api_key=\"YOUR_API_KEY\", model=\"pplx-70b-online\")\n", + "```\n", + "\n", + "You can check a list of available models [here](https://docs.perplexity.ai/docs/model-cards). For reproducibility, we can set the API key dynamically by taking it as an input in this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d3e49d78", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from getpass import getpass\n", + "\n", + "PPLX_API_KEY = getpass()\n", + "os.environ[\"PPLX_API_KEY\"] = PPLX_API_KEY" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "70cf04e8-423a-4ff6-8b09-f11fb711c817", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T11:25:04.349676Z", + "start_time": "2024-01-19T11:25:03.964930Z" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "chat = ChatPerplexity(temperature=0, model=\"pplx-70b-online\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8199ef8f-eb8b-4253-9ea0-6c24a013ca4c", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T11:25:07.274418Z", + "start_time": "2024-01-19T11:25:05.898031Z" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'The Higgs Boson is an elementary subatomic particle that plays a crucial role in the Standard Model of particle physics, which accounts for three of the four fundamental forces governing the behavior of our universe: the strong and weak nuclear forces, electromagnetism, and gravity. The Higgs Boson is important for several reasons:\\n\\n1. **Final Elementary Particle**: The Higgs Boson is the last elementary particle waiting to be discovered under the Standard Model. Its detection helps complete the Standard Model and further our understanding of the fundamental forces in the universe.\\n\\n2. **Mass Generation**: The Higgs Boson is responsible for giving mass to other particles, a process that occurs through its interaction with the Higgs field. This mass generation is essential for the formation of atoms, molecules, and the visible matter we observe in the universe.\\n\\n3. **Implications for New Physics**: While the detection of the Higgs Boson has confirmed many aspects of the Standard Model, it also opens up new possibilities for discoveries beyond the Standard Model. Further research on the Higgs Boson could reveal insights into the nature of dark matter, supersymmetry, and other exotic phenomena.\\n\\n4. **Advancements in Technology**: The search for the Higgs Boson has led to significant advancements in technology, such as the development of artificial intelligence and machine learning algorithms used in particle accelerators like the Large Hadron Collider (LHC). These advancements have not only contributed to the discovery of the Higgs Boson but also have potential applications in various other fields.\\n\\nIn summary, the Higgs Boson is important because it completes the Standard Model, plays a crucial role in mass generation, hints at new physics phenomena beyond the Standard Model, and drives advancements in technology.\\n'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "system = \"You are a helpful assistant.\"\n", + "human = \"{input}\"\n", + "prompt = ChatPromptTemplate.from_messages([(\"system\", system), (\"human\", human)])\n", + "\n", + "chain = prompt | chat\n", + "response = chain.invoke({\"input\": \"Why is the Higgs Boson important?\"})\n", + "response.content" + ] + }, + { + "cell_type": "markdown", + "id": "de6d8d5a", + "metadata": {}, + "source": [ + "You can format and structure the prompts like you would typically. In the following example, we ask the model to tell us a joke about cats." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c5fac0e9-05a4-4fc1-a3b3-e5bbb24b971b", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T11:25:10.448733Z", + "start_time": "2024-01-19T11:25:08.866277Z" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Here\\'s a joke about cats:\\n\\nWhy did the cat want math lessons from a mermaid?\\n\\nBecause it couldn\\'t find its \"core purpose\" in life!\\n\\nRemember, cats are unique and fascinating creatures, and each one has its own special traits and abilities. While some may see them as mysterious or even a bit aloof, they are still beloved pets that bring joy and companionship to their owners. So, if your cat ever seeks guidance from a mermaid, just remember that they are on their own journey to self-discovery!\\n'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chat = ChatPerplexity(temperature=0, model=\"pplx-70b-online\")\n", + "prompt = ChatPromptTemplate.from_messages([(\"human\", \"Tell me a joke about {topic}\")])\n", + "chain = prompt | chat\n", + "response = chain.invoke({\"topic\": \"cats\"})\n", + "response.content" + ] + }, + { + "cell_type": "markdown", + "id": "13d93dc4", + "metadata": {}, + "source": [ + "## `ChatPerplexity` also supports streaming functionality:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "025be980-e50d-4a68-93dc-c9c7b500ce34", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-19T11:25:24.438696Z", + "start_time": "2024-01-19T11:25:14.687480Z" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Here is a list of some famous tourist attractions in Pakistan:\n", + "\n", + "1. **Minar-e-Pakistan**: A 62-meter high minaret in Lahore that represents the history of Pakistan.\n", + "2. **Badshahi Mosque**: A historic mosque in Lahore with a capacity of 10,000 worshippers.\n", + "3. **Shalimar Gardens**: A beautiful garden in Lahore with landscaped grounds and a series of cascading pools.\n", + "4. **Pakistan Monument**: A national monument in Islamabad representing the four provinces and three districts of Pakistan.\n", + "5. **National Museum of Pakistan**: A museum in Karachi showcasing the country's cultural history.\n", + "6. **Faisal Mosque**: A large mosque in Islamabad that can accommodate up to 300,000 worshippers.\n", + "7. **Clifton Beach**: A popular beach in Karachi offering water activities and recreational facilities.\n", + "8. **Kartarpur Corridor**: A visa-free border crossing and religious corridor connecting Gurdwara Darbar Sahib in Pakistan to Gurudwara Sri Kartarpur Sahib in India.\n", + "9. **Mohenjo-daro**: An ancient Indus Valley civilization site in Sindh, Pakistan, dating back to around 2500 BCE.\n", + "10. **Hunza Valley**: A picturesque valley in Gilgit-Baltistan known for its stunning mountain scenery and unique culture.\n", + "\n", + "These attractions showcase the rich history, diverse culture, and natural beauty of Pakistan, making them popular destinations for both local and international tourists.\n" + ] + } + ], + "source": [ + "chat = ChatPerplexity(temperature=0.7, model=\"pplx-70b-online\")\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [(\"human\", \"Give me a list of famous tourist attractions in Pakistan\")]\n", + ")\n", + "chain = prompt | chat\n", + "for chunk in chain.stream({}):\n", + " print(chunk.content, end=\"\", flush=True)" + ] + } + ], + "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.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/libs/community/langchain_community/chat_models/__init__.py b/libs/community/langchain_community/chat_models/__init__.py index 00f7ffeb40a..ba76701ed5e 100644 --- a/libs/community/langchain_community/chat_models/__init__.py +++ b/libs/community/langchain_community/chat_models/__init__.py @@ -49,6 +49,7 @@ from langchain_community.chat_models.ollama import ChatOllama from langchain_community.chat_models.openai import ChatOpenAI from langchain_community.chat_models.pai_eas_endpoint import PaiEasChatEndpoint +from langchain_community.chat_models.perplexity import ChatPerplexity from langchain_community.chat_models.promptlayer_openai import PromptLayerChatOpenAI from langchain_community.chat_models.sparkllm import ChatSparkLLM from langchain_community.chat_models.tongyi import ChatTongyi @@ -98,5 +99,6 @@ "GPTRouter", "ChatYuan2", "ChatZhipuAI", + "ChatPerplexity", "ChatKinetica", ] diff --git a/libs/community/langchain_community/chat_models/perplexity.py b/libs/community/langchain_community/chat_models/perplexity.py new file mode 100644 index 00000000000..479b3fd4780 --- /dev/null +++ b/libs/community/langchain_community/chat_models/perplexity.py @@ -0,0 +1,271 @@ +"""Wrapper around Perplexity APIs.""" +from __future__ import annotations + +import logging +from typing import ( + Any, + Dict, + Iterator, + List, + Mapping, + Optional, + Tuple, + Type, + Union, +) + +from langchain_core.callbacks import CallbackManagerForLLMRun +from langchain_core.language_models.chat_models import ( + BaseChatModel, + generate_from_stream, +) +from langchain_core.messages import ( + AIMessage, + AIMessageChunk, + BaseMessage, + BaseMessageChunk, + ChatMessage, + ChatMessageChunk, + FunctionMessageChunk, + HumanMessage, + HumanMessageChunk, + SystemMessage, + SystemMessageChunk, + ToolMessageChunk, +) +from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult +from langchain_core.pydantic_v1 import Field, root_validator +from langchain_core.utils import get_from_dict_or_env, get_pydantic_field_names + +logger = logging.getLogger(__name__) + + +class ChatPerplexity(BaseChatModel): + """`Perplexity AI` Chat models API. + + To use, you should have the ``openai`` python package installed, and the + environment variable ``PPLX_API_KEY`` set to your API key. + Any parameters that are valid to be passed to the openai.create call can be passed + in, even if not explicitly saved on this class. + + Example: + .. code-block:: python + + from langchain_community.chat_models import ChatPerplexity + + chat = ChatPerplexity(model="pplx-70b-online", temperature=0.7) + """ + + client: Any #: :meta private: + model: str = "pplx-70b-online" + """Model name.""" + temperature: float = 0.7 + """What sampling temperature to use.""" + model_kwargs: Dict[str, Any] = Field(default_factory=dict) + """Holds any model parameters valid for `create` call not explicitly specified.""" + pplx_api_key: Optional[str] = None + """Base URL path for API requests, + leave blank if not using a proxy or service emulator.""" + request_timeout: Optional[Union[float, Tuple[float, float]]] = None + """Timeout for requests to PerplexityChat completion API. Default is 600 seconds.""" + max_retries: int = 6 + """Maximum number of retries to make when generating.""" + streaming: bool = False + """Whether to stream the results or not.""" + max_tokens: Optional[int] = None + """Maximum number of tokens to generate.""" + + class Config: + """Configuration for this pydantic object.""" + + allow_population_by_field_name = True + + @property + def lc_secrets(self) -> Dict[str, str]: + return {"pplx_api_key": "PPLX_API_KEY"} + + @root_validator(pre=True, allow_reuse=True) + def build_extra(cls, values: Dict[str, Any]) -> Dict[str, Any]: + """Build extra kwargs from additional params that were passed in.""" + all_required_field_names = get_pydantic_field_names(cls) + extra = values.get("model_kwargs", {}) + for field_name in list(values): + if field_name in extra: + raise ValueError(f"Found {field_name} supplied twice.") + if field_name not in all_required_field_names: + logger.warning( + f"""WARNING! {field_name} is not a default parameter. + {field_name} was transferred to model_kwargs. + Please confirm that {field_name} is what you intended.""" + ) + extra[field_name] = values.pop(field_name) + + invalid_model_kwargs = all_required_field_names.intersection(extra.keys()) + if invalid_model_kwargs: + raise ValueError( + f"Parameters {invalid_model_kwargs} should be specified explicitly. " + f"Instead they were passed in as part of `model_kwargs` parameter." + ) + + values["model_kwargs"] = extra + return values + + @root_validator(allow_reuse=True) + def validate_environment(cls, values: Dict) -> Dict: + """Validate that api key and python package exists in environment.""" + values["pplx_api_key"] = get_from_dict_or_env( + values, "pplx_api_key", "PPLX_API_KEY" + ) + try: + import openai # noqa: F401 + except ImportError: + raise ImportError( + "Could not import openai python package. " + "Please install it with `pip install openai`." + ) + try: + values["client"] = openai.OpenAI( + api_key=values["pplx_api_key"], base_url="https://api.perplexity.ai" + ) + except AttributeError: + raise ValueError( + "`openai` has no `ChatCompletion` attribute, this is likely " + "due to an old version of the openai package. Try upgrading it " + "with `pip install --upgrade openai`." + ) + return values + + @property + def _default_params(self) -> Dict[str, Any]: + """Get the default parameters for calling PerplexityChat API.""" + return { + "request_timeout": self.request_timeout, + "max_tokens": self.max_tokens, + "stream": self.streaming, + "temperature": self.temperature, + **self.model_kwargs, + } + + def _convert_message_to_dict(self, message: BaseMessage) -> Dict[str, Any]: + if isinstance(message, ChatMessage): + message_dict = {"role": message.role, "content": message.content} + elif isinstance(message, SystemMessage): + message_dict = {"role": "system", "content": message.content} + elif isinstance(message, HumanMessage): + message_dict = {"role": "user", "content": message.content} + elif isinstance(message, AIMessage): + message_dict = {"role": "assistant", "content": message.content} + else: + raise TypeError(f"Got unknown type {message}") + return message_dict + + def _create_message_dicts( + self, messages: List[BaseMessage], stop: Optional[List[str]] + ) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]: + params = dict(self._invocation_params) + if stop is not None: + if "stop" in params: + raise ValueError("`stop` found in both the input and default params.") + params["stop"] = stop + message_dicts = [self._convert_message_to_dict(m) for m in messages] + return message_dicts, params + + def _convert_delta_to_message_chunk( + self, _dict: Mapping[str, Any], default_class: Type[BaseMessageChunk] + ) -> BaseMessageChunk: + role = _dict.get("role") + content = _dict.get("content") or "" + additional_kwargs: Dict = {} + if _dict.get("function_call"): + function_call = dict(_dict["function_call"]) + if "name" in function_call and function_call["name"] is None: + function_call["name"] = "" + additional_kwargs["function_call"] = function_call + if _dict.get("tool_calls"): + additional_kwargs["tool_calls"] = _dict["tool_calls"] + + if role == "user" or default_class == HumanMessageChunk: + return HumanMessageChunk(content=content) + elif role == "assistant" or default_class == AIMessageChunk: + return AIMessageChunk(content=content, additional_kwargs=additional_kwargs) + elif role == "system" or default_class == SystemMessageChunk: + return SystemMessageChunk(content=content) + elif role == "function" or default_class == FunctionMessageChunk: + return FunctionMessageChunk(content=content, name=_dict["name"]) + elif role == "tool" or default_class == ToolMessageChunk: + return ToolMessageChunk(content=content, tool_call_id=_dict["tool_call_id"]) + elif role or default_class == ChatMessageChunk: + return ChatMessageChunk(content=content, role=role) + else: + return default_class(content=content) + + def _stream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + message_dicts, params = self._create_message_dicts(messages, stop) + params = {**params, **kwargs} + default_chunk_class = AIMessageChunk + + if stop: + params["stop_sequences"] = stop + stream_resp = self.client.chat.completions.create( + model=params["model"], messages=message_dicts, stream=True + ) + for chunk in stream_resp: + if not isinstance(chunk, dict): + chunk = chunk.dict() + if len(chunk["choices"]) == 0: + continue + choice = chunk["choices"][0] + chunk = self._convert_delta_to_message_chunk( + choice["delta"], default_chunk_class + ) + finish_reason = choice.get("finish_reason") + generation_info = ( + dict(finish_reason=finish_reason) if finish_reason is not None else None + ) + default_chunk_class = chunk.__class__ + chunk = ChatGenerationChunk(message=chunk, generation_info=generation_info) + yield chunk + if run_manager: + run_manager.on_llm_new_token(chunk.text, chunk=chunk) + + def _generate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> ChatResult: + if self.streaming: + stream_iter = self._stream( + messages, stop=stop, run_manager=run_manager, **kwargs + ) + if stream_iter: + return generate_from_stream(stream_iter) + message_dicts, params = self._create_message_dicts(messages, stop) + params = {**params, **kwargs} + response = self.client.chat.completions.create( + model=params["model"], messages=message_dicts + ) + message = AIMessage(content=response.choices[0].message.content) + return ChatResult(generations=[ChatGeneration(message=message)]) + + @property + def _invocation_params(self) -> Mapping[str, Any]: + """Get the parameters used to invoke the model.""" + pplx_creds: Dict[str, Any] = { + "api_key": self.pplx_api_key, + "api_base": "https://api.perplexity.ai", + "model": self.model, + } + return {**pplx_creds, **self._default_params} + + @property + def _llm_type(self) -> str: + """Return type of chat model.""" + return "perplexitychat" diff --git a/libs/community/tests/unit_tests/chat_models/test_imports.py b/libs/community/tests/unit_tests/chat_models/test_imports.py index f851a6348d7..5f6bf5494cd 100644 --- a/libs/community/tests/unit_tests/chat_models/test_imports.py +++ b/libs/community/tests/unit_tests/chat_models/test_imports.py @@ -40,6 +40,7 @@ "GPTRouter", "ChatYuan2", "ChatZhipuAI", + "ChatPerplexity", "ChatKinetica", ] diff --git a/libs/community/tests/unit_tests/chat_models/test_perplexity.py b/libs/community/tests/unit_tests/chat_models/test_perplexity.py new file mode 100644 index 00000000000..071f1340dd5 --- /dev/null +++ b/libs/community/tests/unit_tests/chat_models/test_perplexity.py @@ -0,0 +1,30 @@ +"""Test Perplexity Chat API wrapper.""" +import os + +import pytest + +from langchain_community.chat_models import ChatPerplexity + +os.environ["PPLX_API_KEY"] = "foo" + + +@pytest.mark.requires("openai") +def test_perplexity_model_name_param() -> None: + llm = ChatPerplexity(model="foo") + assert llm.model == "foo" + + +@pytest.mark.requires("openai") +def test_perplexity_model_kwargs() -> None: + llm = ChatPerplexity(model="test", model_kwargs={"foo": "bar"}) + assert llm.model_kwargs == {"foo": "bar"} + + +@pytest.mark.requires("openai") +def test_perplexity_initialization() -> None: + """Test perplexity initialization.""" + # Verify that chat perplexity can be initialized using a secret key provided + # as a parameter rather than an environment variable. + ChatPerplexity( + model="test", perplexity_api_key="test", temperature=0.7, verbose=True + )