diff --git a/README.md b/README.md index 7490e29f..a0542775 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,6 @@ create a truly powerful app - the real power comes when you are able to combine them with other sources of computation or knowledge. This library is aimed at assisting in the development of those types of applications. -It aims to create: - -1. a comprehensive collection of pieces you would ever want to combine -2. a flexible interface for combining pieces into a single comprehensive "chain" -3. a schema for easily saving and sharing those chains ## 📖 Documentation @@ -31,78 +26,42 @@ Please see [here](https://langchain.readthedocs.io/en/latest/?) for full documen - Reference (full API docs) - Resources (high level explanation of core concepts) -## 🚀 What can I do with this - -This project was largely inspired by a few projects seen on Twitter for which we thought it would make sense to have more explicit tooling. A lot of the initial functionality was done in an attempt to recreate those. Those are: - -**[Self-ask-with-search](https://ofir.io/self-ask.pdf)** - -To recreate this paper, use the following code snippet or checkout the [example notebook](https://github.com/hwchase17/langchain/blob/master/docs/examples/demos/self_ask_with_search.ipynb). - -```python -from langchain import SelfAskWithSearchChain, OpenAI, SerpAPIChain - -llm = OpenAI(temperature=0) -search = SerpAPIChain() - -self_ask_with_search = SelfAskWithSearchChain(llm=llm, search_chain=search) - -self_ask_with_search.run("What is the hometown of the reigning men's U.S. Open champion?") -``` - -**[LLM Math](https://twitter.com/amasad/status/1568824744367259648?s=20&t=-7wxpXBJinPgDuyHLouP1w)** - -To recreate this example, use the following code snippet or check out the [example notebook](https://github.com/hwchase17/langchain/blob/master/docs/examples/demos/llm_math.ipynb). - -```python -from langchain import OpenAI, LLMMathChain - -llm = OpenAI(temperature=0) -llm_math = LLMMathChain(llm=llm) - -llm_math.run("How many of the integers between 0 and 99 inclusive are divisible by 8?") -``` - -**Generic Prompting** - -You can also use this for simple prompting pipelines, as in the below example and this [example notebook](https://github.com/hwchase17/langchain/blob/master/docs/examples/demos/simple_prompts.ipynb). - -```python -from langchain import PromptTemplate, OpenAI, LLMChain - -template = """Question: {question} - -Answer: Let's think step by step.""" -prompt = PromptTemplate(template=template, input_variables=["question"]) -llm = OpenAI(temperature=0) -llm_chain = LLMChain(prompt=prompt, llm=llm) - -question = "What NFL team won the Super Bowl in the year Justin Bieber was born?" - -llm_chain.predict(question=question) -``` - -**Embed & Search Documents** - -We support two vector databases to store and search embeddings -- FAISS and Elasticsearch. Here's a code snippet showing how to use FAISS to store embeddings and search for text similar to a query. Both database backends are featured in this [example notebook](https://github.com/hwchase17/langchain/blob/master/docs/examples/integrations/embeddings.ipynb). - -```python -from langchain.embeddings.openai import OpenAIEmbeddings -from langchain.faiss import FAISS -from langchain.text_splitter import CharacterTextSplitter - -with open('state_of_the_union.txt') as f: - state_of_the_union = f.read() -text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0) -texts = text_splitter.split_text(state_of_the_union) - -embeddings = OpenAIEmbeddings() - -docsearch = FAISS.from_texts(texts, embeddings) - -query = "What did the president say about Ketanji Brown Jackson" -docs = docsearch.similarity_search(query) -``` +## 🚀 What can this help with? + +There are three main areas (with a forth coming soon) that LangChain is designed to help with. +These are, in increasing order of complexity: +1. LLM and Prompt usage +2. Chaining LLMs with other tools in a deterministic manner +3. Having a router LLM which uses other tools as needed +4. (Coming Soon) Memory + +### LLMs and Prompts +Calling out to an LLM once is pretty easy, with most of them being behind well documented APIs. +However, there are still some challenges going from that to an application running in production that LangChain attempts to address: +- Easy switching costs: by exposing a standard interface for all the top LLM providers, LangChain makes it easy to switch from one provider to another, whether it be for production use cases or just for testing stuff out. +- Prompt management: managing your prompts is easy when you only have one simple one, but can get tricky when you have a bunch or when they start to get more complex. LangChain provides a standard way for storing, constructing, and referencing prompts. +- Prompt optimization: despite the underlying models getting better and better, there is still currently a need for carefully constructing prompts. +- More coming soon + +### Chains +Using an LLM in isolation is fine for some simple applications, but many more complex ones require chaining LLMs - either with eachother or with other tools. +LangChain provides several parts to help with that: +- Standard interface for working with Chains +- Easy way to construct chains of LLMs +- Lots of integrations with other tools that you may want to use in conjunction with LLMs (search, databases, Python REPL, etc) +- End-to-end chains for common workflows (database question/answer, recursive summarization, etc) + +### Routing Chains +Some applications will require not just a predetermined chain of calls to LLMs/other tools, but potentially an unknown chain that depends on the user input. +In these types of chains, there is a "router" LLM chain which has access to a suite of tools. +Depending on the user input, the router can then decide which, if any, of these tools to call. +To help develop applications like these, LangChain provides: +- Standard router and router chain interfaces +- Common router LLM chains from literature +- Common chains that can be used as tools + +### Memory +Coming soon. ## 🤖 Developer Guide diff --git a/docs/examples/demos.rst b/docs/examples/demos.rst index e9518f1a..0ca535d2 100644 --- a/docs/examples/demos.rst +++ b/docs/examples/demos.rst @@ -2,9 +2,24 @@ Demos ===== The examples here are all end-to-end chains of specific applications. +They are separated into normal chains and then routing chains. .. toctree:: :maxdepth: 1 :glob: + :caption: Chains - demos/* + demos/llm_math.ipynb + demos/map_reduce.ipynb + demos/simple_prompts.ipynb + demos/sqlite.ipynb + demos/vector_db_qa.ipynb + +.. toctree:: + :maxdepth: 1 + :glob: + :caption: Routing Chains + + demos/mrkl.ipynb + demos/react.ipynb + demos/self_ask_with_search.ipynb diff --git a/docs/examples/demos/map reduce.ipynb b/docs/examples/demos/map_reduce.ipynb similarity index 100% rename from docs/examples/demos/map reduce.ipynb rename to docs/examples/demos/map_reduce.ipynb diff --git a/docs/examples/demos/react.ipynb b/docs/examples/demos/react.ipynb index 801bc121..f7a49d4a 100644 --- a/docs/examples/demos/react.ipynb +++ b/docs/examples/demos/react.ipynb @@ -40,10 +40,10 @@ "Thought 1:\u001b[32;1m\u001b[1;3m I need to search David Chanoff and find the U.S. Navy admiral he collaborated\n", "with.\n", "Action 1: Search[David Chanoff]\u001b[0m\n", - "Observation 1: \u001b[33;1m\u001b[1;3mDavid Chanoff is a noted author of non-fiction work. His work has typically involved collaborations with the principal protagonist of the work concerned. His collaborators have included; Augustus A. White, Joycelyn Elders, Đoàn Văn Toại, William J. Crowe, Ariel Sharon, Kenneth Good and Felix Zandman. He has also written about a wide range of subjects including literary history, education and foreign for The Washington Post, The New Republic and The New York Times Magazine. He has published more than twelve books.\u001b[0m\n", + "Observation 1: \u001b[36;1m\u001b[1;3mDavid Chanoff is a noted author of non-fiction work. His work has typically involved collaborations with the principal protagonist of the work concerned. His collaborators have included; Augustus A. White, Joycelyn Elders, Đoàn Văn Toại, William J. Crowe, Ariel Sharon, Kenneth Good and Felix Zandman. He has also written about a wide range of subjects including literary history, education and foreign for The Washington Post, The New Republic and The New York Times Magazine. He has published more than twelve books.\u001b[0m\n", "Thought 2:\u001b[32;1m\u001b[1;3m The U.S. Navy admiral David Chanoff collaborated with is William J. Crowe.\n", "Action 2: Search[William J. Crowe]\u001b[0m\n", - "Observation 2: \u001b[33;1m\u001b[1;3mWilliam James Crowe Jr. (January 2, 1925 – October 18, 2007) was a United States Navy admiral and diplomat who served as the 11th chairman of the Joint Chiefs of Staff under Presidents Ronald Reagan and George H. W. Bush, and as the ambassador to the United Kingdom and Chair of the Intelligence Oversight Board under President Bill Clinton.\u001b[0m\n", + "Observation 2: \u001b[36;1m\u001b[1;3mWilliam James Crowe Jr. (January 2, 1925 – October 18, 2007) was a United States Navy admiral and diplomat who served as the 11th chairman of the Joint Chiefs of Staff under Presidents Ronald Reagan and George H. W. Bush, and as the ambassador to the United Kingdom and Chair of the Intelligence Oversight Board under President Bill Clinton.\u001b[0m\n", "Thought 3:\u001b[32;1m\u001b[1;3m William J. Crowe served as the ambassador to the United Kingdom under President Bill Clinton.\n", "Action 3: Finish[Bill Clinton]\u001b[0m\n", "\u001b[1m> Finished chain.\u001b[0m\n" @@ -68,7 +68,7 @@ { "cell_type": "code", "execution_count": null, - "id": "2d6f4b1d", + "id": "3cb9d77c", "metadata": {}, "outputs": [], "source": [] diff --git a/docs/index.rst b/docs/index.rst index 1c583635..b2190cab 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,12 +8,47 @@ create a truly powerful app - the real power comes when you are able to combine them with other sources of computation or knowledge. This library is aimed at assisting in the development of those types of applications. -It aims to create: - -1. a comprehensive collection of pieces you would ever want to combine -2. a flexible interface for combining pieces into a single comprehensive "chain" -3. a schema for easily saving and sharing those chains +There are three main areas (with a forth coming soon) that LangChain is designed to help with. +These are, in increasing order of complexity: +1. LLM and Prompt usage +2. Chaining LLMs with other tools in a deterministic manner +3. Having a router LLM which uses other tools as needed +4. (Coming Soon) Memory + +**LLMs and Prompts** + +Calling out to an LLM once is pretty easy, with most of them being behind well documented APIs. +However, there are still some challenges going from that to an application running in production that LangChain attempts to address: +- Easy switching costs: by exposing a standard interface for all the top LLM providers, LangChain makes it easy to switch from one provider to another, whether it be for production use cases or just for testing stuff out. +- Prompt management: managing your prompts is easy when you only have one simple one, but can get tricky when you have a bunch or when they start to get more complex. LangChain provides a standard way for storing, constructing, and referencing prompts. +- Prompt optimization: despite the underlying models getting better and better, there is still currently a need for carefully constructing prompts. +- More coming soon + +**Chains** + +Using an LLM in isolation is fine for some simple applications, but many more complex ones require chaining LLMs - either with eachother or with other tools. +LangChain provides several parts to help with that: +- Standard interface for working with Chains +- Easy way to construct chains of LLMs +- Lots of integrations with other tools that you may want to use in conjunction with LLMs (search, databases, Python REPL, etc) +- End-to-end chains for common workflows (database question/answer, recursive summarization, etc) + +**Routing Chains** + +Some applications will require not just a predetermined chain of calls to LLMs/other tools, but potentially an unknown chain that depends on the user input. +In these types of chains, there is a "router" LLM chain which has access to a suite of tools. +Depending on the user input, the router can then decide which, if any, of these tools to call. +To help develop applications like these, LangChain provides: +- Standard router and router chain interfaces +- Common router LLM chains from literature +- Common chains that can be used as tools + +**Memory** +Coming soon. + +Documentation Structure +======================= The documentation is structured into the following sections: @@ -62,6 +97,7 @@ common tasks or cool demos. modules/text_splitter modules/vectorstore modules/chains + modules/routing_chains Full API documentation. This is the place to look if you want to diff --git a/docs/modules/routing_chains.rst b/docs/modules/routing_chains.rst new file mode 100644 index 00000000..a6d00c80 --- /dev/null +++ b/docs/modules/routing_chains.rst @@ -0,0 +1,7 @@ +:mod:`langchain.routing_chains` +=============================== + +.. automodule:: langchain.routing_chains + :members: + :undoc-members: + diff --git a/langchain/__init__.py b/langchain/__init__.py index c33d1206..7cd80a9f 100644 --- a/langchain/__init__.py +++ b/langchain/__init__.py @@ -21,7 +21,7 @@ from langchain.prompts import ( Prompt, PromptTemplate, ) -from langchain.smart_chains import MRKLChain, ReActChain, SelfAskWithSearchChain +from langchain.routing_chains import MRKLChain, ReActChain, SelfAskWithSearchChain from langchain.sql_database import SQLDatabase from langchain.vectorstores import FAISS, ElasticVectorSearch diff --git a/langchain/routing_chains/__init__.py b/langchain/routing_chains/__init__.py new file mode 100644 index 00000000..2a53116a --- /dev/null +++ b/langchain/routing_chains/__init__.py @@ -0,0 +1,14 @@ +"""Routing chains.""" +from langchain.routing_chains.mrkl.base import MRKLChain +from langchain.routing_chains.react.base import ReActChain +from langchain.routing_chains.router import LLMRouter +from langchain.routing_chains.routing_chain import RoutingChain +from langchain.routing_chains.self_ask_with_search.base import SelfAskWithSearchChain + +__all__ = [ + "MRKLChain", + "SelfAskWithSearchChain", + "ReActChain", + "LLMRouter", + "RoutingChain", +] diff --git a/langchain/smart_chains/mrkl/__init__.py b/langchain/routing_chains/mrkl/__init__.py similarity index 100% rename from langchain/smart_chains/mrkl/__init__.py rename to langchain/routing_chains/mrkl/__init__.py diff --git a/langchain/smart_chains/mrkl/base.py b/langchain/routing_chains/mrkl/base.py similarity index 91% rename from langchain/smart_chains/mrkl/base.py rename to langchain/routing_chains/mrkl/base.py index db357ae1..cc714280 100644 --- a/langchain/smart_chains/mrkl/base.py +++ b/langchain/routing_chains/mrkl/base.py @@ -4,9 +4,9 @@ from typing import Any, Callable, List, NamedTuple, Optional, Tuple from langchain.chains.llm import LLMChain from langchain.llms.base import LLM from langchain.prompts import PromptTemplate -from langchain.smart_chains.mrkl.prompt import BASE_TEMPLATE -from langchain.smart_chains.router import LLMRouterChain -from langchain.smart_chains.router_expert import ExpertConfig, RouterExpertChain +from langchain.routing_chains.mrkl.prompt import BASE_TEMPLATE +from langchain.routing_chains.router import LLMRouter +from langchain.routing_chains.routing_chain import RoutingChain, ToolConfig FINAL_ANSWER_ACTION = "Final Answer: " @@ -46,7 +46,7 @@ def get_action_and_input(llm_output: str) -> Tuple[str, str]: return action, action_input.strip(" ").strip('"') -class MRKLRouterChain(LLMRouterChain): +class MRKLRouterChain(LLMRouter): """Router for the MRKL chain.""" @property @@ -71,11 +71,11 @@ class MRKLRouterChain(LLMRouterChain): stops = ["\nObservation"] super().__init__(llm_chain=llm_chain, stops=stops, **kwargs) - def _extract_action_and_input(self, text: str) -> Optional[Tuple[str, str]]: + def _extract_tool_and_input(self, text: str) -> Optional[Tuple[str, str]]: return get_action_and_input(text) -class MRKLChain(RouterExpertChain): +class MRKLChain(RoutingChain): """Chain that implements the MRKL system. Example: @@ -130,6 +130,6 @@ class MRKLChain(RouterExpertChain): """ router_chain = MRKLRouterChain(llm, chains) expert_configs = [ - ExpertConfig(expert_name=c.action_name, expert=c.action) for c in chains + ToolConfig(tool_name=c.action_name, tool=c.action) for c in chains ] return cls(router_chain=router_chain, expert_configs=expert_configs, **kwargs) diff --git a/langchain/smart_chains/mrkl/prompt.py b/langchain/routing_chains/mrkl/prompt.py similarity index 100% rename from langchain/smart_chains/mrkl/prompt.py rename to langchain/routing_chains/mrkl/prompt.py diff --git a/langchain/smart_chains/react/__init__.py b/langchain/routing_chains/react/__init__.py similarity index 100% rename from langchain/smart_chains/react/__init__.py rename to langchain/routing_chains/react/__init__.py diff --git a/langchain/smart_chains/react/base.py b/langchain/routing_chains/react/base.py similarity index 81% rename from langchain/smart_chains/react/base.py rename to langchain/routing_chains/react/base.py index 44db7201..113465d2 100644 --- a/langchain/smart_chains/react/base.py +++ b/langchain/routing_chains/react/base.py @@ -8,12 +8,12 @@ from langchain.chains.llm import LLMChain from langchain.docstore.base import Docstore from langchain.docstore.document import Document from langchain.llms.base import LLM -from langchain.smart_chains.react.prompt import PROMPT -from langchain.smart_chains.router import LLMRouterChain -from langchain.smart_chains.router_expert import ExpertConfig, RouterExpertChain +from langchain.routing_chains.react.prompt import PROMPT +from langchain.routing_chains.router import LLMRouter +from langchain.routing_chains.routing_chain import RoutingChain, ToolConfig -class ReActRouterChain(LLMRouterChain, BaseModel): +class ReActRouterChain(LLMRouter, BaseModel): """Router for the ReAct chin.""" i: int = 1 @@ -27,7 +27,7 @@ class ReActRouterChain(LLMRouterChain, BaseModel): def _fix_text(self, text: str) -> str: return text + f"\nAction {self.i}:" - def _extract_action_and_input(self, text: str) -> Optional[Tuple[str, str]]: + def _extract_tool_and_input(self, text: str) -> Optional[Tuple[str, str]]: action_prefix = f"Action {self.i}: " if not text.split("\n")[-1].startswith(action_prefix): return None @@ -83,7 +83,7 @@ class DocstoreExplorer: return self.document.lookup(term) -class ReActChain(RouterExpertChain): +class ReActChain(RoutingChain): """Chain that implements the ReAct paper. Example: @@ -95,12 +95,10 @@ class ReActChain(RouterExpertChain): def __init__(self, llm: LLM, docstore: Docstore, **kwargs: Any): """Initialize with the LLM and a docstore.""" - router_chain = ReActRouterChain(llm) + router = ReActRouterChain(llm) docstore_explorer = DocstoreExplorer(docstore) - expert_configs = [ - ExpertConfig(expert_name="Search", expert=docstore_explorer.search), - ExpertConfig(expert_name="Lookup", expert=docstore_explorer.lookup), + tool_configs = [ + ToolConfig(tool_name="Search", tool=docstore_explorer.search), + ToolConfig(tool_name="Lookup", tool=docstore_explorer.lookup), ] - super().__init__( - router_chain=router_chain, expert_configs=expert_configs, **kwargs - ) + super().__init__(router=router, expert_configs=tool_configs, **kwargs) diff --git a/langchain/smart_chains/react/prompt.py b/langchain/routing_chains/react/prompt.py similarity index 100% rename from langchain/smart_chains/react/prompt.py rename to langchain/routing_chains/react/prompt.py diff --git a/langchain/routing_chains/router.py b/langchain/routing_chains/router.py new file mode 100644 index 00000000..cff56b3d --- /dev/null +++ b/langchain/routing_chains/router.py @@ -0,0 +1,86 @@ +"""Chain that takes in an input and produces an action and action input.""" +from abc import ABC, abstractmethod +from typing import NamedTuple, Optional, Tuple + +from pydantic import BaseModel + +from langchain.chains.llm import LLMChain + + +class RouterOutput(NamedTuple): + """Output of a router.""" + + tool: str + tool_input: str + log: str + + +class Router(ABC): + """Chain responsible for deciding the action to take.""" + + @abstractmethod + def route(self, text: str) -> RouterOutput: + """Given input, decided how to route it. + + Args: + text: input string + + Returns: + RouterOutput specifying what tool to use. + """ + + @property + @abstractmethod + def observation_prefix(self) -> str: + """Prefix to append the observation with.""" + + @property + @abstractmethod + def router_prefix(self) -> str: + """Prefix to append the router call with.""" + + @property + def finish_tool_name(self) -> str: + """Name of the tool to use to finish the chain.""" + return "Final Answer" + + @property + def starter_string(self) -> str: + """Put this string after user input but before first router call.""" + return "\n" + + +class LLMRouter(Router, BaseModel, ABC): + """Router that uses an LLM.""" + + llm_chain: LLMChain + + @abstractmethod + def _extract_tool_and_input(self, text: str) -> Optional[Tuple[str, str]]: + """Extract tool and tool input from llm output.""" + + def _fix_text(self, text: str) -> str: + """Fix the text.""" + raise ValueError("fix_text not implemented for this router.") + + def route(self, text: str) -> RouterOutput: + """Given input, decided how to route it. + + Args: + text: input string + + Returns: + RouterOutput specifying what tool to use. + """ + input_key = self.llm_chain.input_keys[0] + inputs = {input_key: text, "stop": [self.observation_prefix]} + full_output = self.llm_chain.predict(**inputs) + parsed_output = self._extract_tool_and_input(full_output) + while parsed_output is None: + full_output = self._fix_text(full_output) + inputs = {input_key: text + full_output, "stop": [self.observation_prefix]} + output = self.llm_chain.predict(**inputs) + full_output += output + parsed_output = self._extract_tool_and_input(full_output) + tool, tool_input = parsed_output + return RouterOutput(tool, tool_input, full_output) diff --git a/langchain/routing_chains/routing_chain.py b/langchain/routing_chains/routing_chain.py new file mode 100644 index 00000000..34101f66 --- /dev/null +++ b/langchain/routing_chains/routing_chain.py @@ -0,0 +1,70 @@ +"""Router-Expert framework.""" +from typing import Callable, Dict, List, NamedTuple + +from pydantic import BaseModel, Extra + +from langchain.chains.base import Chain +from langchain.input import ChainedInput, get_color_mapping +from langchain.routing_chains.router import Router + + +class ToolConfig(NamedTuple): + """Configuration for tools.""" + + tool_name: str + tool: Callable[[str], str] + + +class RoutingChain(Chain, BaseModel): + """Chain that uses a router to use tools.""" + + router: Router + """Router to use.""" + tool_configs: List[ToolConfig] + """Tool configs this chain has access to.""" + input_key: str = "question" #: :meta private: + output_key: str = "answer" #: :meta private: + + class Config: + """Configuration for this pydantic object.""" + + extra = Extra.forbid + arbitrary_types_allowed = True + + @property + def input_keys(self) -> List[str]: + """Expect input key. + + :meta private: + """ + return [self.input_key] + + @property + def output_keys(self) -> List[str]: + """Expect output key. + + :meta private: + """ + return [self.output_key] + + def _call(self, inputs: Dict[str, str]) -> Dict[str, str]: + name_to_tool_map = {tc.tool_name: tc.tool for tc in self.tool_configs} + starter_string = ( + inputs[self.input_key] + + self.router.starter_string + + self.router.router_prefix + ) + chained_input = ChainedInput(starter_string, verbose=self.verbose) + color_mapping = get_color_mapping( + [c.tool_name for c in self.tool_configs], excluded_colors=["green"] + ) + while True: + output = self.router.route(chained_input.input) + chained_input.add(output.log, color="green") + if output.tool == self.router.finish_tool_name: + return {self.output_key: output.tool_input} + chain = name_to_tool_map[output.tool] + observation = chain(output.tool_input) + chained_input.add(f"\n{self.router.observation_prefix}") + chained_input.add(observation, color=color_mapping[output.tool]) + chained_input.add(f"\n{self.router.router_prefix}") diff --git a/langchain/smart_chains/self_ask_with_search/__init__.py b/langchain/routing_chains/self_ask_with_search/__init__.py similarity index 100% rename from langchain/smart_chains/self_ask_with_search/__init__.py rename to langchain/routing_chains/self_ask_with_search/__init__.py diff --git a/langchain/smart_chains/self_ask_with_search/base.py b/langchain/routing_chains/self_ask_with_search/base.py similarity index 73% rename from langchain/smart_chains/self_ask_with_search/base.py rename to langchain/routing_chains/self_ask_with_search/base.py index fa501b78..ab4f8543 100644 --- a/langchain/smart_chains/self_ask_with_search/base.py +++ b/langchain/routing_chains/self_ask_with_search/base.py @@ -4,12 +4,12 @@ from typing import Any, Tuple from langchain.chains.llm import LLMChain from langchain.chains.serpapi import SerpAPIChain from langchain.llms.base import LLM -from langchain.smart_chains.router import LLMRouterChain -from langchain.smart_chains.router_expert import ExpertConfig, RouterExpertChain -from langchain.smart_chains.self_ask_with_search.prompt import PROMPT +from langchain.routing_chains.router import LLMRouter +from langchain.routing_chains.routing_chain import RoutingChain, ToolConfig +from langchain.routing_chains.self_ask_with_search.prompt import PROMPT -class SelfAskWithSearchRouter(LLMRouterChain): +class SelfAskWithSearchRouter(LLMRouter): """Router for the self-ask-with-search paper.""" def __init__(self, llm: LLM, **kwargs: Any): @@ -17,7 +17,7 @@ class SelfAskWithSearchRouter(LLMRouterChain): llm_chain = LLMChain(llm=llm, prompt=PROMPT) super().__init__(llm_chain=llm_chain, **kwargs) - def _extract_action_and_input(self, text: str) -> Tuple[str, str]: + def _extract_tool_and_input(self, text: str) -> Tuple[str, str]: followup = "Follow up:" if "\n" not in text: last_line = text @@ -52,8 +52,13 @@ class SelfAskWithSearchRouter(LLMRouterChain): """Prefix to append the router call with.""" return "" + @property + def starter_string(self) -> str: + """Put this string after user input but before first router call.""" + return "\nAre follow up questions needed here:" + -class SelfAskWithSearchChain(RouterExpertChain): +class SelfAskWithSearchChain(RoutingChain): """Chain that does self ask with search. Example: @@ -68,12 +73,6 @@ class SelfAskWithSearchChain(RouterExpertChain): """Initialize with just an LLM and a search chain.""" intermediate = "\nIntermediate answer:" router = SelfAskWithSearchRouter(llm, stops=[intermediate]) - expert_configs = [ - ExpertConfig(expert_name="Intermediate Answer", expert=search_chain.run) - ] - super().__init__( - router_chain=router, - expert_configs=expert_configs, - starter_string="\nAre follow up questions needed here:", - **kwargs - ) + search_tool = ToolConfig(tool_name="Intermediate Answer", tool=search_chain.run) + expert_configs = [search_tool] + super().__init__(router=router, expert_configs=expert_configs, **kwargs) diff --git a/langchain/smart_chains/self_ask_with_search/prompt.py b/langchain/routing_chains/self_ask_with_search/prompt.py similarity index 100% rename from langchain/smart_chains/self_ask_with_search/prompt.py rename to langchain/routing_chains/self_ask_with_search/prompt.py diff --git a/langchain/smart_chains/__init__.py b/langchain/smart_chains/__init__.py deleted file mode 100644 index 1b049008..00000000 --- a/langchain/smart_chains/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Smart chains.""" -from langchain.smart_chains.mrkl.base import MRKLChain -from langchain.smart_chains.react.base import ReActChain -from langchain.smart_chains.self_ask_with_search.base import SelfAskWithSearchChain - -__all__ = ["MRKLChain", "SelfAskWithSearchChain", "ReActChain"] diff --git a/langchain/smart_chains/router.py b/langchain/smart_chains/router.py deleted file mode 100644 index 15936071..00000000 --- a/langchain/smart_chains/router.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Chain that takes in an input and produces an action and action input.""" -from abc import ABC, abstractmethod -from typing import Dict, List, Optional, Tuple - -from pydantic import BaseModel - -from langchain.chains.base import Chain -from langchain.chains.llm import LLMChain - - -class RouterChain(Chain, BaseModel, ABC): - """Chain responsible for deciding the action to take.""" - - input_key: str = "input_text" #: :meta private: - action_key: str = "action" #: :meta private: - action_input_key: str = "action_input" #: :meta private: - log_key: str = "log" #: :meta private: - - @property - def input_keys(self) -> List[str]: - """Will be the input key. - - :meta private: - """ - return [self.input_key] - - @property - def output_keys(self) -> List[str]: - """Return three keys: the action, the action input, and the log. - - :meta private: - """ - return [self.action_key, self.action_input_key, self.log_key] - - @abstractmethod - def get_action_and_input(self, text: str) -> Tuple[str, str, str]: - """Return action, action input, and log (in that order).""" - - @property - @abstractmethod - def observation_prefix(self) -> str: - """Prefix to append the observation with.""" - - @property - @abstractmethod - def router_prefix(self) -> str: - """Prefix to append the router call with.""" - - @property - def finish_action_name(self) -> str: - """Name of the action of when to finish the chain.""" - return "Final Answer" - - def _call(self, inputs: Dict[str, str]) -> Dict[str, str]: - action, action_input, log = self.get_action_and_input(inputs[self.input_key]) - return { - self.action_key: action, - self.action_input_key: action_input, - self.log_key: log, - } - - -class LLMRouterChain(RouterChain, BaseModel, ABC): - """RouterChain that uses an LLM.""" - - llm_chain: LLMChain - stops: Optional[List[str]] - - @abstractmethod - def _extract_action_and_input(self, text: str) -> Optional[Tuple[str, str]]: - """Extract action and action input from llm output.""" - - def _fix_text(self, text: str) -> str: - """Fix the text.""" - raise ValueError("fix_text not implemented for this router.") - - def get_action_and_input(self, text: str) -> Tuple[str, str, str]: - """Return action, action input, and log (in that order).""" - input_key = self.llm_chain.input_keys[0] - inputs = {input_key: text, "stop": self.stops} - full_output = self.llm_chain.predict(**inputs) - parsed_output = self._extract_action_and_input(full_output) - while parsed_output is None: - full_output = self._fix_text(full_output) - inputs = {input_key: text + full_output, "stop": self.stops} - output = self.llm_chain.predict(**inputs) - full_output += output - parsed_output = self._extract_action_and_input(full_output) - action, action_input = parsed_output - return action, action_input, full_output diff --git a/langchain/smart_chains/router_expert.py b/langchain/smart_chains/router_expert.py deleted file mode 100644 index 408e9ad8..00000000 --- a/langchain/smart_chains/router_expert.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Router-Expert framework.""" -from typing import Callable, Dict, List, NamedTuple - -from pydantic import BaseModel, Extra - -from langchain.chains.base import Chain -from langchain.input import ChainedInput, get_color_mapping -from langchain.smart_chains.router import RouterChain - - -class ExpertConfig(NamedTuple): - """Configuration for experts.""" - - expert_name: str - expert: Callable[[str], str] - - -class RouterExpertChain(Chain, BaseModel): - """Chain that implements the Router/Expert system.""" - - router_chain: RouterChain - """Router chain.""" - expert_configs: List[ExpertConfig] - """Expert configs this chain has access to.""" - starter_string: str = "\n" - """String to put after user input but before first router.""" - input_key: str = "question" #: :meta private: - output_key: str = "answer" #: :meta private: - - class Config: - """Configuration for this pydantic object.""" - - extra = Extra.forbid - arbitrary_types_allowed = True - - @property - def input_keys(self) -> List[str]: - """Expect input key. - - :meta private: - """ - return [self.input_key] - - @property - def output_keys(self) -> List[str]: - """Expect output key. - - :meta private: - """ - return [self.output_key] - - def _call(self, inputs: Dict[str, str]) -> Dict[str, str]: - action_to_chain_map = {e.expert_name: e.expert for e in self.expert_configs} - starter_string = ( - inputs[self.input_key] - + self.starter_string - + self.router_chain.router_prefix - ) - chained_input = ChainedInput( - starter_string, - verbose=self.verbose, - ) - color_mapping = get_color_mapping( - [c.expert_name for c in self.expert_configs], excluded_colors=["green"] - ) - while True: - action, action_input, log = self.router_chain.get_action_and_input( - chained_input.input - ) - chained_input.add(log, color="green") - if action == self.router_chain.finish_action_name: - return {self.output_key: action_input} - chain = action_to_chain_map[action] - ca = chain(action_input) - chained_input.add(f"\n{self.router_chain.observation_prefix}") - chained_input.add(ca, color=color_mapping[action]) - chained_input.add(f"\n{self.router_chain.router_prefix}") diff --git a/tests/integration_tests/chains/test_react.py b/tests/integration_tests/chains/test_react.py index 3414701e..6c8c6663 100644 --- a/tests/integration_tests/chains/test_react.py +++ b/tests/integration_tests/chains/test_react.py @@ -2,7 +2,7 @@ from langchain.docstore.wikipedia import Wikipedia from langchain.llms.openai import OpenAI -from langchain.smart_chains.react.base import ReActChain +from langchain.routing_chains.react.base import ReActChain def test_react() -> None: diff --git a/tests/integration_tests/chains/test_self_ask_with_search.py b/tests/integration_tests/chains/test_self_ask_with_search.py index afc58ab8..e52e8586 100644 --- a/tests/integration_tests/chains/test_self_ask_with_search.py +++ b/tests/integration_tests/chains/test_self_ask_with_search.py @@ -1,7 +1,7 @@ """Integration test for self ask with search.""" from langchain.chains.serpapi import SerpAPIChain from langchain.llms.openai import OpenAI -from langchain.smart_chains.self_ask_with_search.base import SelfAskWithSearchChain +from langchain.routing_chains.self_ask_with_search.base import SelfAskWithSearchChain def test_self_ask_with_search() -> None: diff --git a/tests/unit_tests/routing_chains/__init__.py b/tests/unit_tests/routing_chains/__init__.py new file mode 100644 index 00000000..7976cee8 --- /dev/null +++ b/tests/unit_tests/routing_chains/__init__.py @@ -0,0 +1 @@ +"""Test routing chain functionality.""" diff --git a/tests/unit_tests/smart_chains/test_mrkl.py b/tests/unit_tests/routing_chains/test_mrkl.py similarity index 95% rename from tests/unit_tests/smart_chains/test_mrkl.py rename to tests/unit_tests/routing_chains/test_mrkl.py index fb95a04a..29d833f1 100644 --- a/tests/unit_tests/smart_chains/test_mrkl.py +++ b/tests/unit_tests/routing_chains/test_mrkl.py @@ -3,12 +3,12 @@ import pytest from langchain.prompts import PromptTemplate -from langchain.smart_chains.mrkl.base import ( +from langchain.routing_chains.mrkl.base import ( ChainConfig, MRKLRouterChain, get_action_and_input, ) -from langchain.smart_chains.mrkl.prompt import BASE_TEMPLATE +from langchain.routing_chains.mrkl.prompt import BASE_TEMPLATE from tests.unit_tests.llms.fake_llm import FakeLLM diff --git a/tests/unit_tests/smart_chains/test_react.py b/tests/unit_tests/routing_chains/test_react.py similarity index 86% rename from tests/unit_tests/smart_chains/test_react.py rename to tests/unit_tests/routing_chains/test_react.py index 31d47fb1..9b078f90 100644 --- a/tests/unit_tests/smart_chains/test_react.py +++ b/tests/unit_tests/routing_chains/test_react.py @@ -8,7 +8,7 @@ from langchain.docstore.base import Docstore from langchain.docstore.document import Document from langchain.llms.base import LLM from langchain.prompts.prompt import PromptTemplate -from langchain.smart_chains.react.base import ReActChain, ReActRouterChain +from langchain.routing_chains.react.base import ReActChain, ReActRouterChain _PAGE_CONTENT = """This is a page about LangChain. @@ -53,10 +53,10 @@ def test_predict_until_observation_normal() -> None: outputs = ["foo\nAction 1: search[foo]"] fake_llm = FakeListLLM(outputs) router_chain = ReActRouterChain(llm=fake_llm) - action, directive, ret_text = router_chain.get_action_and_input("") - assert ret_text == outputs[0] - assert action == "search" - assert directive == "foo" + output = router_chain.route("") + assert output.log == outputs[0] + assert output.tool == "search" + assert output.tool_input == "foo" def test_predict_until_observation_repeat() -> None: @@ -64,10 +64,10 @@ def test_predict_until_observation_repeat() -> None: outputs = ["foo", " search[foo]"] fake_llm = FakeListLLM(outputs) router_chain = ReActRouterChain(llm=fake_llm) - action, directive, ret_text = router_chain.get_action_and_input("") - assert ret_text == "foo\nAction 1: search[foo]" - assert action == "search" - assert directive == "foo" + output = router_chain.route("") + assert output.log == "foo\nAction 1: search[foo]" + assert output.tool == "search" + assert output.tool_input == "foo" def test_react_chain() -> None: diff --git a/tests/unit_tests/smart_chains/__init__.py b/tests/unit_tests/smart_chains/__init__.py deleted file mode 100644 index 4b80a982..00000000 --- a/tests/unit_tests/smart_chains/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Test smart chain functionality."""