diff --git a/libs/community/extended_testing_deps.txt b/libs/community/extended_testing_deps.txt index bbbff511b34..6879b6a184d 100644 --- a/libs/community/extended_testing_deps.txt +++ b/libs/community/extended_testing_deps.txt @@ -72,6 +72,7 @@ rspace_client>=2.5.0,<3 scikit-learn>=1.2.2,<2 simsimd>=4.3.1,<5 sqlite-vss>=0.1.2,<0.2 +sseclient-py>=1.8.0,<2 streamlit>=1.18.0,<2 sympy>=1.12,<2 telethon>=1.28.5,<2 diff --git a/libs/community/langchain_community/llms/__init__.py b/libs/community/langchain_community/llms/__init__.py index a303cfd1b5e..cf45f9b4d63 100644 --- a/libs/community/langchain_community/llms/__init__.py +++ b/libs/community/langchain_community/llms/__init__.py @@ -640,6 +640,12 @@ def _import_yuan2() -> Type[BaseLLM]: return Yuan2 +def _import_you() -> Type[BaseLLM]: + from langchain_community.llms.you import You + + return You + + def _import_volcengine_maas() -> Type[BaseLLM]: from langchain_community.llms.volcengine_maas import VolcEngineMaasLLM @@ -847,6 +853,8 @@ def __getattr__(name: str) -> Any: return _import_yandex_gpt() elif name == "Yuan2": return _import_yuan2() + elif name == "You": + return _import_you() elif name == "VolcEngineMaasLLM": return _import_volcengine_maas() elif name == "type_to_cls_dict": @@ -959,6 +967,7 @@ __all__ = [ "Writer", "Xinference", "YandexGPT", + "You", "Yuan2", ] @@ -1056,6 +1065,7 @@ def get_type_to_cls_dict() -> Dict[str, Callable[[], Type[BaseLLM]]]: "qianfan_endpoint": _import_baidu_qianfan_endpoint, "yandex_gpt": _import_yandex_gpt, "yuan2": _import_yuan2, + "you": _import_you, "VolcEngineMaasLLM": _import_volcengine_maas, "SparkLLM": _import_sparkllm, } diff --git a/libs/community/langchain_community/llms/you.py b/libs/community/langchain_community/llms/you.py new file mode 100644 index 00000000000..54c1c31bfec --- /dev/null +++ b/libs/community/langchain_community/llms/you.py @@ -0,0 +1,140 @@ +import os +from typing import Any, Dict, Generator, Iterator, List, Literal, Optional + +import requests +from langchain_core.callbacks.manager import CallbackManagerForLLMRun +from langchain_core.language_models.llms import LLM +from langchain_core.outputs import GenerationChunk +from langchain_core.pydantic_v1 import Field + +SMART_ENDPOINT = "https://chat-api.you.com/smart" +RESEARCH_ENDPOINT = "https://chat-api.you.com/research" + + +def _request(base_url: str, api_key: str, **kwargs: Any) -> Dict[str, Any]: + """ + NOTE: This function can be replaced by a OpenAPI-generated Python SDK in the future, + for better input/output typing support. + """ + headers = {"x-api-key": api_key} + response = requests.post(base_url, headers=headers, json=kwargs) + response.raise_for_status() + return response.json() + + +def _request_stream( + base_url: str, api_key: str, **kwargs: Any +) -> Generator[str, None, None]: + headers = {"x-api-key": api_key} + params = dict(**kwargs, stream=True) + response = requests.post(base_url, headers=headers, stream=True, json=params) + response.raise_for_status() + + # Explicitly coercing the response to a generator to satisfy mypy + event_source = (bytestring for bytestring in response) + + try: + import sseclient + + client = sseclient.SSEClient(event_source) + except ImportError: + raise ImportError( + ( + "Could not import `sseclient`. " + "Please install it with `pip install sseclient-py`." + ) + ) + + for event in client.events(): + if event.event in ("search_results", "done"): + pass + elif event.event == "token": + yield event.data + elif event.event == "error": + raise ValueError(f"Error in response: {event.data}") + else: + raise NotImplementedError(f"Unknown event type {event.event}") + + +class You(LLM): + """Wrapper around You.com's conversational Smart and Research APIs. + + Each API endpoint is designed to generate conversational + responses to a variety of query types, including inline citations + and web results when relevant. + + Smart Endpoint: + - Quick, reliable answers for a variety of questions + - Cites the entire web page URL + + Research Endpoint: + - In-depth answers with extensive citations for a variety of questions + - Cites the specific web page snippet relevant to the claim + + To connect to the You.com api requires an API key which + you can get at https://api.you.com. + + For more information, check out the documentations at + https://documentation.you.com/api-reference/. + + Args: + endpoint: You.com conversational endpoints. Choose from "smart" or "research" + ydc_api_key: You.com API key, if `YDC_API_KEY` is not set in the environment + """ + + endpoint: Literal["smart", "research"] = Field( + "smart", + description=( + 'You.com conversational endpoints. Choose from "smart" or "research"' + ), + ) + ydc_api_key: Optional[str] = Field( + None, + description="You.com API key, if `YDC_API_KEY` is not set in the envrioment", + ) + + def _call( + self, + prompt: str, + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> str: + if stop: + raise NotImplementedError( + "Stop words are not implemented for You.com endpoints." + ) + params = {"query": prompt} + response = _request(self._request_endpoint, api_key=self._api_key, **params) + return response["answer"] + + def _stream( + self, + prompt: str, + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Iterator[GenerationChunk]: + if stop: + raise NotImplementedError( + "Stop words are not implemented for You.com endpoints." + ) + params = {"query": prompt} + for token in _request_stream( + self._request_endpoint, api_key=self._api_key, **params + ): + yield GenerationChunk(text=token) + + @property + def _request_endpoint(self) -> str: + if self.endpoint == "smart": + return SMART_ENDPOINT + return RESEARCH_ENDPOINT + + @property + def _api_key(self) -> str: + return self.ydc_api_key or os.environ["YDC_API_KEY"] + + @property + def _llm_type(self) -> str: + return "you.com" diff --git a/libs/community/tests/unit_tests/llms/test_imports.py b/libs/community/tests/unit_tests/llms/test_imports.py index c0eddec93d6..d6011aebd12 100644 --- a/libs/community/tests/unit_tests/llms/test_imports.py +++ b/libs/community/tests/unit_tests/llms/test_imports.py @@ -98,6 +98,7 @@ EXPECT_ALL = [ "QianfanLLMEndpoint", "YandexGPT", "Yuan2", + "You", "VolcEngineMaasLLM", "WatsonxLLM", "SparkLLM", diff --git a/libs/community/tests/unit_tests/llms/test_you.py b/libs/community/tests/unit_tests/llms/test_you.py new file mode 100644 index 00000000000..98fb040b685 --- /dev/null +++ b/libs/community/tests/unit_tests/llms/test_you.py @@ -0,0 +1,37 @@ +import pytest +import requests_mock + + +@pytest.mark.parametrize("endpoint", ("smart", "research")) +@pytest.mark.requires("sseclient") +def test_invoke( + endpoint: str, requests_mock: requests_mock.Mocker, monkeypatch: pytest.MonkeyPatch +) -> None: + from langchain_community.llms import You + from langchain_community.llms.you import RESEARCH_ENDPOINT, SMART_ENDPOINT + + json = { + "answer": ( + "A solar eclipse occurs when the Moon passes between the Sun and Earth, " + "casting a shadow on Earth and ..." + ), + "search_results": [ + { + "url": "https://en.wikipedia.org/wiki/Solar_eclipse", + "name": "Solar eclipse - Wikipedia", + "snippet": ( + "A solar eclipse occurs when the Moon passes " + "between Earth and the Sun, thereby obscuring the view of the Sun " + "from a small part of Earth, totally or partially. " + ), + } + ], + } + request_endpoint = SMART_ENDPOINT if endpoint == "smart" else RESEARCH_ENDPOINT + requests_mock.post(request_endpoint, json=json) + + monkeypatch.setenv("YDC_API_KEY", "...") + + llm = You(endpoint=endpoint) + output = llm.invoke("What is a solar eclipse?") + assert output == json["answer"]