From 8145c79fd80f2fd27265a28ecd5fd2d33d269768 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Fri, 2 Dec 2022 07:27:36 -0800 Subject: [PATCH 1/5] bump version to 0.0.27 (#244) --- langchain/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langchain/VERSION b/langchain/VERSION index c4475d3b..24ff8558 100644 --- a/langchain/VERSION +++ b/langchain/VERSION @@ -1 +1 @@ -0.0.26 +0.0.27 From 024c3e1dbe5006b069d2aefc98a57296130a9128 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Fri, 2 Dec 2022 09:07:21 -0800 Subject: [PATCH 2/5] add react text world doc (#245) --- docs/explanation/cool_demos.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/explanation/cool_demos.md b/docs/explanation/cool_demos.md index 47758aee..27381633 100644 --- a/docs/explanation/cool_demos.md +++ b/docs/explanation/cool_demos.md @@ -19,6 +19,9 @@ GPT Index is a project consisting of a set of data structures that are created u ### [Grover's Algorithm](https://github.com/JavaFXpert/llm-grovers-search-party) Leveraging Qiskit, OpenAI and LangChain to demonstrate Grover's algorithm +### [ReAct TextWorld](https://colab.research.google.com/drive/19WTIWC3prw5LDMHmRMvqNV2loD9FHls6?usp=sharing) +Leveraging the ReActTextWorldAgent to play TextWorld with an LLM! + ## Not Open Source From c897bd6cbd9f3422ed42937a4a501e49f6e0288d Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Fri, 2 Dec 2022 13:39:36 -0800 Subject: [PATCH 3/5] api chain (#246) Co-authored-by: Subhash Ramesh <33400216+thecooltechguy@users.noreply.github.com> --- langchain/chains/api/__init__.py | 1 + langchain/chains/api/base.py | 101 ++++++++++++++++++++++++++++ langchain/chains/api/prompt.py | 35 ++++++++++ tests/unit_tests/chains/test_api.py | 84 +++++++++++++++++++++++ 4 files changed, 221 insertions(+) create mode 100644 langchain/chains/api/__init__.py create mode 100644 langchain/chains/api/base.py create mode 100644 langchain/chains/api/prompt.py create mode 100644 tests/unit_tests/chains/test_api.py diff --git a/langchain/chains/api/__init__.py b/langchain/chains/api/__init__.py new file mode 100644 index 00000000..efe2fb36 --- /dev/null +++ b/langchain/chains/api/__init__.py @@ -0,0 +1 @@ +"""Chain that makes API calls and summarizes the responses to answer a question.""" diff --git a/langchain/chains/api/base.py b/langchain/chains/api/base.py new file mode 100644 index 00000000..f85c9b0a --- /dev/null +++ b/langchain/chains/api/base.py @@ -0,0 +1,101 @@ +"""Chain that makes API calls and summarizes the responses to answer a question.""" +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +import requests +from pydantic import BaseModel, root_validator + +from langchain import LLMChain +from langchain.chains.api.prompt import API_RESPONSE_PROMPT, API_URL_PROMPT +from langchain.chains.base import Chain +from langchain.llms.base import LLM + + +class RequestsWrapper(BaseModel): + """Lightweight wrapper to partial out everything except the url to hit.""" + + headers: Optional[dict] = None + + def run(self, url: str) -> str: + """Hit the URL and return the text.""" + return requests.get(url, headers=self.headers).text + + +class APIChain(Chain, BaseModel): + """Chain that makes API calls and summarizes the responses to answer a question.""" + + api_request_chain: LLMChain + api_answer_chain: LLMChain + requests_chain: RequestsWrapper + api_docs: str + question_key: str = "question" #: :meta private: + output_key: str = "output" #: :meta private: + + @property + def input_keys(self) -> List[str]: + """Expect input key. + + :meta private: + """ + return [self.question_key] + + @property + def output_keys(self) -> List[str]: + """Expect output key. + + :meta private: + """ + return [self.output_key] + + @root_validator(pre=True) + def validate_api_request_prompt(cls, values: Dict) -> Dict: + """Check that api request prompt expects the right variables.""" + input_vars = values["api_request_chain"].prompt.input_variables + expected_vars = {"question", "api_docs"} + if set(input_vars) != expected_vars: + raise ValueError( + f"Input variables should be {expected_vars}, got {input_vars}" + ) + return values + + @root_validator(pre=True) + def validate_api_answer_prompt(cls, values: Dict) -> Dict: + """Check that api answer prompt expects the right variables.""" + input_vars = values["api_answer_chain"].prompt.input_variables + expected_vars = {"question", "api_docs", "api_url", "api_response"} + if set(input_vars) != expected_vars: + raise ValueError( + f"Input variables should be {expected_vars}, got {input_vars}" + ) + return values + + def _call(self, inputs: Dict[str, str]) -> Dict[str, str]: + question = inputs[self.question_key] + api_url = self.api_request_chain.predict( + question=question, api_docs=self.api_docs + ) + api_response = self.requests_chain.run(api_url) + answer = self.api_answer_chain.predict( + question=question, + api_docs=self.api_docs, + api_url=api_url, + api_response=api_response, + ) + return {self.output_key: answer} + + @classmethod + def from_llm_and_api_docs( + cls, llm: LLM, api_docs: str, headers: Optional[dict] = None, **kwargs: Any + ) -> APIChain: + """Load chain from just an LLM and the api docs.""" + get_request_chain = LLMChain(llm=llm, prompt=API_URL_PROMPT) + requests_wrapper = RequestsWrapper(headers=headers) + get_answer_chain = LLMChain(llm=llm, prompt=API_RESPONSE_PROMPT) + return cls( + request_chain=get_request_chain, + answer_chain=get_answer_chain, + requests_wrapper=requests_wrapper, + api_docs=api_docs, + **kwargs, + ) diff --git a/langchain/chains/api/prompt.py b/langchain/chains/api/prompt.py new file mode 100644 index 00000000..4f2c18dd --- /dev/null +++ b/langchain/chains/api/prompt.py @@ -0,0 +1,35 @@ +# flake8: noqa +from langchain.prompts.prompt import PromptTemplate + +API_URL_PROMPT_TEMPLATE = """You are given the below API Documentation: + +{api_docs} + +Using this documentation, generate the full API url to call for answering this question: {question} + +API url: """ +API_URL_PROMPT = PromptTemplate( + input_variables=[ + "api_docs", + "question", + ], + template=API_URL_PROMPT_TEMPLATE, +) + +API_RESPONSE_PROMPT_TEMPLATE = ( + API_URL_PROMPT_TEMPLATE + + """ {api_url} + +Here is the response from the API: + +{api_response} + +Summarize this response to answer the original question. + +Summary:""" +) + +API_RESPONSE_PROMPT = PromptTemplate( + input_variables=["api_docs", "question", "api_url", "api_response"], + template=API_RESPONSE_PROMPT_TEMPLATE, +) diff --git a/tests/unit_tests/chains/test_api.py b/tests/unit_tests/chains/test_api.py new file mode 100644 index 00000000..e64b9e10 --- /dev/null +++ b/tests/unit_tests/chains/test_api.py @@ -0,0 +1,84 @@ +"""Test LLM Math functionality.""" + +import json + +import pytest + +from langchain import LLMChain +from langchain.chains.api.base import APIChain, RequestsWrapper +from langchain.chains.api.prompt import API_RESPONSE_PROMPT, API_URL_PROMPT +from tests.unit_tests.llms.fake_llm import FakeLLM + + +class FakeRequestsChain(RequestsWrapper): + """Fake requests chain just for testing purposes.""" + + output: str + + def run(self, url: str) -> str: + """Just return the specified output.""" + return self.output + + +@pytest.fixture +def test_api_data() -> dict: + """Fake api data to use for testing.""" + api_docs = """ + This API endpoint will search the notes for a user. + + Endpoint: https://thisapidoesntexist.com + GET /api/notes + + Query parameters: + q | string | The search term for notes + """ + return { + "api_docs": api_docs, + "question": "Search for notes containing langchain", + "api_url": "https://thisapidoesntexist.com/api/notes?q=langchain", + "api_response": json.dumps( + { + "success": True, + "results": [{"id": 1, "content": "Langchain is awesome!"}], + } + ), + "api_summary": "There is 1 note about langchain.", + } + + +@pytest.fixture +def fake_llm_api_chain(test_api_data: dict) -> APIChain: + """Fake LLM API chain for testing.""" + TEST_API_DOCS = test_api_data["api_docs"] + TEST_QUESTION = test_api_data["question"] + TEST_URL = test_api_data["api_url"] + TEST_API_RESPONSE = test_api_data["api_response"] + TEST_API_SUMMARY = test_api_data["api_summary"] + + api_url_query_prompt = API_URL_PROMPT.format( + api_docs=TEST_API_DOCS, question=TEST_QUESTION + ) + api_response_prompt = API_RESPONSE_PROMPT.format( + api_docs=TEST_API_DOCS, + question=TEST_QUESTION, + api_url=TEST_URL, + api_response=TEST_API_RESPONSE, + ) + queries = {api_url_query_prompt: TEST_URL, api_response_prompt: TEST_API_SUMMARY} + fake_llm = FakeLLM(queries=queries) + api_request_chain = LLMChain(llm=fake_llm, prompt=API_URL_PROMPT) + api_answer_chain = LLMChain(llm=fake_llm, prompt=API_RESPONSE_PROMPT) + requests_chain = FakeRequestsChain(output=TEST_API_RESPONSE) + return APIChain( + api_request_chain=api_request_chain, + api_answer_chain=api_answer_chain, + requests_chain=requests_chain, + api_docs=TEST_API_DOCS, + ) + + +def test_api_question(fake_llm_api_chain: APIChain, test_api_data: dict) -> None: + """Test simple question that needs API access.""" + question = test_api_data["question"] + output = fake_llm_api_chain.run(question) + assert output == test_api_data["api_summary"] From a9ce04201f12656452e1ab57cb07660ae9933288 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Fri, 2 Dec 2022 15:44:10 -0800 Subject: [PATCH 4/5] Harrison/improve usability of api chain (#247) improve usability of api chain --- langchain/chains/__init__.py | 2 ++ langchain/chains/api/base.py | 15 ++++++++++----- tests/unit_tests/chains/test_api.py | 4 ++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/langchain/chains/__init__.py b/langchain/chains/__init__.py index bc8e42d4..d63839bc 100644 --- a/langchain/chains/__init__.py +++ b/langchain/chains/__init__.py @@ -1,4 +1,5 @@ """Chains are easily reusable components which can be linked together.""" +from langchain.chains.api.base import APIChain from langchain.chains.conversation.base import ConversationChain from langchain.chains.llm import LLMChain from langchain.chains.llm_math.base import LLMMathChain @@ -22,4 +23,5 @@ __all__ = [ "QAWithSourcesChain", "VectorDBQAWithSourcesChain", "PALChain", + "APIChain", ] diff --git a/langchain/chains/api/base.py b/langchain/chains/api/base.py index f85c9b0a..b73c96bb 100644 --- a/langchain/chains/api/base.py +++ b/langchain/chains/api/base.py @@ -6,9 +6,10 @@ from typing import Any, Dict, List, Optional import requests from pydantic import BaseModel, root_validator -from langchain import LLMChain from langchain.chains.api.prompt import API_RESPONSE_PROMPT, API_URL_PROMPT from langchain.chains.base import Chain +from langchain.chains.llm import LLMChain +from langchain.input import print_text from langchain.llms.base import LLM @@ -27,7 +28,7 @@ class APIChain(Chain, BaseModel): api_request_chain: LLMChain api_answer_chain: LLMChain - requests_chain: RequestsWrapper + requests_wrapper: RequestsWrapper api_docs: str question_key: str = "question" #: :meta private: output_key: str = "output" #: :meta private: @@ -75,7 +76,11 @@ class APIChain(Chain, BaseModel): api_url = self.api_request_chain.predict( question=question, api_docs=self.api_docs ) - api_response = self.requests_chain.run(api_url) + if self.verbose: + print_text(api_url, color="green", end="\n") + api_response = self.requests_wrapper.run(api_url) + if self.verbose: + print_text(api_url, color="yellow", end="\n") answer = self.api_answer_chain.predict( question=question, api_docs=self.api_docs, @@ -93,8 +98,8 @@ class APIChain(Chain, BaseModel): requests_wrapper = RequestsWrapper(headers=headers) get_answer_chain = LLMChain(llm=llm, prompt=API_RESPONSE_PROMPT) return cls( - request_chain=get_request_chain, - answer_chain=get_answer_chain, + api_request_chain=get_request_chain, + api_answer_chain=get_answer_chain, requests_wrapper=requests_wrapper, api_docs=api_docs, **kwargs, diff --git a/tests/unit_tests/chains/test_api.py b/tests/unit_tests/chains/test_api.py index e64b9e10..9915ef22 100644 --- a/tests/unit_tests/chains/test_api.py +++ b/tests/unit_tests/chains/test_api.py @@ -68,11 +68,11 @@ def fake_llm_api_chain(test_api_data: dict) -> APIChain: fake_llm = FakeLLM(queries=queries) api_request_chain = LLMChain(llm=fake_llm, prompt=API_URL_PROMPT) api_answer_chain = LLMChain(llm=fake_llm, prompt=API_RESPONSE_PROMPT) - requests_chain = FakeRequestsChain(output=TEST_API_RESPONSE) + requests_wrapper = FakeRequestsChain(output=TEST_API_RESPONSE) return APIChain( api_request_chain=api_request_chain, api_answer_chain=api_answer_chain, - requests_chain=requests_chain, + requests_wrapper=requests_wrapper, api_docs=TEST_API_DOCS, ) From b4762dfff05dd6958c53c5c4e81b76250389d897 Mon Sep 17 00:00:00 2001 From: Scott Leibrand Date: Sat, 3 Dec 2022 08:11:38 -0800 Subject: [PATCH 5/5] Refine Olivia Wilde's boyfriend example prompt to work better (#248) With the original prompt, the chain keeps trying to jump straight to doing math directly, without first looking up ages. With this two-part question, it behaves more as intended: > Entering new ZeroShotAgent chain... How old is Olivia Wilde's boyfriend? What is that number raised to the 0.23 power? Thought: I need to find out how old Olivia Wilde's boyfriend is, and then use a calculator to calculate the power. Action: Search Action Input: Olivia Wilde's boyfriend age Observation: While Wilde, 37, and Styles, 27, have both kept a low profile when it comes to talking about their relationship, Wilde did address their ... Thought: Olivia Wilde's boyfriend is 27 years old. Action: Calculator Action Input: 27^0.23 > Entering new LLMMathChain chain... 27^0.23 ```python import math print(math.pow(27, 0.23)) ``` Answer: 2.1340945944237553 > Finished LLMMathChain chain. Observation: Answer: 2.1340945944237553 Thought: I now know the final answer. Final Answer: 2.1340945944237553 > Finished ZeroShotAgent chain. --- docs/getting_started/agents.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting_started/agents.ipynb b/docs/getting_started/agents.ipynb index 7f7b22ae..d60030d0 100644 --- a/docs/getting_started/agents.ipynb +++ b/docs/getting_started/agents.ipynb @@ -160,7 +160,7 @@ } ], "source": [ - "agent.run(\"What is the age of Olivia Wilde's boyfriend raised to the 0.23 power?\")" + "agent.run(\"How old is Olivia Wilde's boyfriend? What is that number raised to the 0.23 power?\")" ] }, {