From b7566b5ec3cd7c90bf7af147eb4255f8b6bbf9e7 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Tue, 27 Dec 2022 08:22:48 -0500 Subject: [PATCH 1/7] Harrison/return intermediate steps (#428) --- .../qa_with_sources.ipynb | 92 ++++++++++++++++++- .../question_answering.ipynb | 92 ++++++++++++++++++- .../data_augmented_generation/summarize.ipynb | 90 +++++++++++++++++- langchain/chains/combine_documents/base.py | 9 +- .../chains/combine_documents/map_reduce.py | 25 ++++- langchain/chains/combine_documents/refine.py | 25 ++++- langchain/chains/combine_documents/stuff.py | 6 +- langchain/chains/mapreduce.py | 2 +- langchain/chains/qa_with_sources/base.py | 2 +- langchain/chains/vector_db_qa/base.py | 2 +- 10 files changed, 325 insertions(+), 20 deletions(-) diff --git a/docs/examples/data_augmented_generation/qa_with_sources.ipynb b/docs/examples/data_augmented_generation/qa_with_sources.ipynb index 29454eae23c..b73b291c672 100644 --- a/docs/examples/data_augmented_generation/qa_with_sources.ipynb +++ b/docs/examples/data_augmented_generation/qa_with_sources.ipynb @@ -183,6 +183,51 @@ "chain({\"input_documents\": docs, \"question\": query}, return_only_outputs=True)" ] }, + { + "cell_type": "markdown", + "id": "ae2f6d97", + "metadata": {}, + "source": [ + "**Intermediate Steps**\n", + "\n", + "We can also return the intermediate steps for `map_reduce` chains, should we want to inspect them. This is done with the `return_map_steps` variable." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "15af265f", + "metadata": {}, + "outputs": [], + "source": [ + "chain = load_qa_with_sources_chain(OpenAI(temperature=0), chain_type=\"map_reduce\", return_map_steps=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "21b136e5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'map_steps': [{'text': ' \"Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service.\"'},\n", + " {'text': ' None'},\n", + " {'text': ' None'},\n", + " {'text': ' None'}],\n", + " 'output_text': ' The president thanked Justice Breyer for his service.\\nSOURCES: 30-pl'}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain({\"input_documents\": docs, \"question\": query}, return_only_outputs=True)" + ] + }, { "cell_type": "markdown", "id": "5bf0e1ab", @@ -225,10 +270,55 @@ "chain({\"input_documents\": docs, \"question\": query}, return_only_outputs=True)" ] }, + { + "cell_type": "markdown", + "id": "ac357530", + "metadata": {}, + "source": [ + "**Intermediate Steps**\n", + "\n", + "We can also return the intermediate steps for `refine` chains, should we want to inspect them. This is done with the `return_refine_steps` variable." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "3396a773", + "metadata": {}, + "outputs": [], + "source": [ + "chain = load_qa_with_sources_chain(OpenAI(temperature=0), chain_type=\"refine\", return_refine_steps=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "be5739ef", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'refine_steps': ['\\nThe president said that he was honoring Justice Breyer for his dedication to serving the country and that he was a retiring Justice of the United States Supreme Court.',\n", + " \"\\n\\nThe president said that he was honoring Justice Breyer for his dedication to serving the country, his career as a top litigator in private practice, a former federal public defender, and his family of public school educators and police officers. He also noted Justice Breyer's consensus-building skills and the broad range of support he has received from the Fraternal Order of Police to former judges appointed by Democrats and Republicans. He also highlighted the importance of advancing liberty and justice by securing the border and fixing the immigration system, noting the new technology and joint patrols with Mexico and Guatemala to catch more human traffickers, as well as the dedicated immigration judges and commitments to support partners in South and Central America to host more refugees and secure their own borders. \\nSource: 31\",\n", + " \"\\n\\nThe president said that he was honoring Justice Breyer for his dedication to serving the country, his career as a top litigator in private practice, a former federal public defender, and his family of public school educators and police officers. He also noted Justice Breyer's consensus-building skills and the broad range of support he has received from the Fraternal Order of Police to former judges appointed by Democrats and Republicans. He also highlighted the importance of advancing liberty and justice by securing the border and fixing the immigration system, noting the new technology and joint patrols with Mexico and Guatemala to catch more human traffickers, as well as the dedicated immigration judges and commitments to support partners in South and Central America to host more refugees and secure their own borders. Additionally, he mentioned the need for the bipartisan Equality Act to be passed and signed into law, and the importance of strengthening the Violence Against Women Act. He also offered a Unity Agenda for the Nation, which includes beating the opioid epidemic. \\nSource: 31, 33\",\n", + " \"\\n\\nThe president said that he was honoring Justice Breyer for his dedication to serving the country, his career as a top litigator in private practice, a former federal public defender, and his family of public school educators and police officers. He also noted Justice Breyer's consensus-building skills and the broad range of support he has received from the Fraternal Order of Police to former judges appointed by Democrats and Republicans. He also highlighted the importance of advancing liberty and justice by securing the border and fixing the immigration system, noting the new technology and joint patrols with Mexico and Guatemala to catch more human traffickers, as well as the dedicated immigration judges and commitments to support partners in South and Central America to host more refugees and secure their own borders. Additionally, he mentioned the need for the bipartisan Equality Act to be passed and signed into law, and the importance of strengthening the Violence Against Women Act. He also offered a Unity Agenda for the Nation, which includes beating the opioid epidemic, and announced that the Justice Department will name a chief prosecutor for pandemic fraud. Source: 31, 33, 20\"],\n", + " 'output_text': \"\\n\\nThe president said that he was honoring Justice Breyer for his dedication to serving the country, his career as a top litigator in private practice, a former federal public defender, and his family of public school educators and police officers. He also noted Justice Breyer's consensus-building skills and the broad range of support he has received from the Fraternal Order of Police to former judges appointed by Democrats and Republicans. He also highlighted the importance of advancing liberty and justice by securing the border and fixing the immigration system, noting the new technology and joint patrols with Mexico and Guatemala to catch more human traffickers, as well as the dedicated immigration judges and commitments to support partners in South and Central America to host more refugees and secure their own borders. Additionally, he mentioned the need for the bipartisan Equality Act to be passed and signed into law, and the importance of strengthening the Violence Against Women Act. He also offered a Unity Agenda for the Nation, which includes beating the opioid epidemic, and announced that the Justice Department will name a chief prosecutor for pandemic fraud. Source: 31, 33, 20\"}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain({\"input_documents\": docs, \"question\": query}, return_only_outputs=True)" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "929620d0", + "id": "7355fedd", "metadata": {}, "outputs": [], "source": [] diff --git a/docs/examples/data_augmented_generation/question_answering.ipynb b/docs/examples/data_augmented_generation/question_answering.ipynb index 68ac550f4cd..e140071cbf9 100644 --- a/docs/examples/data_augmented_generation/question_answering.ipynb +++ b/docs/examples/data_augmented_generation/question_answering.ipynb @@ -173,6 +173,51 @@ "chain({\"input_documents\": docs, \"question\": query}, return_only_outputs=True)" ] }, + { + "cell_type": "markdown", + "id": "31478d32", + "metadata": {}, + "source": [ + "**Intermediate Steps**\n", + "\n", + "We can also return the intermediate steps for `map_reduce` chains, should we want to inspect them. This is done with the `return_map_steps` variable." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "452c8680", + "metadata": {}, + "outputs": [], + "source": [ + "chain = load_qa_chain(OpenAI(temperature=0), chain_type=\"map_reduce\", return_map_steps=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "90b47a75", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'map_steps': [{'text': ' \"Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service.\"'},\n", + " {'text': ' None'},\n", + " {'text': ' None'},\n", + " {'text': ' None'}],\n", + " 'output_text': ' The president said, \"Justice Breyer, thank you for your service.\"'}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain({\"input_documents\": docs, \"question\": query}, return_only_outputs=True)" + ] + }, { "cell_type": "markdown", "id": "6ea50ad0", @@ -215,10 +260,55 @@ "chain({\"input_documents\": docs, \"question\": query}, return_only_outputs=True)" ] }, + { + "cell_type": "markdown", + "id": "f95dfb2e", + "metadata": {}, + "source": [ + "**Intermediate Steps**\n", + "\n", + "We can also return the intermediate steps for `refine` chains, should we want to inspect them. This is done with the `return_refine_steps` variable." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "a5c64200", + "metadata": {}, + "outputs": [], + "source": [ + "chain = load_qa_chain(OpenAI(temperature=0), chain_type=\"refine\", return_refine_steps=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "817546ac", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'refine_steps': ['\\nThe president said that he wanted to honor Justice Breyer for his dedication to serving the country and his legacy of excellence.',\n", + " '\\n\\nThe president said that he wanted to honor Justice Breyer for his dedication to serving the country, his legacy of excellence, and his commitment to advancing liberty and justice.',\n", + " '\\n\\nThe president said that he wanted to honor Justice Breyer for his dedication to serving the country, his legacy of excellence, and his commitment to advancing liberty and justice, as well as for his commitment to protecting the rights of LGBTQ+ Americans and his support for the bipartisan Equality Act.',\n", + " '\\n\\nThe president said that he wanted to honor Justice Breyer for his dedication to serving the country, his legacy of excellence, and his commitment to advancing liberty and justice, as well as for his commitment to protecting the rights of LGBTQ+ Americans and his support for the bipartisan Equality Act. He also mentioned his plan to lower costs to give families a fair shot, lower the deficit, and go after criminals who stole pandemic relief funds. He also announced that the Justice Department will name a chief prosecutor for pandemic fraud.'],\n", + " 'output_text': '\\n\\nThe president said that he wanted to honor Justice Breyer for his dedication to serving the country, his legacy of excellence, and his commitment to advancing liberty and justice, as well as for his commitment to protecting the rights of LGBTQ+ Americans and his support for the bipartisan Equality Act. He also mentioned his plan to lower costs to give families a fair shot, lower the deficit, and go after criminals who stole pandemic relief funds. He also announced that the Justice Department will name a chief prosecutor for pandemic fraud.'}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain({\"input_documents\": docs, \"question\": query}, return_only_outputs=True)" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "49e9c6d7", + "id": "97d335c6", "metadata": {}, "outputs": [], "source": [] diff --git a/docs/examples/data_augmented_generation/summarize.ipynb b/docs/examples/data_augmented_generation/summarize.ipynb index ee6b6572b31..24c17ca13d5 100644 --- a/docs/examples/data_augmented_generation/summarize.ipynb +++ b/docs/examples/data_augmented_generation/summarize.ipynb @@ -160,6 +160,50 @@ "chain.run(docs)" ] }, + { + "cell_type": "markdown", + "id": "d0c2a6d3", + "metadata": {}, + "source": [ + "**Intermediate Steps**\n", + "\n", + "We can also return the intermediate steps for `map_reduce` chains, should we want to inspect them. This is done with the `return_map_steps` variable." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "d9cfc24e", + "metadata": {}, + "outputs": [], + "source": [ + "chain = load_summarize_chain(OpenAI(temperature=0), chain_type=\"map_reduce\", return_map_steps=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "c7dff5e8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'map_steps': [{'text': \" In response to Russia's aggression in Ukraine, the United States has united with other freedom-loving nations to impose economic sanctions and hold Putin accountable. The U.S. Department of Justice is also assembling a task force to go after the crimes of Russian oligarchs and seize their ill-gotten gains.\"},\n", + " {'text': ' The United States and its European allies are taking action to punish Russia for its invasion of Ukraine, including seizing assets, closing off airspace, and providing economic and military assistance to Ukraine. The US is also mobilizing forces to protect NATO countries and has released 30 million barrels of oil from its Strategic Petroleum Reserve to help blunt gas prices. The world is uniting in support of Ukraine and democracy, and the US stands with its Ukrainian allies.'},\n", + " {'text': \" President Biden and Vice President Harris ran for office with a new economic vision for America, and have since passed the American Rescue Plan and the Bipartisan Infrastructure Law to help struggling families and rebuild America's infrastructure. This includes creating jobs, modernizing roads, airports, ports, and waterways, replacing lead pipes, providing affordable high-speed internet, and investing in American products to support American jobs.\"}],\n", + " 'output_text': \" In response to Russia's aggression in Ukraine, the United States and its allies have imposed economic sanctions and are taking other measures to hold Putin accountable. The US is also providing economic and military assistance to Ukraine, protecting NATO countries, and passing legislation to help struggling families and rebuild America's infrastructure. The world is uniting in support of Ukraine and democracy, and the US stands with its Ukrainian allies.\"}" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain({\"input_documents\": docs}, return_only_outputs=True)" + ] + }, { "cell_type": "markdown", "id": "f61350f9", @@ -201,10 +245,54 @@ "chain.run(docs)" ] }, + { + "cell_type": "markdown", + "id": "84e9567e", + "metadata": {}, + "source": [ + "**Intermediate Steps**\n", + "\n", + "We can also return the intermediate steps for `refine` chains, should we want to inspect them. This is done with the `return_refine_steps` variable." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "cd49ac4d", + "metadata": {}, + "outputs": [], + "source": [ + "chain = load_summarize_chain(OpenAI(temperature=0), chain_type=\"refine\", return_refine_steps=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6a74029d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'refine_steps': [\" In response to Russia's aggression in Ukraine, the United States has united with other freedom-loving nations to impose economic sanctions and hold Putin accountable. The U.S. Department of Justice is also assembling a task force to go after the crimes of Russian oligarchs and seize their ill-gotten gains.\",\n", + " \"\\n\\nIn response to Russia's aggression in Ukraine, the United States has united with other freedom-loving nations to impose economic sanctions and hold Putin accountable. The U.S. Department of Justice is also assembling a task force to go after the crimes of Russian oligarchs and seize their ill-gotten gains. We are joining with our European allies to find and seize the assets of Russian oligarchs, including yachts, luxury apartments, and private jets. The U.S. is also closing off American airspace to all Russian flights, further isolating Russia and adding an additional squeeze on their economy. The U.S. and its allies are providing support to the Ukrainians in their fight for freedom, including military, economic, and humanitarian assistance. The U.S. is also mobilizing ground forces, air squadrons, and ship deployments to protect NATO countries. The U.S. and its allies are also releasing 60 million barrels of oil from reserves around the world, with the U.S. contributing 30 million barrels from its own Strategic Petroleum Reserve. Putin's war on Ukraine has left Russia weaker and the rest of the world stronger, with the world uniting in support of democracy and peace.\",\n", + " \"\\n\\nIn response to Russia's aggression in Ukraine, the United States has united with other freedom-loving nations to impose economic sanctions and hold Putin accountable. The U.S. Department of Justice is also assembling a task force to go after the crimes of Russian oligarchs and seize their ill-gotten gains. We are joining with our European allies to find and seize the assets of Russian oligarchs, including yachts, luxury apartments, and private jets. The U.S. is also closing off American airspace to all Russian flights, further isolating Russia and adding an additional squeeze on their economy. The U.S. and its allies are providing support to the Ukrainians in their fight for freedom, including military, economic, and humanitarian assistance. The U.S. is also mobilizing ground forces, air squadrons, and ship deployments to protect NATO countries. The U.S. and its allies are also releasing 60 million barrels of oil from reserves around the world, with the U.S. contributing 30 million barrels from its own Strategic Petroleum Reserve. In addition, the U.S. has passed the American Rescue Plan to provide immediate economic relief for tens of millions of Americans, and the Bipartisan Infrastructure Law to rebuild America and create jobs. This includes investing\"],\n", + " 'output_text': \"\\n\\nIn response to Russia's aggression in Ukraine, the United States has united with other freedom-loving nations to impose economic sanctions and hold Putin accountable. The U.S. Department of Justice is also assembling a task force to go after the crimes of Russian oligarchs and seize their ill-gotten gains. We are joining with our European allies to find and seize the assets of Russian oligarchs, including yachts, luxury apartments, and private jets. The U.S. is also closing off American airspace to all Russian flights, further isolating Russia and adding an additional squeeze on their economy. The U.S. and its allies are providing support to the Ukrainians in their fight for freedom, including military, economic, and humanitarian assistance. The U.S. is also mobilizing ground forces, air squadrons, and ship deployments to protect NATO countries. The U.S. and its allies are also releasing 60 million barrels of oil from reserves around the world, with the U.S. contributing 30 million barrels from its own Strategic Petroleum Reserve. In addition, the U.S. has passed the American Rescue Plan to provide immediate economic relief for tens of millions of Americans, and the Bipartisan Infrastructure Law to rebuild America and create jobs. This includes investing\"}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chain({\"input_documents\": docs}, return_only_outputs=True)" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "0da92750", + "id": "db1ed69d", "metadata": {}, "outputs": [], "source": [] diff --git a/langchain/chains/combine_documents/base.py b/langchain/chains/combine_documents/base.py index 7d5574caaea..944440e94a0 100644 --- a/langchain/chains/combine_documents/base.py +++ b/langchain/chains/combine_documents/base.py @@ -1,7 +1,7 @@ """Base interface for chains combining documents.""" from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple from pydantic import BaseModel @@ -39,12 +39,13 @@ class BaseCombineDocumentsChain(Chain, BaseModel, ABC): return None @abstractmethod - def combine_docs(self, docs: List[Document], **kwargs: Any) -> str: + def combine_docs(self, docs: List[Document], **kwargs: Any) -> Tuple[str, dict]: """Combine documents into a single string.""" def _call(self, inputs: Dict[str, Any]) -> Dict[str, str]: docs = inputs[self.input_key] # Other keys are assumed to be needed for LLM prediction other_keys = {k: v for k, v in inputs.items() if k != self.input_key} - output = self.combine_docs(docs, **other_keys) - return {self.output_key: output} + output, extra_return_dict = self.combine_docs(docs, **other_keys) + extra_return_dict[self.output_key] = output + return extra_return_dict diff --git a/langchain/chains/combine_documents/map_reduce.py b/langchain/chains/combine_documents/map_reduce.py index 8653ef1a7f8..dd192062f3d 100644 --- a/langchain/chains/combine_documents/map_reduce.py +++ b/langchain/chains/combine_documents/map_reduce.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional, Tuple from pydantic import BaseModel, Extra, root_validator @@ -65,6 +65,19 @@ class MapReduceDocumentsChain(BaseCombineDocumentsChain, BaseModel): document_variable_name: str """The variable name in the llm_chain to put the documents in. If only one variable in the llm_chain, this need not be provided.""" + return_map_steps: bool = False + """Return the results of the map steps in the output.""" + + @property + def output_keys(self) -> List[str]: + """Expect input key. + + :meta private: + """ + _output_keys = super().output_keys + if self.return_map_steps: + _output_keys = _output_keys + ["map_steps"] + return _output_keys class Config: """Configuration for this pydantic object.""" @@ -102,7 +115,7 @@ class MapReduceDocumentsChain(BaseCombineDocumentsChain, BaseModel): def combine_docs( self, docs: List[Document], token_max: int = 3000, **kwargs: Any - ) -> str: + ) -> Tuple[str, dict]: """Combine documents in a map reduce manner. Combine by mapping first chain over all documents, then reducing the results. @@ -133,5 +146,9 @@ class MapReduceDocumentsChain(BaseCombineDocumentsChain, BaseModel): num_tokens = self.combine_document_chain.prompt_length( result_docs, **kwargs ) - output = self.combine_document_chain.combine_docs(result_docs, **kwargs) - return output + if self.return_map_steps: + extra_return_dict = {"map_steps": results} + else: + extra_return_dict = {} + output, _ = self.combine_document_chain.combine_docs(result_docs, **kwargs) + return output, extra_return_dict diff --git a/langchain/chains/combine_documents/refine.py b/langchain/chains/combine_documents/refine.py index 105df6f4887..c91bf07089f 100644 --- a/langchain/chains/combine_documents/refine.py +++ b/langchain/chains/combine_documents/refine.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Dict, List +from typing import Any, Dict, List, Tuple from pydantic import BaseModel, Extra, Field, root_validator @@ -33,6 +33,19 @@ class RefineDocumentsChain(BaseCombineDocumentsChain, BaseModel): default_factory=_get_default_document_prompt ) """Prompt to use to format each document.""" + return_refine_steps: bool = False + """Return the results of the refine steps in the output.""" + + @property + def output_keys(self) -> List[str]: + """Expect input key. + + :meta private: + """ + _output_keys = super().output_keys + if self.return_refine_steps: + _output_keys = _output_keys + ["refine_steps"] + return _output_keys class Config: """Configuration for this pydantic object.""" @@ -61,7 +74,7 @@ class RefineDocumentsChain(BaseCombineDocumentsChain, BaseModel): ) return values - def combine_docs(self, docs: List[Document], **kwargs: Any) -> str: + def combine_docs(self, docs: List[Document], **kwargs: Any) -> Tuple[str, dict]: """Combine by mapping first chain over all, then stuffing into final chain.""" base_info = {"page_content": docs[0].page_content} base_info.update(docs[0].metadata) @@ -71,6 +84,7 @@ class RefineDocumentsChain(BaseCombineDocumentsChain, BaseModel): } inputs = {**base_inputs, **kwargs} res = self.initial_llm_chain.predict(**inputs) + refine_steps = [res] for doc in docs[1:]: base_info = {"page_content": doc.page_content} base_info.update(doc.metadata) @@ -85,4 +99,9 @@ class RefineDocumentsChain(BaseCombineDocumentsChain, BaseModel): } inputs = {**base_inputs, **kwargs} res = self.refine_llm_chain.predict(**inputs) - return res + refine_steps.append(res) + if self.return_refine_steps: + extra_return_dict = {"refine_steps": refine_steps} + else: + extra_return_dict = {} + return res, extra_return_dict diff --git a/langchain/chains/combine_documents/stuff.py b/langchain/chains/combine_documents/stuff.py index 796de39e37f..67bdfa7512b 100644 --- a/langchain/chains/combine_documents/stuff.py +++ b/langchain/chains/combine_documents/stuff.py @@ -1,6 +1,6 @@ """Chain that combines documents by stuffing into context.""" -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple from pydantic import BaseModel, Extra, Field, root_validator @@ -78,8 +78,8 @@ class StuffDocumentsChain(BaseCombineDocumentsChain, BaseModel): prompt = self.llm_chain.prompt.format(**inputs) return self.llm_chain.llm.get_num_tokens(prompt) - def combine_docs(self, docs: List[Document], **kwargs: Any) -> str: + def combine_docs(self, docs: List[Document], **kwargs: Any) -> Tuple[str, dict]: """Stuff all documents into one prompt and pass to LLM.""" inputs = self._get_inputs(docs, **kwargs) # Call predict on the LLM. - return self.llm_chain.predict(**inputs) + return self.llm_chain.predict(**inputs), {} diff --git a/langchain/chains/mapreduce.py b/langchain/chains/mapreduce.py index ea01ab54283..583e484badd 100644 --- a/langchain/chains/mapreduce.py +++ b/langchain/chains/mapreduce.py @@ -70,5 +70,5 @@ class MapReduceChain(Chain, BaseModel): # Split the larger text into smaller chunks. texts = self.text_splitter.split_text(inputs[self.input_key]) docs = [Document(page_content=text) for text in texts] - outputs = self.combine_documents_chain.combine_docs(docs) + outputs, _ = self.combine_documents_chain.combine_docs(docs) return {self.output_key: outputs} diff --git a/langchain/chains/qa_with_sources/base.py b/langchain/chains/qa_with_sources/base.py index 48a3d017271..9eabb9f56e3 100644 --- a/langchain/chains/qa_with_sources/base.py +++ b/langchain/chains/qa_with_sources/base.py @@ -106,7 +106,7 @@ class BaseQAWithSourcesChain(Chain, BaseModel, ABC): def _call(self, inputs: Dict[str, Any]) -> Dict[str, str]: docs = self._get_docs(inputs) - answer = self.combine_document_chain.combine_docs(docs, **inputs) + answer, _ = self.combine_document_chain.combine_docs(docs, **inputs) if "\nSOURCES: " in answer: answer, sources = answer.split("\nSOURCES: ") else: diff --git a/langchain/chains/vector_db_qa/base.py b/langchain/chains/vector_db_qa/base.py index 1788d962a72..2067dc34006 100644 --- a/langchain/chains/vector_db_qa/base.py +++ b/langchain/chains/vector_db_qa/base.py @@ -101,5 +101,5 @@ class VectorDBQA(Chain, BaseModel): def _call(self, inputs: Dict[str, str]) -> Dict[str, str]: question = inputs[self.input_key] docs = self.vectorstore.similarity_search(question, k=self.k) - answer = self.combine_documents_chain.combine_docs(docs, question=question) + answer, _ = self.combine_documents_chain.combine_docs(docs, question=question) return {self.output_key: answer} From 150b67de10d45967ce226212c7565b65dbb67805 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Tue, 27 Dec 2022 08:23:13 -0500 Subject: [PATCH 2/7] Harrison/weaviate improvements (#433) Co-authored-by: Connor Shorten --- langchain/vectorstores/weaviate.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/langchain/vectorstores/weaviate.py b/langchain/vectorstores/weaviate.py index 5eba77997c6..ea08cf9245e 100644 --- a/langchain/vectorstores/weaviate.py +++ b/langchain/vectorstores/weaviate.py @@ -2,6 +2,7 @@ from __future__ import annotations from typing import Any, Iterable, List, Optional +from uuid import uuid4 from langchain.docstore.document import Document from langchain.embeddings.base import Embeddings @@ -52,8 +53,23 @@ class Weaviate(VectorStore): def add_texts( self, texts: Iterable[str], metadatas: Optional[List[dict]] = None ) -> List[str]: - """Not implemented for Weaviate yet.""" - raise NotImplementedError("weaviate does not currently support `add_texts`.") + """Upload texts with metadata (properties) to Weaviate.""" + from weaviate.util import get_valid_uuid + + with self._client.batch as batch: + ids = [] + for i, doc in enumerate(texts): + data_properties = { + self._text_key: doc, + } + if metadatas is not None: + for key in metadatas[i].keys(): + data_properties[key] = metadatas[i][key] + + _id = get_valid_uuid(uuid4()) + batch.add_data_object(data_properties, self._index_name, _id) + ids.append(_id) + return ids def similarity_search(self, query: str, k: int = 4) -> List[Document]: """Look up similar documents in weaviate.""" From f8b605293f2dde556132e903e30bd37ec8d8ac8c Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Tue, 27 Dec 2022 08:23:51 -0500 Subject: [PATCH 3/7] Harrison/improve memory (#432) add AI prefix add new type of memory Co-authored-by: Jason --- langchain/chains/conversation/memory.py | 97 ++++++++++++++++++- tests/integration_tests/chains/test_memory.py | 31 ++++++ tests/unit_tests/chains/test_conversation.py | 10 +- 3 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 tests/integration_tests/chains/test_memory.py diff --git a/langchain/chains/conversation/memory.py b/langchain/chains/conversation/memory.py index 0a686ddee05..9311acc1b96 100644 --- a/langchain/chains/conversation/memory.py +++ b/langchain/chains/conversation/memory.py @@ -22,6 +22,8 @@ def _get_prompt_input_key(inputs: Dict[str, Any], memory_variables: List[str]) - class ConversationBufferMemory(Memory, BaseModel): """Buffer for storing conversation memory.""" + ai_prefix: str = "AI" + """Prefix to use for AI generated responses.""" buffer: str = "" memory_key: str = "history" #: :meta private: @@ -43,7 +45,7 @@ class ConversationBufferMemory(Memory, BaseModel): if len(outputs) != 1: raise ValueError(f"One output key expected, got {outputs.keys()}") human = "Human: " + inputs[prompt_input_key] - ai = "AI: " + outputs[list(outputs.keys())[0]] + ai = f"{self.ai_prefix}: " + outputs[list(outputs.keys())[0]] self.buffer += "\n" + "\n".join([human, ai]) def clear(self) -> None: @@ -54,6 +56,8 @@ class ConversationBufferMemory(Memory, BaseModel): class ConversationalBufferWindowMemory(Memory, BaseModel): """Buffer for storing conversation memory.""" + ai_prefix: str = "AI" + """Prefix to use for AI generated responses.""" buffer: List[str] = Field(default_factory=list) memory_key: str = "history" #: :meta private: k: int = 5 @@ -76,7 +80,7 @@ class ConversationalBufferWindowMemory(Memory, BaseModel): if len(outputs) != 1: raise ValueError(f"One output key expected, got {outputs.keys()}") human = "Human: " + inputs[prompt_input_key] - ai = "AI: " + outputs[list(outputs.keys())[0]] + ai = f"{self.ai_prefix}: " + outputs[list(outputs.keys())[0]] self.buffer.append("\n".join([human, ai])) def clear(self) -> None: @@ -88,6 +92,8 @@ class ConversationSummaryMemory(Memory, BaseModel): """Conversation summarizer to memory.""" buffer: str = "" + ai_prefix: str = "AI" + """Prefix to use for AI generated responses.""" llm: BaseLLM prompt: BasePromptTemplate = SUMMARY_PROMPT memory_key: str = "history" #: :meta private: @@ -122,7 +128,7 @@ class ConversationSummaryMemory(Memory, BaseModel): if len(outputs) != 1: raise ValueError(f"One output key expected, got {outputs.keys()}") human = f"Human: {inputs[prompt_input_key]}" - ai = f"AI: {list(outputs.values())[0]}" + ai = f"{self.ai_prefix}: {list(outputs.values())[0]}" new_lines = "\n".join([human, ai]) chain = LLMChain(llm=self.llm, prompt=self.prompt) self.buffer = chain.predict(summary=self.buffer, new_lines=new_lines) @@ -130,3 +136,88 @@ class ConversationSummaryMemory(Memory, BaseModel): def clear(self) -> None: """Clear memory contents.""" self.buffer = "" + + +class ConversationSummaryBufferMemory(Memory, BaseModel): + """Buffer with summarizer for storing conversation memory.""" + + buffer: List[str] = Field(default_factory=list) + max_token_limit: int = 2000 + moving_summary_buffer: str = "" + llm: BaseLLM + prompt: BasePromptTemplate = SUMMARY_PROMPT + memory_key: str = "history" + ai_prefix: str = "AI" + """Prefix to use for AI generated responses.""" + + @property + def memory_variables(self) -> List[str]: + """Will always return list of memory variables. + + :meta private: + """ + return [self.memory_key] + + def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, str]: + """Return history buffer.""" + if self.moving_summary_buffer == "": + return {self.memory_key: "\n".join(self.buffer)} + memory_val = self.moving_summary_buffer + "\n" + "\n".join(self.buffer) + return {self.memory_key: memory_val} + + @root_validator() + def validate_prompt_input_variables(cls, values: Dict) -> Dict: + """Validate that prompt input variables are consistent.""" + prompt_variables = values["prompt"].input_variables + expected_keys = {"summary", "new_lines"} + if expected_keys != set(prompt_variables): + raise ValueError( + "Got unexpected prompt input variables. The prompt expects " + f"{prompt_variables}, but it should have {expected_keys}." + ) + return values + + def get_num_tokens_list(self, arr: List[str]) -> List[int]: + """Get list of number of tokens in each string in the input array.""" + try: + import tiktoken + except ImportError: + raise ValueError( + "Could not import tiktoken python package. " + "This is needed in order to calculate get_num_tokens_list. " + "Please it install it with `pip install tiktoken`." + ) + # create a GPT-3 encoder instance + enc = tiktoken.get_encoding("gpt2") + + # encode the list of text using the GPT-3 encoder + tokenized_text = enc.encode_ordinary_batch(arr) + + # calculate the number of tokens for each encoded text in the list + return [len(x) for x in tokenized_text] + + def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, str]) -> None: + """Save context from this conversation to buffer.""" + prompt_input_key = _get_prompt_input_key(inputs, self.memory_variables) + if len(outputs) != 1: + raise ValueError(f"One output key expected, got {outputs.keys()}") + human = f"Human: {inputs[prompt_input_key]}" + ai = f"{self.ai_prefix}: {list(outputs.values())[0]}" + new_lines = "\n".join([human, ai]) + self.buffer.append(new_lines) + # Prune buffer if it exceeds max token limit + curr_buffer_length = sum(self.get_num_tokens_list(self.buffer)) + if curr_buffer_length > self.max_token_limit: + pruned_memory = [] + while curr_buffer_length > self.max_token_limit: + pruned_memory.append(self.buffer.pop(0)) + curr_buffer_length = sum(self.get_num_tokens_list(self.buffer)) + chain = LLMChain(llm=self.llm, prompt=self.prompt) + self.moving_summary_buffer = chain.predict( + summary=self.moving_summary_buffer, new_lines=("\n".join(pruned_memory)) + ) + + def clear(self) -> None: + """Clear memory contents.""" + self.buffer = [] + self.moving_summary_buffer = "" diff --git a/tests/integration_tests/chains/test_memory.py b/tests/integration_tests/chains/test_memory.py new file mode 100644 index 00000000000..20e723fe208 --- /dev/null +++ b/tests/integration_tests/chains/test_memory.py @@ -0,0 +1,31 @@ +"""Test memory functionality.""" +from langchain.chains.conversation.memory import ConversationSummaryBufferMemory +from tests.unit_tests.llms.fake_llm import FakeLLM + + +def test_summary_buffer_memory_no_buffer_yet() -> None: + """Test ConversationSummaryBufferMemory when no inputs put in buffer yet.""" + memory = ConversationSummaryBufferMemory(llm=FakeLLM(), memory_key="baz") + output = memory.load_memory_variables({}) + assert output == {"baz": ""} + + +def test_summary_buffer_memory_buffer_only() -> None: + """Test ConversationSummaryBufferMemory when only buffer.""" + memory = ConversationSummaryBufferMemory(llm=FakeLLM(), memory_key="baz") + memory.save_context({"input": "bar"}, {"output": "foo"}) + assert memory.buffer == ["Human: bar\nAI: foo"] + output = memory.load_memory_variables({}) + assert output == {"baz": "Human: bar\nAI: foo"} + + +def test_summary_buffer_memory_summary() -> None: + """Test ConversationSummaryBufferMemory when only buffer.""" + memory = ConversationSummaryBufferMemory( + llm=FakeLLM(), memory_key="baz", max_token_limit=13 + ) + memory.save_context({"input": "bar"}, {"output": "foo"}) + memory.save_context({"input": "bar1"}, {"output": "foo1"}) + assert memory.buffer == ["Human: bar1\nAI: foo1"] + output = memory.load_memory_variables({}) + assert output == {"baz": "foo\nHuman: bar1\nAI: foo1"} diff --git a/tests/unit_tests/chains/test_conversation.py b/tests/unit_tests/chains/test_conversation.py index fd7eb55fb13..ce80fd6f56e 100644 --- a/tests/unit_tests/chains/test_conversation.py +++ b/tests/unit_tests/chains/test_conversation.py @@ -12,6 +12,13 @@ from langchain.prompts.prompt import PromptTemplate from tests.unit_tests.llms.fake_llm import FakeLLM +def test_memory_ai_prefix() -> None: + """Test that ai_prefix in the memory component works.""" + memory = ConversationBufferMemory(memory_key="foo", ai_prefix="Assistant") + memory.save_context({"input": "bar"}, {"output": "foo"}) + assert memory.buffer == "\nHuman: bar\nAssistant: foo" + + def test_conversation_chain_works() -> None: """Test that conversation chain works in basic setting.""" llm = FakeLLM() @@ -42,6 +49,7 @@ def test_conversation_chain_errors_bad_variable() -> None: "memory", [ ConversationBufferMemory(memory_key="baz"), + ConversationalBufferWindowMemory(memory_key="baz"), ConversationSummaryMemory(llm=FakeLLM(), memory_key="baz"), ], ) @@ -81,7 +89,7 @@ def test_clearing_conversation_memory(memory: Memory) -> None: """Test clearing the conversation memory.""" # This is a good input because the input is not the same as baz. good_inputs = {"foo": "bar", "baz": "foo"} - # This is a good output because these is one variable. + # This is a good output because there is one variable. good_outputs = {"bar": "foo"} memory.save_context(good_inputs, good_outputs) From 0c5d3fd894ae56aefba9ad498ccd41ef5c0f8f44 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Tue, 27 Dec 2022 09:17:01 -0500 Subject: [PATCH 4/7] version 0.0.49 (#436) --- docs/examples/memory.rst | 15 +- .../memory/conversational_customization.ipynb | 262 +++++++ .../memory/conversational_memory.ipynb | 724 ++++++++++++++++++ docs/getting_started/memory.ipynb | 524 +++++-------- langchain/chains/conversation/memory.py | 23 +- pyproject.toml | 2 +- tests/unit_tests/chains/test_conversation.py | 6 +- 7 files changed, 1203 insertions(+), 353 deletions(-) create mode 100644 docs/examples/memory/conversational_customization.ipynb create mode 100644 docs/examples/memory/conversational_memory.ipynb diff --git a/docs/examples/memory.rst b/docs/examples/memory.rst index c5c064f88db..ddab6c7b733 100644 --- a/docs/examples/memory.rst +++ b/docs/examples/memory.rst @@ -1,11 +1,22 @@ Memory ====== -The examples here are all related to working with the concept of Memory in LangChain. +The examples here all highlight how to use memory in different ways. + +`Adding Memory `_: How to add a memory component to any chain. + +`Conversational Memory Types `_: An overview of the different types of conversation memory you can load and use with a conversation-like chain. + +`Conversational Memory Customization `_: How to customize existing conversation memory components. + +`Custom Memory `_: How to write your own custom memory component. + +`Adding Memory to Agents `_: How to add a memory component to any agent. + .. toctree:: :maxdepth: 1 :glob: - :caption: Memory + :hidden: memory/* \ No newline at end of file diff --git a/docs/examples/memory/conversational_customization.ipynb b/docs/examples/memory/conversational_customization.ipynb new file mode 100644 index 00000000000..a7ab34e5337 --- /dev/null +++ b/docs/examples/memory/conversational_customization.ipynb @@ -0,0 +1,262 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "69e35d6f", + "metadata": {}, + "source": [ + "# Conversational Memory Customization\n", + "\n", + "This notebook walks through a few ways to customize conversational memory.\n", + "\n", + "The main way to do so is by changing the AI prefix in the conversation summary. By default, this is set to \"AI\", but you can set this to be anything you want. Note that if you change this, you should also change the prompt used in the chain to reflect this naming change. Let's walk through an example of that in the example below." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0f964494", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.llms import OpenAI\n", + "from langchain.chains import ConversationChain\n", + "from langchain.chains.conversation.memory import ConversationBufferMemory\n", + "\n", + "\n", + "llm = OpenAI(temperature=0)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d0e66d87", + "metadata": {}, + "outputs": [], + "source": [ + "# Here it is by default set to \"AI\"\n", + "conversation = ConversationChain(\n", + " llm=llm, \n", + " verbose=True, \n", + " memory=ConversationBufferMemory()\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f8fa6999", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "Human: Hi there!\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\" Hi there! It's nice to meet you. How can I help you today?\"" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation.predict(input=\"Hi there!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "de213386", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "Human: Hi there!\n", + "AI: Hi there! It's nice to meet you. How can I help you today?\n", + "Human: What's the weather?\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "' The current weather is sunny and warm with a temperature of 75 degrees Fahrenheit. The forecast for the next few days is sunny with temperatures in the mid-70s.'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation.predict(input=\"What's the weather?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "585949eb", + "metadata": {}, + "outputs": [], + "source": [ + "# Now we can override it and set it to \"AI Assistant\"\n", + "from langchain.prompts.prompt import PromptTemplate\n", + "\n", + "template = \"\"\"The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "{history}\n", + "Human: {input}\n", + "AI Assistant:\"\"\"\n", + "PROMPT = PromptTemplate(\n", + " input_variables=[\"history\", \"input\"], template=template\n", + ")\n", + "conversation = ConversationChain(\n", + " prompt=PROMPT,\n", + " llm=llm, \n", + " verbose=True, \n", + " memory=ConversationBufferMemory(ai_prefix=\"AI Assistant\")\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "1bb9bc53", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "Human: Hi there!\n", + "AI Assistant:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\" Hi there! It's nice to meet you. How can I help you today?\"" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation.predict(input=\"Hi there!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "d9241923", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "Human: Hi there!\n", + "AI Assistant: Hi there! It's nice to meet you. How can I help you today?\n", + "Human: What's the weather?\n", + "AI Assistant:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "' The current weather is sunny and warm with a temperature of 75 degrees Fahrenheit. The forecast for the rest of the day is sunny with a high of 78 degrees and a low of 65 degrees.'" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation.predict(input=\"What's the weather?\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1023b6ef", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/examples/memory/conversational_memory.ipynb b/docs/examples/memory/conversational_memory.ipynb new file mode 100644 index 00000000000..154ba982748 --- /dev/null +++ b/docs/examples/memory/conversational_memory.ipynb @@ -0,0 +1,724 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d31df93e", + "metadata": {}, + "source": [ + "# Conversational Memory\n", + "\n", + "This notebook walks through the different types of memory you can use with the `ConversationChain`." + ] + }, + { + "cell_type": "markdown", + "id": "d051c1da", + "metadata": {}, + "source": [ + "### ConversationBufferMemory (default)\n", + "By default, the `ConversationChain` uses `ConversationBufferMemory`: a simple type of memory that remembers all previous inputs/outputs and adds them to the context that is passed. Let's take a look at using this chain (setting `verbose=True` so we can see the prompt)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54301321", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.llms import OpenAI\n", + "from langchain.chains import ConversationChain\n", + "from langchain.chains.conversation.memory import ConversationBufferMemory\n", + "\n", + "\n", + "llm = OpenAI(temperature=0)\n", + "conversation = ConversationChain(\n", + " llm=llm, \n", + " verbose=True, \n", + " memory=ConversationBufferMemory()\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ae046bff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "Human: Hi there!\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\" Hi there! It's nice to meet you. How can I help you today?\"" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation.predict(input=\"Hi there!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d8e2a6ff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "Human: Hi there!\n", + "AI: Hi there! It's nice to meet you. How can I help you today?\n", + "Human: I'm doing well! Just having a conversation with an AI.\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\" That's great! It's always nice to have a conversation with someone new. What would you like to talk about?\"" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation.predict(input=\"I'm doing well! Just having a conversation with an AI.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "15eda316", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "Human: Hi there!\n", + "AI: Hi there! It's nice to meet you. How can I help you today?\n", + "Human: I'm doing well! Just having a conversation with an AI.\n", + "AI: That's great! It's always nice to have a conversation with someone new. What would you like to talk about?\n", + "Human: Tell me about yourself.\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\" Sure! I'm an AI created to help people with their everyday tasks. I'm programmed to understand natural language and provide helpful information. I'm also constantly learning and updating my knowledge base so I can provide more accurate and helpful answers.\"" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation.predict(input=\"Tell me about yourself.\")" + ] + }, + { + "cell_type": "markdown", + "id": "4fad9448", + "metadata": {}, + "source": [ + "### ConversationSummaryMemory\n", + "Now let's take a look at using a slightly more complex type of memory - `ConversationSummaryMemory`. This type of memory creates a summary of the conversation over time. This can be useful for condensing information from the conversation over time.\n", + "\n", + "Let's walk through an example, again setting `verbose=True` so we can see the prompt." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f60a2fe8", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.chains.conversation.memory import ConversationSummaryMemory" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b7274f2c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "Human: Hi, what's up?\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\" Hi there! I'm doing great. I'm currently helping a customer with a technical issue. How about you?\"" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation_with_summary = ConversationChain(\n", + " llm=llm, \n", + " memory=ConversationSummaryMemory(llm=OpenAI()),\n", + " verbose=True\n", + ")\n", + "conversation_with_summary.predict(input=\"Hi, what's up?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a6b6b88f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "The human greeted the AI, to which the AI replied that it was doing great and was helping a customer with a technical issue.\n", + "Human: Tell me more about it!\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "' Sure! The customer was having trouble with their computer not connecting to the internet. I was able to help them troubleshoot the issue and get them connected. It was a great feeling to be able to help them out!'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation_with_summary.predict(input=\"Tell me more about it!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "dad869fe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "The human greeted the AI, to which the AI replied that it was doing great and was helping a customer with a technical issue. The AI explained the customer was having trouble with their computer not connecting to the internet and that it was a great feeling to be able to help them out and get them connected.\n", + "Human: Very cool -- what is the scope of the project?\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\" The scope of the project is to help the customer troubleshoot their computer and get it connected to the internet. I'm currently helping them identify the source of the issue and then providing them with the necessary steps to get their computer connected.\"" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation_with_summary.predict(input=\"Very cool -- what is the scope of the project?\")" + ] + }, + { + "cell_type": "markdown", + "id": "6eecf9d9", + "metadata": {}, + "source": [ + "# ConversationBufferWindowMemory\n", + "\n", + "`ConversationBufferWindowMemory` keeps a list of the interactions of the conversation over time. It only uses the last K interactions. This can be useful for keeping a sliding window of the most recent interactions, so the buffer does not get too large\n", + "\n", + "Let's walk through an example, again setting `verbose=True` so we can see the prompt." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2dac7769", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.chains.conversation.memory import ConversationBufferWindowMemory" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0b9da4cd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "Human: Hi, what's up?\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\" Hi there! I'm doing great. I'm currently helping a customer with a technical issue. How about you?\"" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation_with_summary = ConversationChain(\n", + " llm=llm, \n", + " # We set a low k=2, to only keep the last 2 interactions in memory\n", + " memory=ConversationBufferWindowMemory(k=2), \n", + " verbose=True\n", + ")\n", + "conversation_with_summary.predict(input=\"Hi, what's up?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "90f73431", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "Human: Hi, what's up?\n", + "AI: Hi there! I'm doing great. I'm currently helping a customer with a technical issue. How about you?\n", + "Human: What's their issues?\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\" The customer is having trouble connecting to their Wi-Fi network. I'm helping them troubleshoot the issue and get them connected.\"" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation_with_summary.predict(input=\"What's their issues?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cbb499e7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "Human: Hi, what's up?\n", + "AI: Hi there! I'm doing great. I'm currently helping a customer with a technical issue. How about you?\n", + "Human: What's their issues?\n", + "AI: The customer is having trouble connecting to their Wi-Fi network. I'm helping them troubleshoot the issue and get them connected.\n", + "Human: Is it going well?\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\" Yes, it's going well so far. We've already identified the problem and are now working on a solution.\"" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation_with_summary.predict(input=\"Is it going well?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "0d209cfe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "Human: What's their issues?\n", + "AI: The customer is having trouble connecting to their Wi-Fi network. I'm helping them troubleshoot the issue and get them connected.\n", + "Human: Is it going well?\n", + "AI: Yes, it's going well so far. We've already identified the problem and are now working on a solution.\n", + "Human: What's the solution?\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\" The solution is to reset the router and reconfigure the settings. We're currently in the process of doing that.\"" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Notice here that the first interaction does not appear.\n", + "conversation_with_summary.predict(input=\"What's the solution?\")" + ] + }, + { + "cell_type": "markdown", + "id": "a6d2569f", + "metadata": {}, + "source": [ + "# ConversationSummaryBufferMemory\n", + "\n", + "`ConversationSummaryBufferMemory` combines the last two ideas. It keeps a buffer of recent interactions in memory, but rather than just completely flushing old interactions it compiles them into a summary and uses both. Unlike the previous implementation though, it uses token length rather than number of interactions to determine when to flush interactions.\n", + "\n", + "Let's walk through an example, again setting `verbose=True` so we can see the prompt." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e583a661", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.chains.conversation.memory import ConversationSummaryBufferMemory" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "ebd68c10", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "Human: Hi, what's up?\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\" Hi there! I'm doing great. I'm currently helping a customer with a technical issue. How about you?\"" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation_with_summary = ConversationChain(\n", + " llm=llm, \n", + " # We set a very low max_token_limit for the purposes of testing.\n", + " memory=ConversationSummaryBufferMemory(llm=OpenAI(), max_token_limit=40),\n", + " verbose=True\n", + ")\n", + "conversation_with_summary.predict(input=\"Hi, what's up?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "86207a61", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "Human: Hi, what's up?\n", + "AI: Hi there! I'm doing great. I'm currently helping a customer with a technical issue. How about you?\n", + "Human: Just working on writing some documentation!\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "' That sounds like a lot of work. What kind of documentation are you writing?'" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation_with_summary.predict(input=\"Just working on writing some documentation!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "76a0ab39", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "The human asked the AI what it was up to and the AI replied that it was helping a customer with a technical issue.\n", + "Human: Just working on writing some documentation!\n", + "AI: That sounds like a lot of work. What kind of documentation are you writing?\n", + "Human: For LangChain! Have you heard of it?\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "' Yes, I have heard of LangChain. It is a blockchain-based language learning platform. Can you tell me more about the documentation you are writing?'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# We can see here that there is a summary of the conversation and then some previous interactions\n", + "conversation_with_summary.predict(input=\"For LangChain! Have you heard of it?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "8c669db1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new ConversationChain chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "The human asked the AI what it was up to and the AI replied that it was helping a customer with a technical issue. The human mentioned they were writing documentation for LangChain, a blockchain-based language learning platform, and the AI had heard of it and asked for more information.\n", + "\n", + "Human: Haha nope, although a lot of people confuse it for that\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished ConversationChain chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "' Oh, I see. So what is LangChain then?'" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# We can see here that the summary and the buffer are updated\n", + "conversation_with_summary.predict(input=\"Haha nope, although a lot of people confuse it for that\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f71f40ba", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/getting_started/memory.ipynb b/docs/getting_started/memory.ipynb index 98ec8209b36..f300c081f83 100644 --- a/docs/getting_started/memory.ipynb +++ b/docs/getting_started/memory.ipynb @@ -1,333 +1,197 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "d31df93e", - "metadata": {}, - "source": [ - "# Memory\n", - "So far, all the chains and agents we've gone through have been stateless. But often, you may want a chain or agent to have some concept of \"memory\" so that it may remember information about its previous interactions. The clearest and simple example of this is when designing a chatbot - you want it to remember previous messages so it can use context from that to have a better conversation. This would be a type of \"short-term memory\". On the more complex side, you could imagine a chain/agent remembering key pieces of information over time - this would be a form of \"long-term memory\". For more concrete ideas on the latter, see this [awesome paper](https://memprompt.com/).\n", - "\n", - "LangChain provides several specially created chains just for this purpose. This notebook walks through using one of those chains (the `ConversationChain`) with two different types of memory." - ] - }, - { - "cell_type": "markdown", - "id": "d051c1da", - "metadata": {}, - "source": [ - "### ConversationChain with default memory\n", - "By default, the `ConversationChain` has a simple type of memory that remembers all previous inputs/outputs and adds them to the context that is passed. Let's take a look at using this chain (setting `verbose=True` so we can see the prompt)." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "ae046bff", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\u001b[1m> Entering new chain...\u001b[0m\n", - "Prompt after formatting:\n", - "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", - "\n", - "Current conversation:\n", - "\n", - "Human: Hi there!\n", - "AI:\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "' Hello! How are you today?'" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from langchain import OpenAI, ConversationChain\n", - "\n", - "llm = OpenAI(temperature=0)\n", - "conversation = ConversationChain(llm=llm, verbose=True)\n", - "\n", - "conversation.predict(input=\"Hi there!\")" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "d8e2a6ff", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\u001b[1m> Entering new chain...\u001b[0m\n", - "Prompt after formatting:\n", - "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", - "\n", - "Current conversation:\n", - "\n", - "Human: Hi there!\n", - "AI: Hello! How are you today?\n", - "Human: I'm doing well! Just having a conversation with an AI.\n", - "AI:\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "\" That's great! What would you like to talk about?\"" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "conversation.predict(input=\"I'm doing well! Just having a conversation with an AI.\")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "15eda316", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\u001b[1m> Entering new chain...\u001b[0m\n", - "Prompt after formatting:\n", - "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", - "\n", - "Current conversation:\n", - "\n", - "Human: Hi there!\n", - "AI: Hello! How are you today?\n", - "Human: I'm doing well! Just having a conversation with an AI.\n", - "AI: That's great! What would you like to talk about?\n", - "Human: Tell me about yourself.\n", - "AI:\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "' I am an AI created to provide information and support to humans. I enjoy learning and exploring new things.'" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "conversation.predict(input=\"Tell me about yourself.\")" - ] - }, - { - "cell_type": "markdown", - "id": "4fad9448", - "metadata": {}, - "source": [ - "### ConversationChain with ConversationSummaryMemory\n", - "Now let's take a look at using a slightly more complex type of memory - `ConversationSummaryMemory`. This type of memory creates a summary of the conversation over time. This can be useful for condensing information from the conversation over time.\n", - "\n", - "Let's walk through an example, again setting `verbose=True` so we can see the prompt." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "f60a2fe8", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.chains.conversation.memory import ConversationSummaryMemory" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "b7274f2c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\u001b[1m> Entering new chain...\u001b[0m\n", - "Prompt after formatting:\n", - "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", - "\n", - "Current conversation:\n", - "\n", - "Human: Hi, what's up?\n", - "AI:\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "\"\\n\\nI'm doing well, thank you for asking. I'm currently working on a project that I'm really excited about.\"" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "conversation_with_summary = ConversationChain(llm=llm, memory=ConversationSummaryMemory(llm=OpenAI()), verbose=True)\n", - "conversation_with_summary.predict(input=\"Hi, what's up?\")" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "a6b6b88f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\u001b[1m> Entering new chain...\u001b[0m\n", - "Prompt after formatting:\n", - "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", - "\n", - "Current conversation:\n", - "\n", - "The human and artificial intelligence are talking. The human asked the AI what it is doing, and the AI said that it is working on a project that it is excited about.\n", - "Human: Tell me more about it!\n", - "AI:\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "\"\\n\\nI'm working on a project that I'm really excited about. It's a lot of work, but I think it's going to be really great when it's finished. I can't wait to show it to you!\"" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "conversation_with_summary.predict(input=\"Tell me more about it!\")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "dad869fe", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\u001b[1m> Entering new chain...\u001b[0m\n", - "Prompt after formatting:\n", - "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", - "\n", - "Current conversation:\n", - "\n", - "\n", - "The human and artificial intelligence are talking. The human asked the AI what it is doing, and the AI said that it is working on a project that it is excited about. The AI said that the project is a lot of work, but it is going to be great when it is finished.\n", - "Human: Very cool -- what is the scope of the project?\n", - "AI:\u001b[0m\n", - "\n", - "\u001b[1m> Finished chain.\u001b[0m\n" - ] - }, - { - "data": { - "text/plain": [ - "'\\n\\nThe project is quite large in scope. It involves a lot of data analysis and work with artificial intelligence algorithms.'" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "conversation_with_summary.predict(input=\"Very cool -- what is the scope of the project?\")" - ] - }, - { - "cell_type": "markdown", - "id": "5c8735cc", - "metadata": {}, - "source": [ - "### More Resources on Memory\n", - "\n", - "This just scratches the surface of what you can do with memory. For more examples on things like how to implement custom memory classes, how to add memory to a custom LLM chain and how to use memory with an agent, please see the [How-To: Memory](../../examples/memory) section. For even more advanced ideas on memory (which will hopefully be included in LangChain soon!) see the [MemPrompt](https://memprompt.com/) paper." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "436dda66", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "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.1" - } + "cells": [ + { + "cell_type": "markdown", + "id": "d31df93e", + "metadata": {}, + "source": [ + "# Memory\n", + "So far, all the chains and agents we've gone through have been stateless. But often, you may want a chain or agent to have some concept of \"memory\" so that it may remember information about its previous interactions. The clearest and simple example of this is when designing a chatbot - you want it to remember previous messages so it can use context from that to have a better conversation. This would be a type of \"short-term memory\". On the more complex side, you could imagine a chain/agent remembering key pieces of information over time - this would be a form of \"long-term memory\". For more concrete ideas on the latter, see this [awesome paper](https://memprompt.com/).\n", + "\n", + "LangChain provides several specially created chains just for this purpose. This notebook walks through using one of those chains (the `ConversationChain`) with two different types of memory." + ] }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "markdown", + "id": "d051c1da", + "metadata": {}, + "source": [ + "### ConversationChain with default memory\n", + "By default, the `ConversationChain` has a simple type of memory that remembers all previous inputs/outputs and adds them to the context that is passed. Let's take a look at using this chain (setting `verbose=True` so we can see the prompt)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ae046bff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "Human: Hi there!\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "' Hello! How are you today?'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain import OpenAI, ConversationChain\n", + "\n", + "llm = OpenAI(temperature=0)\n", + "conversation = ConversationChain(llm=llm, verbose=True)\n", + "\n", + "conversation.predict(input=\"Hi there!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d8e2a6ff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "Human: Hi there!\n", + "AI: Hello! How are you today?\n", + "Human: I'm doing well! Just having a conversation with an AI.\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\" That's great! What would you like to talk about?\"" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation.predict(input=\"I'm doing well! Just having a conversation with an AI.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "15eda316", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new chain...\u001b[0m\n", + "Prompt after formatting:\n", + "\u001b[32;1m\u001b[1;3mThe following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n", + "\n", + "Current conversation:\n", + "\n", + "Human: Hi there!\n", + "AI: Hello! How are you today?\n", + "Human: I'm doing well! Just having a conversation with an AI.\n", + "AI: That's great! What would you like to talk about?\n", + "Human: Tell me about yourself.\n", + "AI:\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "' I am an AI created to provide information and support to humans. I enjoy learning and exploring new things.'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversation.predict(input=\"Tell me about yourself.\")" + ] + }, + { + "cell_type": "markdown", + "id": "5c8735cc", + "metadata": {}, + "source": [ + "### More Resources on Memory\n", + "\n", + "This just scratches the surface of what you can do with memory. \n", + "\n", + "For more concrete examples of conversational memory, please this [this notebook](../../examples/memory/conversational_memory.ipynb)\n", + "\n", + "For more examples on things like how to implement custom memory classes, how to add memory to a custom LLM chain and how to use memory with an agent, please see the [How-To: Memory](../../examples/memory) section. \n", + "\n", + "For even more advanced ideas on memory (which will hopefully be included in LangChain soon!) see the [MemPrompt](https://memprompt.com/) paper." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "436dda66", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.10.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/langchain/chains/conversation/memory.py b/langchain/chains/conversation/memory.py index 9311acc1b96..ace8bf99740 100644 --- a/langchain/chains/conversation/memory.py +++ b/langchain/chains/conversation/memory.py @@ -53,7 +53,7 @@ class ConversationBufferMemory(Memory, BaseModel): self.buffer = "" -class ConversationalBufferWindowMemory(Memory, BaseModel): +class ConversationBufferWindowMemory(Memory, BaseModel): """Buffer for storing conversation memory.""" ai_prefix: str = "AI" @@ -88,6 +88,10 @@ class ConversationalBufferWindowMemory(Memory, BaseModel): self.buffer = [] +# For legacy naming reasons +ConversationalBufferWindowMemory = ConversationBufferWindowMemory + + class ConversationSummaryMemory(Memory, BaseModel): """Conversation summarizer to memory.""" @@ -179,22 +183,7 @@ class ConversationSummaryBufferMemory(Memory, BaseModel): def get_num_tokens_list(self, arr: List[str]) -> List[int]: """Get list of number of tokens in each string in the input array.""" - try: - import tiktoken - except ImportError: - raise ValueError( - "Could not import tiktoken python package. " - "This is needed in order to calculate get_num_tokens_list. " - "Please it install it with `pip install tiktoken`." - ) - # create a GPT-3 encoder instance - enc = tiktoken.get_encoding("gpt2") - - # encode the list of text using the GPT-3 encoder - tokenized_text = enc.encode_ordinary_batch(arr) - - # calculate the number of tokens for each encoded text in the list - return [len(x) for x in tokenized_text] + return [self.llm.get_num_tokens(x) for x in arr] def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, str]) -> None: """Save context from this conversation to buffer.""" diff --git a/pyproject.toml b/pyproject.toml index 3b5d2e0d5e9..5d833d3417a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langchain" -version = "0.0.48" +version = "0.0.49" description = "Building applications with LLMs through composability" authors = [] license = "MIT" diff --git a/tests/unit_tests/chains/test_conversation.py b/tests/unit_tests/chains/test_conversation.py index ce80fd6f56e..08eb0f0ef24 100644 --- a/tests/unit_tests/chains/test_conversation.py +++ b/tests/unit_tests/chains/test_conversation.py @@ -4,8 +4,8 @@ import pytest from langchain.chains.base import Memory from langchain.chains.conversation.base import ConversationChain from langchain.chains.conversation.memory import ( - ConversationalBufferWindowMemory, ConversationBufferMemory, + ConversationBufferWindowMemory, ConversationSummaryMemory, ) from langchain.prompts.prompt import PromptTemplate @@ -49,7 +49,7 @@ def test_conversation_chain_errors_bad_variable() -> None: "memory", [ ConversationBufferMemory(memory_key="baz"), - ConversationalBufferWindowMemory(memory_key="baz"), + ConversationBufferWindowMemory(memory_key="baz"), ConversationSummaryMemory(llm=FakeLLM(), memory_key="baz"), ], ) @@ -82,7 +82,7 @@ def test_conversation_memory(memory: Memory) -> None: [ ConversationBufferMemory(memory_key="baz"), ConversationSummaryMemory(llm=FakeLLM(), memory_key="baz"), - ConversationalBufferWindowMemory(memory_key="baz"), + ConversationBufferWindowMemory(memory_key="baz"), ], ) def test_clearing_conversation_memory(memory: Memory) -> None: From ffe35c396c48e4a20acc827cc92ce72dfc87cbd7 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Tue, 27 Dec 2022 19:53:45 -0500 Subject: [PATCH 5/7] unify return types across map-reduce and refine (#442) --- langchain/chains/combine_documents/map_reduce.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/langchain/chains/combine_documents/map_reduce.py b/langchain/chains/combine_documents/map_reduce.py index dd192062f3d..18389edf6ae 100644 --- a/langchain/chains/combine_documents/map_reduce.py +++ b/langchain/chains/combine_documents/map_reduce.py @@ -147,7 +147,8 @@ class MapReduceDocumentsChain(BaseCombineDocumentsChain, BaseModel): result_docs, **kwargs ) if self.return_map_steps: - extra_return_dict = {"map_steps": results} + _results = [r[self.llm_chain.output_key] for r in results] + extra_return_dict = {"map_steps": _results} else: extra_return_dict = {} output, _ = self.combine_document_chain.combine_docs(result_docs, **kwargs) From c994ce6b7ff7a4e30565c742f313e163f0b85f2c Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Tue, 27 Dec 2022 20:27:18 -0500 Subject: [PATCH 6/7] Harrison/serp api imp (#444) improve serp api Co-authored-by: Bruno Bornsztein --- langchain/serpapi.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/langchain/serpapi.py b/langchain/serpapi.py index 6224939d581..cb9e68e27c3 100644 --- a/langchain/serpapi.py +++ b/langchain/serpapi.py @@ -92,6 +92,16 @@ class SerpAPIWrapper(BaseModel): toret = res["answer_box"]["snippet_highlighted_words"][0] elif "snippet" in res["organic_results"][0].keys(): toret = res["organic_results"][0]["snippet"] + elif ( + "sports_results" in res.keys() + and "game_spotlight" in res["sports_results"].keys() + ): + toret = res["sports_results"]["game_spotlight"] + elif ( + "knowledge_graph" in res.keys() + and "description" in res["knowledge_graph"].keys() + ): + toret = res["knowledge_graph"]["description"] else: toret = "No good search result found" return toret From 9ec01dfc164777f234ba784b51d07fdaf6713384 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Tue, 27 Dec 2022 20:28:08 -0500 Subject: [PATCH 7/7] regex output parser (#435) --- langchain/evaluation/qa/generate_prompt.py | 27 +++++----------------- langchain/prompts/base.py | 18 ++++++++++++++- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/langchain/evaluation/qa/generate_prompt.py b/langchain/evaluation/qa/generate_prompt.py index 9ee74e8ceb3..6eb31374769 100644 --- a/langchain/evaluation/qa/generate_prompt.py +++ b/langchain/evaluation/qa/generate_prompt.py @@ -1,24 +1,6 @@ # flake8: noqa -import re -from typing import Dict - from langchain.prompts import PromptTemplate -from langchain.prompts.base import BaseOutputParser - - -class QAGenerationOutputParser(BaseOutputParser): - """Parse output in question/answer pair.""" - - def parse(self, text: str) -> Dict[str, str]: - regex = r"QUESTION: (.*?)\nANSWER: (.*)" - match = re.search(regex, text) - if match: - question = match.group(1) - answer = match.group(2) - return {"query": question, "answer": answer} - else: - raise ValueError(f"Could not parse output: {text}") - +from langchain.prompts.base import RegexParser template = """You are a teacher coming up with questions to ask on a quiz. Given the following document, please generate a question and answer based on that document. @@ -35,6 +17,9 @@ These questions should be detailed and be based explicitly on information in the {doc} """ -PROMPT = PromptTemplate( - input_variables=["doc"], template=template, output_parser=QAGenerationOutputParser() +output_parser = RegexParser( + regex=r"QUESTION: (.*?)\nANSWER: (.*)", output_keys=["question", "answer"] +) +PROMPT = PromptTemplate( + input_variables=["doc"], template=template, output_parser=output_parser ) diff --git a/langchain/prompts/base.py b/langchain/prompts/base.py index c7b708a345a..5221ff36459 100644 --- a/langchain/prompts/base.py +++ b/langchain/prompts/base.py @@ -1,5 +1,6 @@ """BasePrompt schema definition.""" import json +import re from abc import ABC, abstractmethod from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Union @@ -55,7 +56,7 @@ class BaseOutputParser(ABC): """Parse the output of an LLM call.""" -class ListOutputParser(ABC): +class ListOutputParser(BaseOutputParser): """Class to parse the output of an LLM call to a list.""" @abstractmethod @@ -63,6 +64,21 @@ class ListOutputParser(ABC): """Parse the output of an LLM call.""" +class RegexParser(BaseOutputParser, BaseModel): + """Class to parse the output into a dictionary.""" + + regex: str + output_keys: List[str] + + def parse(self, text: str) -> Dict[str, str]: + """Parse the output of an LLM call.""" + match = re.search(self.regex, text) + if match: + return {key: match.group(i) for i, key in enumerate(self.output_keys)} + else: + raise ValueError(f"Could not parse output: {text}") + + class BasePromptTemplate(BaseModel, ABC): """Base prompt should expose the format method, returning a prompt."""