From 09c0823c3aaf29a46d74e2622dd4da0fadc3edf9 Mon Sep 17 00:00:00 2001 From: ccurme Date: Mon, 19 Aug 2024 09:29:25 -0400 Subject: [PATCH] docs: update summarization guides (#25408) --- docs/docs/how_to/index.mdx | 9 + docs/docs/how_to/summarize_map_reduce.ipynb | 449 ++++++++++++++ docs/docs/how_to/summarize_refine.ipynb | 333 ++++++++++ docs/docs/how_to/summarize_stuff.ipynb | 209 +++++++ docs/docs/tutorials/summarization.ipynb | 634 +++++++++++--------- 5 files changed, 1344 insertions(+), 290 deletions(-) create mode 100644 docs/docs/how_to/summarize_map_reduce.ipynb create mode 100644 docs/docs/how_to/summarize_refine.ipynb create mode 100644 docs/docs/how_to/summarize_stuff.ipynb diff --git a/docs/docs/how_to/index.mdx b/docs/docs/how_to/index.mdx index de9000095f..05f90a7d24 100644 --- a/docs/docs/how_to/index.mdx +++ b/docs/docs/how_to/index.mdx @@ -315,6 +315,15 @@ For a high-level tutorial, check out [this guide](/docs/tutorials/graph/). - [How to: improve results with prompting](/docs/how_to/graph_prompting) - [How to: construct knowledge graphs](/docs/how_to/graph_constructing) +### Summarization + +LLMs can summarize and otherwise distill desired information from text, including +large volumes of text. For a high-level tutorial, check out [this guide](/docs/tutorials/summarization). + +- [How to: summarize text in a single LLM call](/docs/how_to/summarize_stuff) +- [How to: summarize text through parallelization](/docs/how_to/summarize_map_reduce) +- [How to: summarize text through iterative refinement](/docs/how_to/summarize_refine) + ## [LangGraph](https://langchain-ai.github.io/langgraph) LangGraph is an extension of LangChain aimed at diff --git a/docs/docs/how_to/summarize_map_reduce.ipynb b/docs/docs/how_to/summarize_map_reduce.ipynb new file mode 100644 index 0000000000..71ffa176e0 --- /dev/null +++ b/docs/docs/how_to/summarize_map_reduce.ipynb @@ -0,0 +1,449 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c47f5b2f-e14c-43e7-a0ab-d71562636624", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 3\n", + "keywords: [summarize, summarization, map reduce]\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "682a4f53-27db-43ef-a909-dd9ded76051b", + "metadata": {}, + "source": [ + "# How to summarize text through parallelization\n", + "\n", + "LLMs can summarize and otherwise distill desired information from text, including large volumes of text. In many cases, especially when the amount of text is large compared to the size of the model's context window, it can be helpful (or necessary) to break up the summarization task into smaller components.\n", + "\n", + "Map-reduce represents one class of strategies for accomplishing this. The idea is to break the text into \"sub-documents\", and first map each sub-document to an individual summary using an LLM. Then, we reduce or consolidate those summaries into a single global summary.\n", + "\n", + "Note that the map step is typically parallelized over the input documents. This strategy is especially effective when understanding of a sub-document does not rely on preceeding context. For example, when summarizing a corpus of many, shorter documents.\n", + "\n", + "[LangGraph](https://langchain-ai.github.io/langgraph/), built on top of `langchain-core`, suports [map-reduce](https://langchain-ai.github.io/langgraph/how-tos/map-reduce/) workflows and is well-suited to this problem:\n", + "\n", + "- LangGraph allows for individual steps (such as successive summarizations) to be streamed, allowing for greater control of execution;\n", + "- LangGraph's [checkpointing](https://langchain-ai.github.io/langgraph/how-tos/persistence/) supports error recovery, extending with human-in-the-loop workflows, and easier incorporation into conversational applications.\n", + "- The LangGraph implementation is straightforward to modify and extend.\n", + "\n", + "Below, we demonstrate how to summarize text via a map-reduce strategy." + ] + }, + { + "cell_type": "markdown", + "id": "4aa52e84-d1b5-4b33-b4c4-541156686ef3", + "metadata": {}, + "source": [ + "## Load chat model\n", + "\n", + "Let's first load a chat model:\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e5f426fc-cea6-4351-8931-1e422d3c8b69", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)" + ] + }, + { + "cell_type": "markdown", + "id": "b137fe82-0a53-4910-b53e-b87a297f329d", + "metadata": {}, + "source": [ + "## Load documents\n", + "\n", + "First we load in our documents. We will use [WebBaseLoader](https://api.python.langchain.com/en/latest/document_loaders/langchain_community.document_loaders.web_base.WebBaseLoader.html) to load a blog post, and split the documents into smaller sub-documents." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "27c8fed0-b2d7-4549-a086-f5ee657efc41", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Created a chunk of size 1003, which is longer than the specified 1000\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generated 14 documents.\n" + ] + } + ], + "source": [ + "from langchain_community.document_loaders import WebBaseLoader\n", + "from langchain_text_splitters import CharacterTextSplitter\n", + "\n", + "text_splitter = CharacterTextSplitter.from_tiktoken_encoder(\n", + " chunk_size=1000, chunk_overlap=0\n", + ")\n", + "\n", + "loader = WebBaseLoader(\"https://lilianweng.github.io/posts/2023-06-23-agent/\")\n", + "docs = loader.load()\n", + "\n", + "split_docs = text_splitter.split_documents(docs)\n", + "print(f\"Generated {len(split_docs)} documents.\")" + ] + }, + { + "cell_type": "markdown", + "id": "84216044-6f1e-4b90-b4fa-29ec305abf51", + "metadata": {}, + "source": [ + "## Create graph\n", + "\n", + "### Map step\n", + "Let's first define the prompt associated with the map step, and associated it with the LLM via a [chain](/docs/how_to/sequence/):" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "669afa40-2708-4fa1-841e-c74a67bd9175", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "\n", + "map_prompt = ChatPromptTemplate.from_messages(\n", + " [(\"human\", \"Write a concise summary of the following:\\\\n\\\\n{context}\")]\n", + ")\n", + "\n", + "map_chain = map_prompt | llm | StrOutputParser()" + ] + }, + { + "cell_type": "markdown", + "id": "81597ed0-8df5-4cbc-a242-3140a168a7f4", + "metadata": {}, + "source": [ + "### Reduce step\n", + "\n", + "We also define a chain that takes the document mapping results and reduces them into a single output." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "de59caae-8fb2-4cf4-aea0-be78a081a695", + "metadata": {}, + "outputs": [], + "source": [ + "reduce_template = \"\"\"\n", + "The following is a set of summaries:\n", + "{docs}\n", + "Take these and distill it into a final, consolidated summary\n", + "of the main themes.\n", + "\"\"\"\n", + "\n", + "reduce_prompt = ChatPromptTemplate([(\"human\", reduce_template)])\n", + "\n", + "reduce_chain = reduce_prompt | llm | StrOutputParser()" + ] + }, + { + "cell_type": "markdown", + "id": "cb264a71-12f5-44ef-ad2e-d38c4bf71bbd", + "metadata": {}, + "source": [ + "### Orchestration via LangGraph\n", + "\n", + "Below we implement a simple application that maps the summarization step on a list of documents, then reduces them using the above prompts.\n", + "\n", + "Map-reduce flows are particularly useful when texts are long compared to the context window of a LLM. For long texts, we need a mechanism that ensures that the context to be summarized in the reduce step does not exceed a model's context window size. Here we implement a recursive \"collapsing\" of the summaries: the inputs are partitioned based on a token limit, and summaries are generated of the partitions. This step is repeated until the total length of the summaries is within a desired limit, allowing for the summarization of arbitrary-length text.\n", + "\n", + "We will need to install `langgraph`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6dc8cf11-c0e5-4448-a921-9377acad1df0", + "metadata": {}, + "outputs": [], + "source": [ + "pip install -qU langgraph" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "dafedc2e-feeb-44bc-9f38-e55394953de5", + "metadata": {}, + "outputs": [], + "source": [ + "import operator\n", + "from typing import Annotated, List, Literal, TypedDict\n", + "\n", + "from langchain.chains.combine_documents.reduce import (\n", + " acollapse_docs,\n", + " split_list_of_docs,\n", + ")\n", + "from langchain_core.documents import Document\n", + "from langgraph.constants import Send\n", + "from langgraph.graph import END, START, StateGraph\n", + "\n", + "token_max = 1000\n", + "\n", + "\n", + "def length_function(documents: List[Document]) -> int:\n", + " \"\"\"Get number of tokens for input contents.\"\"\"\n", + " return sum(llm.get_num_tokens(doc.page_content) for doc in documents)\n", + "\n", + "\n", + "# This will be the overall state of the main graph.\n", + "# It will contain the input document contents, corresponding\n", + "# summaries, and a final summary.\n", + "class OverallState(TypedDict):\n", + " # Notice here we use the operator.add\n", + " # This is because we want combine all the summaries we generate\n", + " # from individual nodes back into one list - this is essentially\n", + " # the \"reduce\" part\n", + " contents: List[str]\n", + " summaries: Annotated[list, operator.add]\n", + " collapsed_summaries: List[Document]\n", + " final_summary: str\n", + "\n", + "\n", + "# This will be the state of the node that we will \"map\" all\n", + "# documents to in order to generate summaries\n", + "class SummaryState(TypedDict):\n", + " content: str\n", + "\n", + "\n", + "# Here we generate a summary, given a document\n", + "async def generate_summary(state: SummaryState):\n", + " response = await map_chain.ainvoke(state[\"content\"])\n", + " return {\"summaries\": [response]}\n", + "\n", + "\n", + "# Here we define the logic to map out over the documents\n", + "# We will use this an edge in the graph\n", + "def map_summaries(state: OverallState):\n", + " # We will return a list of `Send` objects\n", + " # Each `Send` object consists of the name of a node in the graph\n", + " # as well as the state to send to that node\n", + " return [\n", + " Send(\"generate_summary\", {\"content\": content}) for content in state[\"contents\"]\n", + " ]\n", + "\n", + "\n", + "def collect_summaries(state: OverallState):\n", + " return {\n", + " \"collapsed_summaries\": [Document(summary) for summary in state[\"summaries\"]]\n", + " }\n", + "\n", + "\n", + "# Add node to collapse summaries\n", + "async def collapse_summaries(state: OverallState):\n", + " doc_lists = split_list_of_docs(\n", + " state[\"collapsed_summaries\"], length_function, token_max\n", + " )\n", + " results = []\n", + " for doc_list in doc_lists:\n", + " results.append(await acollapse_docs(doc_list, reduce_chain.ainvoke))\n", + "\n", + " return {\"collapsed_summaries\": results}\n", + "\n", + "\n", + "# This represents a conditional edge in the graph that determines\n", + "# if we should collapse the summaries or not\n", + "def should_collapse(\n", + " state: OverallState,\n", + ") -> Literal[\"collapse_summaries\", \"generate_final_summary\"]:\n", + " num_tokens = length_function(state[\"collapsed_summaries\"])\n", + " if num_tokens > token_max:\n", + " return \"collapse_summaries\"\n", + " else:\n", + " return \"generate_final_summary\"\n", + "\n", + "\n", + "# Here we will generate the final summary\n", + "async def generate_final_summary(state: OverallState):\n", + " response = await reduce_chain.ainvoke(state[\"collapsed_summaries\"])\n", + " return {\"final_summary\": response}\n", + "\n", + "\n", + "# Construct the graph\n", + "# Nodes:\n", + "graph = StateGraph(OverallState)\n", + "graph.add_node(\"generate_summary\", generate_summary) # same as before\n", + "graph.add_node(\"collect_summaries\", collect_summaries)\n", + "graph.add_node(\"collapse_summaries\", collapse_summaries)\n", + "graph.add_node(\"generate_final_summary\", generate_final_summary)\n", + "\n", + "# Edges:\n", + "graph.add_conditional_edges(START, map_summaries, [\"generate_summary\"])\n", + "graph.add_edge(\"generate_summary\", \"collect_summaries\")\n", + "graph.add_conditional_edges(\"collect_summaries\", should_collapse)\n", + "graph.add_conditional_edges(\"collapse_summaries\", should_collapse)\n", + "graph.add_edge(\"generate_final_summary\", END)\n", + "\n", + "app = graph.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "c2de9413-fa18-4807-9c1f-85a62a8eb7ab", + "metadata": {}, + "source": [ + "LangGraph allows the graph structure to be plotted to help visualize its function:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "4f26c1e3-3d3c-44f7-bb5f-46db9dc40f4b", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import Image\n", + "\n", + "Image(app.get_graph().draw_mermaid_png())" + ] + }, + { + "cell_type": "markdown", + "id": "74f3e276-f003-4112-ba14-c6952076c4f8", + "metadata": {}, + "source": [ + "## Invoke graph\n", + "\n", + "When running the application, we can stream the graph to observe its sequence of steps. Below, we will simply print out the name of the step.\n", + "\n", + "Note that because we have a loop in the graph, it can be helpful to specify a [recursion_limit](https://langchain-ai.github.io/langgraph/reference/errors/#langgraph.errors.GraphRecursionError) on its execution. This will raise a specific error when the specified limit is exceeded." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "0701bb7d-fbc6-497e-a577-25d56e6e43c6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['collect_summaries']\n", + "['collapse_summaries']\n", + "['collapse_summaries']\n", + "['generate_final_summary']\n" + ] + } + ], + "source": [ + "async for step in app.astream(\n", + " {\"contents\": [doc.page_content for doc in split_docs]},\n", + " {\"recursion_limit\": 10},\n", + "):\n", + " print(list(step.keys()))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "0dc27458-7b37-4a2b-9452-b59274a55828", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'generate_final_summary': {'final_summary': 'The consolidated summary of the main themes from the provided documents highlights the advancements and applications of large language models (LLMs) in artificial intelligence, particularly in autonomous agents and software development. Key themes include:\\n\\n1. **Integration of LLMs**: LLMs play a crucial role in enabling autonomous agents to perform complex tasks through advanced reasoning and decision-making techniques, such as Chain of Thought (CoT) and Tree of Thoughts.\\n\\n2. **Memory Management**: The categorization of memory into sensory, short-term, and long-term types parallels machine learning concepts, with short-term memory facilitating in-context learning and long-term memory enhanced by external storage solutions.\\n\\n3. **Tool Use and APIs**: Autonomous agents utilize external APIs to expand their capabilities, demonstrating adaptability and improved problem-solving skills.\\n\\n4. **Search Algorithms**: Various approximate nearest neighbor search algorithms, including Locality-Sensitive Hashing (LSH) and FAISS, are discussed for enhancing search efficiency in high-dimensional spaces.\\n\\n5. **Neuro-Symbolic Architectures**: The integration of neuro-symbolic systems, such as the MRKL framework, combines expert modules with LLMs to improve problem-solving, particularly in complex tasks.\\n\\n6. **Challenges and Innovations**: The documents address challenges like hallucination and inefficient planning in LLMs, alongside innovative methods such as Chain of Hindsight (CoH) and Algorithm Distillation (AD) for performance enhancement.\\n\\n7. **Software Development Practices**: The use of LLMs in software development is explored, particularly in creating structured applications like a Super Mario game using the model-view-controller (MVC) architecture, emphasizing task management, component organization, and documentation.\\n\\n8. **Limitations of LLMs**: Constraints such as finite context length and challenges in long-term planning are acknowledged, along with concerns regarding the reliability of natural language as an interface.\\n\\nOverall, the integration of LLMs and neuro-symbolic architectures signifies a significant evolution in AI, with ongoing research focused on enhancing planning, memory management, and problem-solving capabilities across various applications.'}}\n" + ] + } + ], + "source": [ + "print(step)" + ] + }, + { + "cell_type": "markdown", + "id": "f15c225a-db1d-48cf-b135-f588e7d615e6", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "Check out the [LangGraph documentation](https://langchain-ai.github.io/langgraph/) for detail on building with LangGraph, including [this guide](https://langchain-ai.github.io/langgraph/how-tos/map-reduce/) on the details of map-reduce in LangGraph.\n", + "\n", + "See the summarization [how-to guides](/docs/how_to/#summarization) for additional summarization strategies, including those designed for larger volumes of text.\n", + "\n", + "See also [this tutorial](/docs/tutorials/summarization) for more detail on summarization." + ] + } + ], + "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.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/how_to/summarize_refine.ipynb b/docs/docs/how_to/summarize_refine.ipynb new file mode 100644 index 0000000000..4364785217 --- /dev/null +++ b/docs/docs/how_to/summarize_refine.ipynb @@ -0,0 +1,333 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c47f5b2f-e14c-43e7-a0ab-d71562636624", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 3\n", + "keywords: [summarize, summarization, refine]\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "682a4f53-27db-43ef-a909-dd9ded76051b", + "metadata": {}, + "source": [ + "# How to summarize text through iterative refinement\n", + "\n", + "LLMs can summarize and otherwise distill desired information from text, including large volumes of text. In many cases, especially when the amount of text is large compared to the size of the model's context window, it can be helpful (or necessary) to break up the summarization task into smaller components.\n", + "\n", + "Iterative refinement represents one strategy for summarizing long texts. The strategy is as follows:\n", + "\n", + "- Split a text into smaller documents;\n", + "- Summarize the first document;\n", + "- Refine or update the result based on the next document;\n", + "- Repeat through the sequence of documents until finished.\n", + "\n", + "Note that this strategy is not parallelized. It is especially effective when understanding of a sub-document depends on prior context-- for instance, when summarizing a novel or body of text with an inherent sequence.\n", + "\n", + "[LangGraph](https://langchain-ai.github.io/langgraph/), built on top of `langchain-core`, is well-suited to this problem:\n", + "\n", + "- LangGraph allows for individual steps (such as successive summarizations) to be streamed, allowing for greater control of execution;\n", + "- LangGraph's [checkpointing](https://langchain-ai.github.io/langgraph/how-tos/persistence/) supports error recovery, extending with human-in-the-loop workflows, and easier incorporation into conversational applications.\n", + "- Because it is assembled from modular components, it is also simple to extend or modify (e.g., to incorporate [tool calling](/docs/concepts/#functiontool-calling) or other behavior).\n", + "\n", + "Below, we demonstrate how to summarize text via iterative refinement." + ] + }, + { + "cell_type": "markdown", + "id": "4aa52e84-d1b5-4b33-b4c4-541156686ef3", + "metadata": {}, + "source": [ + "## Load chat model\n", + "\n", + "Let's first load a chat model:\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e5f426fc-cea6-4351-8931-1e422d3c8b69", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)" + ] + }, + { + "cell_type": "markdown", + "id": "b137fe82-0a53-4910-b53e-b87a297f329d", + "metadata": {}, + "source": [ + "## Load documents" + ] + }, + { + "cell_type": "markdown", + "id": "a81dc91d-ae72-4996-b809-d4a9050e815e", + "metadata": {}, + "source": [ + "Next, we need some documents to summarize. Below, we generate some toy documents for illustrative purposes. See the document loader [how-to guides](/docs/how_to/#document-loaders) and [integration pages](/docs/integrations/document_loaders/) for additional sources of data. The [summarization tutorial](/docs/tutorials/summarization) also includes an example summarizing a blog post." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "27c8fed0-b2d7-4549-a086-f5ee657efc41", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.documents import Document\n", + "\n", + "documents = [\n", + " Document(page_content=\"Apples are red\", metadata={\"title\": \"apple_book\"}),\n", + " Document(page_content=\"Blueberries are blue\", metadata={\"title\": \"blueberry_book\"}),\n", + " Document(page_content=\"Bananas are yelow\", metadata={\"title\": \"banana_book\"}),\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "84216044-6f1e-4b90-b4fa-29ec305abf51", + "metadata": {}, + "source": [ + "## Create graph\n", + "\n", + "Below we show a LangGraph implementation of this process:\n", + "\n", + "- We generate a simple chain for the initial summary that plucks out the first document, formats it into a prompt and runs inference with our LLM.\n", + "- We generate a second `refine_summary_chain` that operates on each successive document, refining the initial summary.\n", + "\n", + "We will need to install `langgraph`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bf7acdb7-19ca-43ba-98f4-91f5b804da21", + "metadata": {}, + "outputs": [], + "source": [ + "pip install -qU langgraph" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "669afa40-2708-4fa1-841e-c74a67bd9175", + "metadata": {}, + "outputs": [], + "source": [ + "import operator\n", + "from typing import List, Literal, TypedDict\n", + "\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.runnables import RunnableConfig\n", + "from langgraph.constants import Send\n", + "from langgraph.graph import END, START, StateGraph\n", + "\n", + "# Initial summary\n", + "summarize_prompt = ChatPromptTemplate(\n", + " [\n", + " (\"human\", \"Write a concise summary of the following: {context}\"),\n", + " ]\n", + ")\n", + "initial_summary_chain = summarize_prompt | llm | StrOutputParser()\n", + "\n", + "# Refining the summary with new docs\n", + "refine_template = \"\"\"\n", + "Produce a final summary.\n", + "\n", + "Existing summary up to this point:\n", + "{existing_answer}\n", + "\n", + "New context:\n", + "------------\n", + "{context}\n", + "------------\n", + "\n", + "Given the new context, refine the original summary.\n", + "\"\"\"\n", + "refine_prompt = ChatPromptTemplate([(\"human\", refine_template)])\n", + "\n", + "refine_summary_chain = refine_prompt | llm | StrOutputParser()\n", + "\n", + "\n", + "# We will define the state of the graph to hold the document\n", + "# contents and summary. We also include an index to keep track\n", + "# of our position in the sequence of documents.\n", + "class State(TypedDict):\n", + " contents: List[str]\n", + " index: int\n", + " summary: str\n", + "\n", + "\n", + "# We define functions for each node, including a node that generates\n", + "# the initial summary:\n", + "async def generate_initial_summary(state: State, config: RunnableConfig):\n", + " summary = await initial_summary_chain.ainvoke(\n", + " state[\"contents\"][0],\n", + " config,\n", + " )\n", + " return {\"summary\": summary, \"index\": 1}\n", + "\n", + "\n", + "# And a node that refines the summary based on the next document\n", + "async def refine_summary(state: State, config: RunnableConfig):\n", + " content = state[\"contents\"][state[\"index\"]]\n", + " summary = await refine_summary_chain.ainvoke(\n", + " {\"existing_answer\": state[\"summary\"], \"context\": content},\n", + " config,\n", + " )\n", + "\n", + " return {\"summary\": summary, \"index\": state[\"index\"] + 1}\n", + "\n", + "\n", + "# Here we implement logic to either exit the application or refine\n", + "# the summary.\n", + "def should_refine(state: State) -> Literal[\"refine_summary\", END]:\n", + " if state[\"index\"] >= len(state[\"contents\"]):\n", + " return END\n", + " else:\n", + " return \"refine_summary\"\n", + "\n", + "\n", + "graph = StateGraph(State)\n", + "graph.add_node(\"generate_initial_summary\", generate_initial_summary)\n", + "graph.add_node(\"refine_summary\", refine_summary)\n", + "\n", + "graph.add_edge(START, \"generate_initial_summary\")\n", + "graph.add_conditional_edges(\"generate_initial_summary\", should_refine)\n", + "graph.add_conditional_edges(\"refine_summary\", should_refine)\n", + "app = graph.compile()" + ] + }, + { + "cell_type": "markdown", + "id": "cdc11401-8640-4cf8-a713-4031df690cf7", + "metadata": {}, + "source": [ + "LangGraph allows the graph structure to be plotted to help visualize its function:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "21711ff5-4e06-4843-9109-e7d89e679449", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAEvAQsDASIAAhEBAxEB/8QAHQABAAICAwEBAAAAAAAAAAAAAAYHBQgBAwQCCf/EAFYQAAEDBAADAggICAgMBQUAAAEAAgMEBQYRBxIhEzEIFBUWIkGU0xcyUVRVVmGVI1NxkpPR0tQzQlJ0dYKxsgkkJTY3OHJzgZGztDQ1Q2JjZIOho9X/xAAbAQEBAAMBAQEAAAAAAAAAAAAAAQIDBAYFB//EADQRAQABAgMFBgMIAwEAAAAAAAABAhEDElEEExQxkSFBUmGh0QUVUyMzcYGxweHwIjLxQv/aAAwDAQACEQMRAD8A/VNERAREQEREBERAXVUVUNJGZJ5o4Ix/HkcGj/mVhblcay518tqtEniz4eXxu4OjD20+xsMYD0dKQQdHYaCC4HYa7rpuHthjk7aqoGXWsI06ruf+Myn1nTn75Rv+K3QGhoDQW+KKaYviTbyhbavecqsoOjeKDf8AOWfrXHnVZPpig9qZ+tcnF7MTs2ig3/NmfqTzWsv0RQezM/Ur9j5+i9jjzqsn0xQe1M/WnnVZPpig9qZ+tc+a1l+iKD2Zn6k81rL9EUHszP1J9j5+h2OPOqyfTFB7Uz9aedVk+mKD2pn61z5rWX6IoPZmfqTzWsv0RQezM/Un2Pn6HY486rJ9MUHtTP1r10dzo7hvxWrgqddT2MjX/wBhXl81rL9EUHszP1Ly1mCY7XaMtkoRICC2WKBscjSO4te3TgftBT7Ge+fT+E7GeRRcvq8L5XVFVPcrDsNdLUHnqKLZ1zPf3yRfK47czW3FzSSyULXXRl7Ym8STAiItaCIiAiIgIiICIiAiIgIiICIiAiIgIiIC8V6ukdks9fcZgTDRwSVDwPW1jS4/2L2rEZfan37E73bI/wCErKGenbv5XxuaP7VnhxTNcRVyusc3XhlsktWN0UdQWurpWeMVcjd/hJ3+nI7r11zE6HqGh6lm1j8eujL3YbdcI9hlVTxzAOGiOZoOiPURvRHqWKyriZh+C1MNNkmV2PHqidnaRRXW4w0z5G71zND3Akb6bCuJNU11TVzuTzSVQribxWtvC+Gztqrdc71cbxWeI2+12eFstTUyhjpHaD3saAGscSXOHcvKfCC4XBgeeJOIBhJAd5dpdEjWx/CfaP8AmofxSyPF+MmMR2/GLVa+LwpauOaogx/I6WCrth5X9lUxSiQcj+YaBD2nRd1OiDrR05r4QV9sPEPhvabdgt/rbfkdBW1tVSGmgjrWuiazliaJKhga5nMXSB3qczlJPMBJs54+W7h3f5aO84xk8VmglghqMmjt7XWyB0paGl0nPzloL2guawgHYJ6FVvTYPxUx+g4M5NcrY7N8nxmC5Ul3omXGGOpdHVNaIndtIWxyPjbFG152C47I2oZxn4FZzxBPEaOowOLJ75d5mVFgyGvvELYLVStjiIo44nOLo5Q9kreZrQ15k254CDYCt46W+LibccEoMcyC9Xu3NpJKp9BTwmnhiqN8srpHytAa3XpD43eWtcA7WF4Acar7xWrcsp7xidys0dsvNdRwVkrIG07Y4ZRG2B/LO95nAJLiG8mwdO7gslw+xO9W/jVxIyavtrqC2XyiszKN8k0T3OfDFOJmEMcSCwyNGz0O/RJCjfDuounBO951S5fQ0dnw+vyGuvdNl1XdqaGk5aqRr2QPY94e2QOLm93KdDR6oL2RQBvhB8LXnTeJWIOOidC/UvcOp/8AUXtsfGfh9k91p7ZZs6xq7XKoJENHQ3enmmkIBJDWNeSdAE9B3AoJfLEyeJ8UrGyRvaWuY8bDge8EesKO4DK9lmntsjzI+01UtAHOJJMbDuLZPUnsnR7J7zsqSqMYKO3ZfbgN9nXXWeSPY1tsYbBv8h7EkH1gg+tdFH3VV/Lr/wAuscknREXOgiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgiolbgtTUGfTMdqJXTifrqikeS5/P6hE5xLubuaXHehoiRGGmrmRylkVQxzQWP0HAg9QQfkXoUZk4fWuOR8lvfWWRzyS5tsqXwxkk7J7IHs9k9d8u/tXRmoxO2ubTrzv+P97V582d8m0nzWH9GP1LshpoaffZRMi338jQNqOOwmcknzovw36hPF0/8A1rjzIn+tN+/Txe6Td4fj9JW0apSii3mRP9ab9+ni90qm4iXnIMX4/wDCPDaPJ7qbRlLLu6vMr4zKDTUzZIuR3IA30id7B2PkTd4fj9JLRq2CXxLEyZhZIxsjT/FcNhRnzIn+tN+/Txe6TzIn+tN+/Txe6Td4fj9JLRqkHk2k+aw/ox+pfUdDTRPD2U8THDuc1gBCjvmRP9ab9+ni90vrzCpp+lddbxco+m4p657GO18rY+UEfYdg/ImTDjnX6f8AEtGrsut5kvE81nsswdVD0KutYdsom9xGx0M2vis9XRzumg7NW6309pt9NRUkQhpaaNsUUbe5rWjQH/ILmgt9La6OKkoqaGkpYm8scEDAxjB8gaOgC9CwrriYy08v1/voCIi1IIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgLXfjP8A64Pg5/7vI/8AsWLYha78Z/8AXB8HP/d5H/2LEGxCIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAtd+M/8Arg+Dn/u8j/7Fi2IWu/Gf/XB8HP8A3eR/9ixBsQiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIuHODGlziGtA2Se4KFuy+93YCosttofJr+sNRcKh7HzN9TxG1h5WnvGzsjvAW7DwqsW+VYi6aooR5czD5jY/apvdp5czD5jY/apvdrdwtesdYLJuihHlzMPmNj9qm92nlzMPmNj9qm92nC16x1gsm6KEeXMw+Y2P2qb3aeXMw+Y2P2qb3acLXrHWCybooR5czD5jY/apvdp5czD5jY/apvdpwtesdYLJuihHlzMPmNj9qm92nlzMPmNj9qm92nC16x1gskmT47Q5fjd2sVzjM1tulJLQ1UbXcpdFIwseN+rbXFfhLxc4W3ThHxPvuF3GN0lbbqswxua3/AMRGesUjR8j2FrgPt13r9tvLmYfMbH7VN7tU5xF8HmXiVxpw7iRcqGzNuePD0qZs8pjrCwl0Bf8Ag+hjeS4a7+gPQJwtesdYLJr4JHBQcBuB1jx6dgbeKgG43U//AFUobzN/qNayPfr7PfrVyqEeXMw+Y2P2qb3aeXMw+Y2P2qb3acLXrHWCybooR5czD5jY/apvdp5czD5jY/apvdpwtesdYLJuihHlzMPmNj9qm92nlzMPmNj9qm92nC16x1gsm6KEeXMw+Y2P2qb3aeXMw+Y2P2qb3acLXrHWCybooR5czD5jY/apvdp5czD5jY/apvdpwtesdYLJuihHlzMPmNj9qm92vpuQZbCeeS12ioYOpjhrZGPI/wDaXR639h0PtCcLXrHWCyaovFZrvT323RVtNzCN+2lkjeV7HNJa5jh6iHAgj5QvauSYmmbTzQREUBERAREQYvKCW4zdyDoijmII/wBgqPYyAMbtIAAApIug/wBgKQ5V/mxeP5nN/cKj2M/5uWr+aRf3Avo4P3M/j+zLuZJFqZiXGDM8qv3D+4+e0ck9+yWpt9ywako6YS22mh7ffM4tMw5OxZ2hf39oOXl6E8Yhxe4v8QaC35pj9ovlXbq6u5oLN4jbG2t1EJzG4GodUiqEojDnc/KBzjXZ6WOeGLbRFq1kfEriFb8U4nZtBloZR4dktRRU9k8m05iqqWOSIuZLIW8++WQhpYWkaBJdterLOJ/EnLs/zSgw2C+09BjVU23QNtFuttRFU1HYskcal1XUMkDdyAARAeiN8xJ0LmgbNotfrXkXEnN+LNJjtVfHYPDHh9uvFyoKOkpqmWGvknmZLGySRrxyeho75ujG8uiSTHMl40ZVaOI1JcLJfrrkOJOyqnsNZE+yUsNrhEs4gfHHU84qJJY3O+OA5hc0g6TMNomyMc9zA5pc3XM0HqN9219LW7hxFc8T4m8d8lqcluNdbrTcjVT2nxemDKkC3QyN24Rc4LG6Y3lcAQwF3MSScfwz4h8Y8oqcNyJ1tvVfaL5LTz19HU0NshtlLRztDjJTyx1JqT2Yc1w7Rri8A7a0nQZhtCvJQXiguktZFRVtNVy0c3i9SyCVr3QS8odyPAPou05p0euiD6161qjT5pmNgo8hobdkEDLrPxThx511ktNKHvppaWAkyMjYxsjxz9Hn0jytBcQNKzNhtLcrlR2a31FfcKqChoaaMyz1NTII4omAbLnOcQGgDvJXbFUwzCMxyskErO0YWuB529PSHyjqOv2hawcQcqzG3YTx0xusyg3SoxW2U1zo7nVWuje+ohmgle6mnhMRhe3cLhvsweV3y9VlWWK73XwqMfqKXKK21RDCYqp1NS0tKY3xNq4g+n9OIkMeepIIcO5rmjopmGwdHeKC41VbTUlbTVVTRSCKqhhla98Dy0ODXgHbSWuB0ddCD61xd7zb7Bb5K+6V1NbaGItD6mrmbFGwucGt25xAG3EAfKSAtcYc2uWK03F6KryqWiucWVUtut1xorFSz11Q+Wmp3sp2QsYxs0pD3RtdJzEAAuJDSonlefZTkvBHizj+WGtmrsfudoZDU3Okp6WrfDNPTStE0dO50XMDzaLD1BbsA7CmYbjIiofivm2VYrxZt7bhkdThuASU9M2nulPaYqylnq3TESQ1krgXU4LezDHDlbtx27Y0s5mwvhFr9cuKWUU/Cvj7eI7ny3HF7pcae0TeLxHxaOKjgkjHLy6fp73Hbw4nejsaCwuS5xxBulXxVmteZuslNiFiortSU8Vsppu3lfRPmeyRz2E9mXRno3TgX9HAABY5oGzaLXG38Qc5x27Ysbpkzb3T5didwvDYDb4YG22qghglHYlo26MifXLIXn0Qeb1LDY7lPE67jgy+XiI9reIFtfNWtZZqT/EnNoxUh1P6HxjotPac7fSJDR0aGYbTotaBxYySTA6u01mWV0GW0mXVmO0tVZbLBVV93bAHOHJA/UMbuQtc95AY0MPdzBYul4zcRLjwupI23DyZldPxBp8Tlra+3wB8sL5GdZ4WOdGHcsoDhE4fF9Fw3tM0DatFSN4ze9cE84tsWX5TU5BilxstdKK2spaaGSGtpeaocNwxsGn0xeADvrT/ACuO51warMiufDDHrhlc/bX+vpvHalvZtj7HtXGRkOmgD8GxzY962eTZJJJVib9glPDY/wCTbuPULtV6H/3NqXKIcNf/AC68f0vV/wB9S9aNp++qWeYiIuVBERAREQYvKv8ANi8fzOb+4VHsZ/zctX80i/uBS6tpI6+jnppd9lNG6N2u/RGj/aq/pblU4tRU1suVruUs1LG2EVNDRSVMU4aAA8dm0lu9dWuAIOx1Gifo7P8A5Yc0RzuyjtizXzFOF/EbHOKsdwslsvFlpqi8me6V12utsraOqoTKXSNbyQCrc9zdcvO70ToFxAVsWDgBasUyDx6yZFktqtPjzrj5t0twDbaJnO536Zyc4Y5xLjGHhhJPoqYeedP9FX77kq/dp550/wBFX77kq/drbGz1x/5kyzoi9y4EWC6YXm2MS1lybQZbcJrlXSMljEsckvJzCIlmg38G3QcHHqepXTk/AS1X7K7lkNvyHI8Tr7rHHHcxj9e2nZX8jeVjpA5jiHhvoh7C12vWpd550/0VfvuSr92nnnT/AEVfvuSr92ruK/CZZ0dFFgFvoc/q8vZPVvudTa4LQ+OR7TF2UUkkjXAcvNzkyu2S4jQHQdSYFcfBfx64OqIm5BktJbHXTy1TWqmrmNpaKt7btzNE0xkncnM7kkL2AuJDQdEWJ550/wBFX77kq/dp550/0VfvuSr92m4r8MmWdGCbwdtkHES45dS3S7Uj7oGeU7PFOw2+vc2EwtfLG5hOwzQ9FzQeVuwdLFYdwJpOG9VBNYMiyWW228Sut+M1d0/ybCXNcBH0jMhYOY6D3PDehA2Apl550/0VfvuSr92nnnT/AEVfvuSr92m4r8MmWdEbGRcUtjeDYyB6yMrm/wD5665eBFgmqaic1lyD58qiy9wEsehVxxxxtYPQ/gtRt2PjbJ9JZXJOLWP4dZp7tfvKNltcHL2tbcLZUQQx8zg1u3uYANkgDr1JAXrt/EO23agp62io7zV0dTG2aGogs9U+ORjhtrmuEeiCCCCE3GJ30ymWWFyLgnY8mkz59VV3CM5pboLZcOxkjHZRRMlY0w7YeVxEztl3MNgdB6/rJuDNtyG/2K+U95vVgvFopDb2VlpqGRvqKYua4wyh7HBzeZjT0AIPcQpB550/0VfvuSr92nnnT/RV++5Kv3abivwyuWdESvnADH76b/K+vu1JW3a809/bW0s7GTUVZDEyKN8B5CAOVnUPDweZ3qOh4o/Brxt9szCir7rfru3LIYGXSeurQ+WSWEns52ODByPHogBumARs00a6zrzzp/oq/fclX7tPPOn+ir99yVfu03FfhMs6I46p4h46yG12vHrVkNBRxRwRXW8ZLJDWVQawAyTMZQuaHkgk6Oj39N6GJyPgzNxbhZUZncbvaIqhkcVfi9ovPb2uoZHKXs5i+Bj/AEuhdy8hPQEkAFZ6w8asVym4XSgs1TWXautUvY19NRW+eWSkfsgNla1hLDtrho6+KfkKzfnnT/RV++5Kv3abjE76ZTLKB5n4NlhzOTKmPv2RWi25R6d1tdrrI4qaebs2x9tp0bnB3KxmwHcruUczXdd5w8FrIW5s3xq4ay22w2uu/CM/BxRU74GmL0OjuV5JLuYb10A6KQeedP8ARV++5Kv3aeedP9FX77kq/dpuK/CuWdGBruDFkr5sWkkqq8Ox20VVmpOWRmnw1EUUT3Seh1eGwtII0Nk7B6ALVwZstoZw6bDVV7hg1M6ltvPIw9sx1N4sTNpg5jydfR5fS+zos9550/0VfvuSr92nnnT/AEVfvuSr92m4r8JlnRC67wd7DUGWelu97tV0N+qshgulDURNqKWoqGdnMyPmjLTG5nTle1x+1fFs8HDHbVSGmjut8nidkdNlLzVVbZnvroeTbi5zC4tkLGl7d9/xeQdFN/POn+ir99yVfu0886f6Kv33JV+7TcV+Eyzor3wguG1fxl82sTdY4p8ebcqe53C8T1TGiBkTjzwsi6ve+RhczfRoDzs+pXCAAAANALAeedP9FX77kq/drlmWtnPJT2W+zzHo2M2uaHmPyc8rWsH5XOA+1NzXHbZLSyXDX/y68f0vV/31L1hMQsk1itDo6ksNXUTy1U4jO2tfI8u5QdDYaCG70N63obWbXBtFUV4tUxyJ5iIi50EREBERAREQEREBERAREQFEOJPFXG+E1st9dkdZJTsuFdDbqSGCB88088h01rI2AudobJ0O4HvOgfjKeK9hxHOMWxGtdVy33I3yiigpKV8wayNu3yyOaNMYDygk9xcCegJHn4YYVkmO2isZmuUDNLtLc5q6CpdRRwR0cZ9GOOJo6jTd9SSdvcN67w+LHh2TVeVZjPmF6t1/xa4SQx2iwNt7RFSQsGy6Uu2XyOeeu9j0Gka3ytnvciICIiAiIghHEvD79dcVu4wK7UeJZbVSQ1Dbq+iZK2d8Zbpkw0S5rmt5C7qQ09Ae5fVk4q2Ot4g1nD6e4A5nbrfDX1NP4tJDHNG/QMkJdsOaHEb053LzAEkg6mqw2VWCa/WO501vr32O7VVHJS094p4mPnpS4dHN5h10dO19nq70GZRVbj3EMcPLhgnDzOb3Pec2vFFIWXiK2uhpK2aLq5nM0crX8uzrp0aSeXmaDaSAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgKurtxPor/nl64Z2SpuVFlMVofWOvENv7Wktz3+jDzOcORzySXBvUHkcCQeisVQvDxmvn1mpyEUXmx21N5vGn123Z9ke37XXXfaa1v1IMhw+xOsw7ELTarpfqzKrnRQmKW9XFrRUVBLuZxPL3DegBsnTW7LiNmSIiAiIgIiICIiAiIg+JImy62BzNJLXaBLTojY369E/81TrMoPg1YraaTOsnv8AmsFzvbqSnvctuEjqGOXZibUuiHVoI5efWyXgBoaOlyqG8XxmZ4dXj4PhRHMNReIeUNdhvtWc/Nvp/B8+vt0gmIOxsdQuV8Rc3ZM59c+hza+X1r7QEREBERAREQEREBERAREQEREBERAREQFWXDi1WSi4rcUKq3ZXNerpV1NC642eRxLLS5sBEbWj1do30j+RWaqy4cXWyVvFbihS27FJrLdKSpoW3G8SNIZdnOgJjc0+vs2+ifyoLNREQEREBF1vqIo3cr5WNd8jnAFfPjkH4+P88K2kdyLp8cg/Hx/nhPHIPx8f54S0juRdPjkH4+P88J45B+Pj/PCWkdy/PLwxfDduFBU5xwiuOCV1nmjqGQx3qhyDspnwtlZNFMxvix5e0Y1uxzHQeRs6X6D+OQfj4/zwtEv8J7wRjyTGrVxKs7Gy3G1ctvubItEvpnuPZSdP5Eji37RKPU1LSLV8ErwyK7wnchvFtZgL8etloomzTXTyt40DK54bHEW9izRc0Su3s/wZ6dVs8qG8DTgvT8B+CFptdYIoshuX+UbqS4czZngcsR6/+mzlboHXMHEd6vPxyD8fH+eEtI7kXT45B+Pj/PCeOQfj4/zwlpHci6fHIPx8f54TxyD8fH+eEtI7kXT45B+Pj/PC+45o5d8j2v138p2lpH2iIoCLgnQ2egWNx/J7NllE+ssd2obzSMkMTqi31LJ42vABLS5hI3og6+0IMmiIgIiICIiAiIgKF4f57efOa+cPiXmv21N5veL67Xs+yPb9rrrvtNa36lNFWXDi1WSi4rcUKq3ZXNerpV1NC642eRxLLS5sBEbWj1do30j+RBZqIiAozntxnpLdQ0tPO+mfcayOjdNGdPYwhzncp9RLWEA+rexo6Kkyh/Eb42M/0xH/ANGZdOzRE4sXWObGjh/jGhzY9a5D63S0cb3H7S4gkn7SnwfYt9W7R7BF+yu/L8vtGBY1X5BfavxC0UDO0qKns3ydm3YG+VgLj1I7gVGZeO2EQY7NfZbxJFamVTaKOd9DUNNVK5oc1tO0x81Rtp2DEHgjZB6Fd+/xPHPUvOrP/B9i31btHsEX7KfB9i31btHsEX7KrbiR4T+NYzwor8ux6oF7mZVMtsFOaSpHZVb3NaG1DBH2kXKHBxDw0u6Nbtzmg+vH+LlbJkuF2K53e0yV10tdVdaxkdmuFG6aFujC6nEoc1haN9oyV3ONt00b0pxGJ456l51T74PsW+rdo9gi/ZT4PsW+rdo9gi/ZUbxLwgMBzq6Wu32S/isqLpEZqFzqSeKKqDWc7mxyvjDHPa3ZcwO5m6IcAQdfb+PmAx5V5vOyGIXLxsUHN2E3i3jO9dh4xydj2m+nJz82+mt9E3+J456l51SH4PsW+rdo9gi/ZT4PsW+rdo9gi/ZWfUKoOMuH3XN5sSo7uam+wzSU8kMVLMYmysYXvi7bk7Lna0ElnNsa7ld/ieKepedWW+D7Fvq3aPYIv2U+D7Fvq3aPYIv2Vj6Ti3iddjON5DBdeez5FUw0drqfFpR4xLKSI28pZzN2Wnq4ADXUhR2s8Jzhpb6h8VTkzYBHVy0Ek76KpEEdTG5zXwvl7PkbJtjtMLgXDRaCHAmb/E8c9S86pl8H2LfVu0ewRfsp8H2LfVu0ewRfsqPQ8esElxq7359+FLbLPUQ0txfWUk9PJSSSvYyPtYpGNkYHGRunFvLok70CRipPCh4axOrGPv1Syoo2iSopnWitE8UWt9sYux5xFrr2uuTqPS6hOIxPHPUvOqbfB9i31btHsEX7KfB9i31btHsEX7KwOW8dcHweltlTdr3yU9ypvHaWakpJ6pkkGge1JhY8NZog8ztDr3rtyTjZhWJixeUb23d9pn1drFJTzVRrYmiMkxCJji86lYQ0dSDsAgHTf4njnqXnVmfg+xb6t2j2CL9lddThNoo6eSe1W+ls1wia58FZQQshkjd3g7aOo6DbTsOHQgjovFQ8XcTuGM3/ACCO6GO1WDtBc5KmlmgfSlkTZXB0b2B++R7XDTTvfTZ6KSRVsVytDKunLnQVEAljL2OY4tc3Y21wBB0e4gEetZRjYkz/ALT1W8onjl84pZ5w1vNYKOw4nfqtzH2CpMj6yF1O4McJZmaBDi3m00d2xvuKyVy4ZZHlNHgk13zy7W26WF8dRcjjzm0tPeJWmMkSsIP4Mlh2zuIe4etSXhr/AKOsV/oql/6LVJF8vGpinEqpjlEyk80NtnCTGbTxLu+fQUk5ye6UzaOoqZKuV0YhAjHI2Iu5Gg9kwkhu9jv6lQ/OODeK4lglG6wV7eGFjx67x5PWS2SHsoZRA3cjZo2kB7HMaObYPxB0OlcS89xt9NdrfU0NbAypo6mJ0M0Eo22RjgQ5pHrBBIWpHRYb5QZPY7feLXUtrLZcKeOrpahgIbLE9ocxw316gg9V71X3Bq9y11pvVnOFTYPQY7dJrPb6Qt1BVUsQaI6iE8rRyO2dAA613lWCgIiICIiAiIgKsuHF1slbxW4oUtuxSay3SkqaFtxvEjSGXZzoCY3NPr7Nvon8qs1QvD/Pbz5zXzh8S81+2pvN7xfXa9n2R7ftddd9prW/UgmiIiAofxG+NjP9MR/9GZTBRHiIwluOyfxI7vEXH5NxyNH/AOXAf8V1bN97H5/oyp5q28KWyV2R+D/mlttlBU3OuqaNrIqSkhdLLKe0YSGsaCT0B7go14SOG11bknDrI4LbfrnYLBPWRXGjxWpmguEbJ4WsjmiEL2PcGFmnNYd8sh6EbV8ot8xdi1gyvA6Gv4I5PXYpjOYR3O8XuzvqIsiNXU3CqbT11L+F5JnvkDGxh3fohrCSAAFYPE6x3G4cbeHNdS2+qqaGltN+jqKmGFz44XyR0oja9wGmlxa7lB7+U67lbyKZRrHjWH3yl4TeDXTPslwirrRdqN9whdSSNkooxQ1TXmZutxjmc1pLtDbgD3qMcLuF1FbrNb+H2cYxxGrrtT3F0c89LcK82Kpb4wZY6vbZhA1vxXlug4OB9ElbhopkgFrraxd8f49eL4VZcqt9oul3qZcno7tby20OHZu/x6lqD3SPe1noMcQ7mJLWkbVkP8HjhfI9z38PcZc5x2XG1Qkk/mqeUdHBb6SClpYWU9NAxsUUMTQ1rGNGg0AdwAAGllaZGouN0mQUvDbgzgcuG5Iy74xk9v8AKtS62SCkhihlkBlbNrlkYQQ4OZsAfGLfXmW4ZfvgegojYrj44OJ3lA0/icnaeLeWjJ2/LrfZ9n6fP3cvXeltOixyjVnjJht+ul740vorFcauK4sxLxR0FJI8VJhrXOm7PQ9Pkbou1vlGt6CsSqx+4SeEBmlw8m1LrdU4XSUkVV2DjDLMKirLomu1pzgHNJaDvTh06hXEiuUagWC35db8Q4c2G/WvOYMbhwymijt+MwzQTvug218NW9nK+FrWdnyh7mR7LuY9NLN8GcPvlHcPB+NysFypH2HG7xQ1zquje0Uc4NNG1rnEabzBj+Q79Nuy3YW0iKZRrDxqw24VXHW041QRtfj/ABJZTvvrd9WC2SNlkd+SaF0cJ/2Qtmar/wANL/sH+xYOg4f4/bcxuWVwW1gyK4RNgqLhI98j+zaGgMZzEiNvoNJawAEjZ2eqzVdI2GiqJHuDWMjc5zj6gB1WdMdo7uGv+jrFf6Kpf+i1SRaK5j/hIbHwgttJh9vwu7XTILLTxUFUbjI2igEjIw0vZ0e97TrY21mwQR0O11eCN4X/ABH8JbjubZdqy049jVrttTc6i3W+g344A6OGON0sj3OZp87ZOZpG+z5dad05Mftxa/xn9Vnm3vRFCeMnEuzcJOHtxyO/RVtRbo3RUzoLaN1MrppGxNbGOZvpbfvoQeh0tCOjh9RZFNlmY3y4ZTR37GLpPAbDRUHK6Ohijj5JdvA9Jz39T6RHTprZCnqjXDjALJwuwq14vjlG6gs1AxzYKd7y9zeZ7nu24kkkuc4k79akqAiIgIiICIiAqy4cWqyUXFbihVW7K5r1dKupoXXGzyOJZaXNgIja0ertG+kfyKzVWXDi62St4rcUKW3YpNZbpSVNC243iRpDLs50BMbmn19m30T+VBZqIiAvNcrbTXehmo6yFtRTSjlfG/uPyfkIOiCOoIBC9KKxMxN4EPfgFUDqHLr3BGO5nLSSa/rPgLj/AMSSuPMCv+ud7/Q0P7spii6eJxfLpHsyvKHeYFf9c73+hof3ZPMCv+ud7/Q0P7spiicTieXSPYvKHeYFf9c73+hof3ZPMCv+ud7/AEND+7KYonE4nl0j2Lyh3mBX/XO9/oaH92UW4nYVntHg1zmwTJ6qvytoj8Sp7vHRtpnntGh/OWwNPRnORojqAraVa+Eda7LeuC+R0WQ5TNhdnlbB299p3Fr6XU8ZaQR/KcGs/rJxOJ5dI9i8svHgNyMbefMr0H6HMBDQ637MvrzAr/rne/0ND+7KWwACCMNdztDRp3y9O9dicTieXSPYvKHeYFf9c73+hof3ZPMCv+ud7/Q0P7spiicTieXSPYvKHeYFf9c73+hof3ZPMCv+ud7/AEND+7KYonE4nl0j2Lyh3mBX/XO9/oaH92XZFw9ZMWtul7uV6pgdupKsQMik7iA8RRMLh0+KTo9xBHRS1E4nF19Ij9i8qS8JHwTcO8JG0DypGbTklPHyUd+pIwZox3hkg6drHs/FJBGzyluzujfA/wDAouPDk8V7DxNx+huNpvMVJQUVwp6lp8cpg6V8zWPjcJomlwpy5p5Nlje/l6bvouVirO5cDqeOy4XaMZya/YbasXkZ2VFaav8AB1kILfwNRzgue3TSN736RPVZeDArtHxPrMnlzC5VFknpBTsxeRjPE4n6aO1B1zc3ok/1ipqiAiIgIiICIiAiIgKF4f57efOa+cPiXmv21N5veL67Xs+yPb9rrrvtNa36lNFTGQV9q8HnK77mF4rMhvNHm13oKLxeko31UNqeIzEx2m7LWPcWg6BJc5oAO0FzoiICIiAiIgIiICIiAq18I66WWy8F8jrchxabNLPE2Dt7FTtLn1W54w0AD+S4tf8A1VZSq3MM4u3ECxXq18IMkx6bKbTc4aC5T1xdNFQNJDpfRaNPeG9AN62HjYc0gBZ0BBgjLW8jS0ab8nTuXYuGghoDjs66nWtrlAREQEREBERAREQEREBERAREQEREBERAXBAPeNrlEFTXmjquBoz/AD2S45Vmlrrnw1oxmnY2qfROGmSupwdO5OXldybAaGHv30s60XOK9WmiuEMc0UNXAydkdTE6KVrXNDgHscAWuAPVpGwehXrWu/hFcQsZ8Gq/ycVLtkl4muFbbXWqkwuKsBp7rMxwdG8McD2Qj53c8gGgHjoXODJA2IRfmF4FXhYXvIPCsvlRl9VAPhBLIZRBGIoYaqJnLSNYN9GhgMI3tzuZpc4nZP6eoCIiAiIgLqqamGippaiolZBTwsMkksrg1jGgbLiT0AA67K0c/wAKRxlFgwSzcOaGflrb7IK64Na4bbSRO/BtI+R8o2D/APCflXo8DDwosv8ACXzqmtWRX6koTYbJI6vtFPbgRfi54jNU+T4sXJzRc0beUFzyWjlcWxhsUMuvPGBuHX/hbltm8zWXOXy3UzUj5ZquKJxYYYN6ADnBwLuh1yOaSNh1h2HGLRi0FTDZrZSWuKqqZKydlJC2ISzSHmfI7Q6uce8nqvRabRQ2C2U1utlHT2630sYigpaWJscUTB3Na1oAAHyBetAREQEREBERAREQEREBERAREQEREBERAREQEREBa++FHw/4XcXaahtWZUlwut5tjZfExZZyyooxNyF5JJ7Ic3ZxnUgJ0NtHUqyOK+bTYta6ejt7xHdriXMil1zdhG3XaS6PQkbaBvpzOBIIBCpGKJsLSGg+k4vc4kkucTsuJPUkkkknqSdlei+HfDI2mnfY3+vdGv8AByao3HwInUF/prniGR1dlNJM2emdcnNqJ43tPMx/PG1gBBAPct6qTjtf46SFlTjVvnqWsaJZY7o9jXu11Ib2B5QT6tnXylQVF6L5XsX0/Wr3M3ksD4ebz9VaL74f+7p8PN5+qtF98P8A3dV+ivyvYvp+tXuZvJYHw83n6q0X3w/93T4ebz9VaL74f+7qvZZWQRPkke2ONgLnPcdBoHeSfUF10VdTXOjgq6Ooiq6SdgkingeHskYRsOa4dCCOoIU+WbFy3frV7mbyUDxu8HG/8eeLtxzTIL/DBSVLmMitdK0l0FOwBrYmSOGt62S7l6ucTobVweDrwI4NcG8tt1+bQ3yjyak520t1vdcJYI3SMdG7RhDGDbHuG5WAdeh3oqSIQHAgjYPeCtdfwnY64tTTl/CZ/eZM3k2l71yqc4PZlLQ3GLGKuQvo5WONuc7viLG7dAP/AG8oc5o/ihrhvXKBca8XtezV7Jizh1flOsAiIuMEREBERAREQEREBERAREQEREBERAREQEREFC8XpnT8R5GO+LBbYGsB305pJS4/8dAf1VE1YnHCwvp7tbcgjaTBJGLfUuA+IeYuhJ+QbdI3fyuaPWq1rX1EVFUPpIY6iqbG4xRSyGNj369FrnAO5QToE6OvkPcv0b4dXTXslE090W6JU7kUNF6z3fXE7Hr7Mgl/dFzHec7dIwSYrY2MJHM5t/lJA9Z14oNrs3tOk9J9mKqrbxH4j5bTSZHYLddamldWSMpbY2kofEZII5nRkPlfMJw8hpJcAAHdA0jqffkGZZlDaeJOQUmRCCnxW5yR01tNDC5k8bIoZHMkeRzaIeQC0tI6kl3QCeUPCC32m+SV1svV8tdHLWePyWekrAyjfMXczjy8vMA4jZaHBp2eml6qvhZaa2xZfaX1FYKfJ55Kisc17OeNz42RkRnl0BqNuth3UlcEYGNl7apv+PfaeX59yobf75kme3nM6G03wY7abBSRxujbRxzyVk0tP2x5y/4rA1zWgN0SdnmHRS/gp/oewn+hqT/otXmvnB22Xi81Fzp7tebJUVlMykrha6psbKxjGlrO1BY70g0kBzdHXrXbQ0OSYTa7fYMesVvudnttLDS09VcLy6Cd7WMDfTY2mcN9O8Hr36Hct1FNdGJNdfnrPf2dnd2CcIoab1nvTWJ2P7d5BL+6KQWCqu9XROfebfS22qEhDYaOsdVMLNDTud0cZB3vpr1Dr16ddOJFU2i/SUZe3zupb9Yp4zqSO6Ugb37PNMxjgPytc4f8VtCtdsAsL8kze2xBhdS297bhUv10byk9k3fymQAj7I3fItiV5D45XTOLRTHOI7fz/vq2dwiIvNIIiICIiAiIgIiICIiAiIgIiICIiAiIgIiIPPX0FNdaKejq4WVFLOwxyRSDbXNPeCqRynhRerBM+S0wvvds72sa8CqiHyEOIEgHyg832E9TeyLv2TbcXY6r4fKecTyVqy+OshJEtpu8Lh05ZLZUNP8Ac6/lC+Oaf6Ouf3dP+wtqUX2/ntX0/X+EtDVbmn+jrn93T/sJzT/R1z+7p/2FtSifPavp+v8ABaGq3NP9HXP7un/YTmn+jrn93T/sLalE+e1fT9f4LQ1YaKl5022XRx/ktttQSfyDk6rO2HAckyWVohtstqpSfSrLpGY+Uevli2HuP2ENB/lBbFItdfxzFmLUURE9V7GFxPE6HD7UKKiDnFx55qiTRknfoAucR6+gGh0AAA0As0iLzlddWJVNdc3mUERFgCIiAiIgIiICIiAiIgIiIP/Z", + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import Image\n", + "\n", + "Image(app.get_graph().draw_mermaid_png())" + ] + }, + { + "cell_type": "markdown", + "id": "74f3e276-f003-4112-ba14-c6952076c4f8", + "metadata": {}, + "source": [ + "## Invoke graph\n", + "\n", + "We can step through the execution as follows, printing out the summary as it is refined:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0701bb7d-fbc6-497e-a577-25d56e6e43c6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Apples are characterized by their red color.\n", + "Apples are characterized by their red color, while blueberries are known for their blue hue.\n", + "Apples are characterized by their red color, blueberries are known for their blue hue, and bananas are recognized for their yellow color.\n" + ] + } + ], + "source": [ + "async for step in app.astream(\n", + " {\"contents\": [doc.page_content for doc in documents]},\n", + " stream_mode=\"values\",\n", + "):\n", + " if summary := step.get(\"summary\"):\n", + " print(summary)" + ] + }, + { + "cell_type": "markdown", + "id": "49147724-de8b-44fd-bf13-5ef3432c7c6b", + "metadata": {}, + "source": [ + "The final `step` contains the summary as synthesized from the entire set of documents." + ] + }, + { + "cell_type": "markdown", + "id": "f15c225a-db1d-48cf-b135-f588e7d615e6", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "Check out the summarization [how-to guides](/docs/how_to/#summarization) for additional summarization strategies, including those designed for larger volumes of text.\n", + "\n", + "See [this tutorial](/docs/tutorials/summarization) for more detail on summarization.\n", + "\n", + "See also the [LangGraph documentation](https://langchain-ai.github.io/langgraph/) for detail on building with LangGraph." + ] + } + ], + "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.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/how_to/summarize_stuff.ipynb b/docs/docs/how_to/summarize_stuff.ipynb new file mode 100644 index 0000000000..3c47398752 --- /dev/null +++ b/docs/docs/how_to/summarize_stuff.ipynb @@ -0,0 +1,209 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c47f5b2f-e14c-43e7-a0ab-d71562636624", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 3\n", + "keywords: [summarize, summarization, stuff, create_stuff_documents_chain]\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "682a4f53-27db-43ef-a909-dd9ded76051b", + "metadata": {}, + "source": [ + "# How to summarize text in a single LLM call\n", + "\n", + "LLMs can summarize and otherwise distill desired information from text, including large volumes of text. In many cases, especially for models with larger context windows, this can be adequately achieved via a single LLM call.\n", + "\n", + "LangChain implements a simple [pre-built chain](https://api.python.langchain.com/en/latest/chains/langchain.chains.combine_documents.stuff.create_stuff_documents_chain.html) that \"stuffs\" a prompt with the desired context for summarization and other purposes. In this guide we demonstrate how to use the chain." + ] + }, + { + "cell_type": "markdown", + "id": "4aa52e84-d1b5-4b33-b4c4-541156686ef3", + "metadata": {}, + "source": [ + "## Load chat model\n", + "\n", + "Let's first load a chat model:\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e5f426fc-cea6-4351-8931-1e422d3c8b69", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)" + ] + }, + { + "cell_type": "markdown", + "id": "b137fe82-0a53-4910-b53e-b87a297f329d", + "metadata": {}, + "source": [ + "## Load documents" + ] + }, + { + "cell_type": "markdown", + "id": "a81dc91d-ae72-4996-b809-d4a9050e815e", + "metadata": {}, + "source": [ + "Next, we need some documents to summarize. Below, we generate some toy documents for illustrative purposes. See the document loader [how-to guides](/docs/how_to/#document-loaders) and [integration pages](/docs/integrations/document_loaders/) for additional sources of data. The [summarization tutorial](/docs/tutorials/summarization) also includes an example summarizing a blog post." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "27c8fed0-b2d7-4549-a086-f5ee657efc41", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.documents import Document\n", + "\n", + "documents = [\n", + " Document(page_content=\"Apples are red\", metadata={\"title\": \"apple_book\"}),\n", + " Document(page_content=\"Blueberries are blue\", metadata={\"title\": \"blueberry_book\"}),\n", + " Document(page_content=\"Bananas are yelow\", metadata={\"title\": \"banana_book\"}),\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "84216044-6f1e-4b90-b4fa-29ec305abf51", + "metadata": {}, + "source": [ + "## Load chain\n", + "\n", + "Below, we define a simple prompt and instantiate the chain with our chat model and documents:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "669afa40-2708-4fa1-841e-c74a67bd9175", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.chains.combine_documents import create_stuff_documents_chain\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "\n", + "prompt = ChatPromptTemplate.from_template(\"Summarize this content: {context}\")\n", + "chain = create_stuff_documents_chain(llm, prompt)" + ] + }, + { + "cell_type": "markdown", + "id": "74f3e276-f003-4112-ba14-c6952076c4f8", + "metadata": {}, + "source": [ + "## Invoke chain\n", + "\n", + "Because the chain is a [Runnable](/docs/concepts/#runnable-interface), it implements the usual methods for invocation:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0701bb7d-fbc6-497e-a577-25d56e6e43c6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'The content describes the colors of three fruits: apples are red, blueberries are blue, and bananas are yellow.'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = chain.invoke({\"context\": documents})\n", + "result" + ] + }, + { + "cell_type": "markdown", + "id": "14fb5647-1458-43af-afb7-5aae7b8cab1d", + "metadata": {}, + "source": [ + "### Streaming\n", + "\n", + "Note that the chain also supports streaming of individual output tokens:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0d7a5f67-2ec8-4f90-b085-2969fcb14dce", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "|The| content| describes| the| colors| of| three| fruits|:| apples| are| red|,| blueberries| are| blue|,| and| bananas| are| yellow|.||" + ] + } + ], + "source": [ + "for chunk in chain.stream({\"context\": documents}):\n", + " print(chunk, end=\"|\")" + ] + }, + { + "cell_type": "markdown", + "id": "f15c225a-db1d-48cf-b135-f588e7d615e6", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "See the summarization [how-to guides](/docs/how_to/#summarization) for additional summarization strategies, including those designed for larger volumes of text.\n", + "\n", + "See also [this tutorial](/docs/tutorials/summarization) for more detail on summarization." + ] + } + ], + "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.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/tutorials/summarization.ipynb b/docs/docs/tutorials/summarization.ipynb index 0daae27ff4..a4f0746d32 100644 --- a/docs/docs/tutorials/summarization.ipynb +++ b/docs/docs/tutorials/summarization.ipynb @@ -18,6 +18,14 @@ "source": [ "# Summarize Text\n", "\n", + ":::{.callout-info}\n", + "\n", + "This tutorial demonstrates text summarization using built-in chains and [LangGraph](https://langchain-ai.github.io/langgraph/).\n", + "\n", + "A [previous version](https://python.langchain.com/v0.1/docs/use_cases/summarization/) of this page showcased the legacy chains [StuffDocumentsChain](/docs/versions/migrating_chains/stuff_docs_chain/), [MapReduceDocumentsChain](/docs/versions/migrating_chains/map_reduce_chain/), and [RefineDocumentsChain](https://python.langchain.com/v0.2/docs/versions/migrating_chains/refine_docs_chain/). See [here](/docs/versions/migrating_chains/) for information on using those abstractions and a comparison with the methods demonstrated in this tutorial.\n", + "\n", + ":::\n", + "\n", "Suppose you have a set of documents (PDFs, Notion pages, customer questions, etc.) and you want to summarize the content. \n", "\n", "LLMs are a great tool for this given their proficiency in understanding and synthesizing text.\n", @@ -48,12 +56,11 @@ "\n", "- Using [document loaders](/docs/concepts/#document-loaders), specifically the [WebBaseLoader](https://api.python.langchain.com/en/latest/document_loaders/langchain_community.document_loaders.web_base.WebBaseLoader.html) to load content from an HTML webpage.\n", "\n", - "- Three ways to summarize or otherwise combine documents.\n", + "- Two ways to summarize or otherwise combine documents.\n", " 1. [Stuff](/docs/tutorials/summarization#stuff), which simply concatenates documents into a prompt;\n", - " 2. [Map-reduce](/docs/tutorials/summarization#map-reduce), which splits documents into batches, summarizes those, and then summarizes the summaries;\n", - " 3. [Refine](/docs/tutorials/summarization#refine), which updates a rolling summary be iterating over the documents in a sequence.\n", + " 2. [Map-reduce](/docs/tutorials/summarization#map-reduce), for larger sets of documents. This splits documents into batches, summarizes those, and then summarizes the summaries.\n", "\n", - "That's a fair amount to cover! Let's dive in.\n", + "Shorter, targeted guides on these strategies and others, including [iterative refinement](/docs/how_to/summarize_refine), can be found in the [how-to guides](/docs/how_to/#summarization).\n", "\n", "## Setup\n", "\n", @@ -117,15 +124,13 @@ "source": [ "## Overview\n", "\n", - "A central question for building a summarizer is how to pass your documents into the LLM's context window. Three common approaches for this are:\n", + "A central question for building a summarizer is how to pass your documents into the LLM's context window. Two common approaches for this are:\n", "\n", "1. `Stuff`: Simply \"stuff\" all your documents into a single prompt. This is the simplest approach (see [here](/docs/tutorials/rag#built-in-chains) for more on the `create_stuff_documents_chain` constructor, which is used for this method).\n", "\n", "2. `Map-reduce`: Summarize each document on its own in a \"map\" step and then \"reduce\" the summaries into a final summary (see [here](https://api.python.langchain.com/en/latest/chains/langchain.chains.combine_documents.map_reduce.MapReduceDocumentsChain.html) for more on the `MapReduceDocumentsChain`, which is used for this method).\n", "\n", - "3. `Refine`: Update a rolling summary be iterating over the documents in a sequence.\n", - " \n", - " " + "Note that map-reduce is especially effective when understanding of a sub-document does not rely on preceeding context. For example, when summarizing a corpus of many, shorter documents. In other cases, such as summarizing a novel or body of text with an inherent sequence, [iterative refinement](/docs/how_to/summarize_refine) may be more effective." ] }, { @@ -141,11 +146,7 @@ "id": "bea785ac", "metadata": {}, "source": [ - "## Quickstart\n", - "\n", - "To give you a sneak preview, either pipeline can be wrapped in a single object: `load_summarize_chain`. \n", - "\n", - "Suppose we want to summarize a blog post. We can create this in a few lines of code.\n", + "## Setup\n", "\n", "First set environment variables and install packages:" ] @@ -157,7 +158,7 @@ "metadata": {}, "outputs": [], "source": [ - "%pip install --upgrade --quiet langchain-openai tiktoken chromadb langchain beautifulsoup4\n", + "%pip install --upgrade --quiet tiktoken langchain langgraph beautifulsoup4\n", "\n", "# Set env var OPENAI_API_KEY or load from a .env file\n", "# import dotenv\n", @@ -165,21 +166,6 @@ "# dotenv.load_dotenv()" ] }, - { - "cell_type": "markdown", - "id": "36138740", - "metadata": {}, - "source": [ - "We can use `chain_type=\"stuff\"`, especially if using larger context window models such as:\n", - "\n", - "* 128k token OpenAI `gpt-4-turbo-2024-04-09` \n", - "* 200k token Anthropic `claude-3-sonnet-20240229`\n", - "\n", - "We can also supply `chain_type=\"map_reduce\"` or `chain_type=\"refine\"`.\n", - "\n", - "First we load in our documents. We will use [WebBaseLoader](https://api.python.langchain.com/en/latest/document_loaders/langchain_community.document_loaders.web_base.WebBaseLoader.html) to load a blog post:" - ] - }, { "cell_type": "code", "execution_count": 2, @@ -189,37 +175,59 @@ "source": [ "import os\n", "\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"True\"" + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"" + ] + }, + { + "cell_type": "markdown", + "id": "21541329-f883-42ca-bc94-ab9793951dfa", + "metadata": {}, + "source": [ + "First we load in our documents. We will use [WebBaseLoader](https://api.python.langchain.com/en/latest/document_loaders/langchain_community.document_loaders.web_base.WebBaseLoader.html) to load a blog post:" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 4, "id": "23154e97-c4cb-4bcb-a742-f0c9d06639da", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The article discusses the concept of LLM-powered autonomous agents, with a focus on the components of planning, memory, and tool use. It includes case studies and proof-of-concept examples, as well as challenges and references to related research. The author emphasizes the potential of LLMs in creating powerful problem-solving agents, while also highlighting limitations such as finite context length and reliability of natural language interfaces.\n" - ] - } - ], + "outputs": [], "source": [ - "from langchain.chains.summarize import load_summarize_chain\n", "from langchain_community.document_loaders import WebBaseLoader\n", - "from langchain_openai import ChatOpenAI\n", "\n", "loader = WebBaseLoader(\"https://lilianweng.github.io/posts/2023-06-23-agent/\")\n", - "docs = loader.load()\n", + "docs = loader.load()" + ] + }, + { + "cell_type": "markdown", + "id": "22548ae0-7f67-4dd0-a3f8-d6675b38df53", + "metadata": {}, + "source": [ + "Let's next select a LLM:\n", "\n", - "llm = ChatOpenAI(temperature=0, model_name=\"gpt-3.5-turbo-1106\")\n", - "chain = load_summarize_chain(llm, chain_type=\"stuff\")\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", "\n", - "result = chain.invoke(docs)\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b1c639d9-b27c-4e71-9312-d2666b05f1e3", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", "\n", - "print(result[\"output_text\"])" + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)" ] }, { @@ -227,16 +235,19 @@ "id": "615b36e1", "metadata": {}, "source": [ - "## Option 1. Stuff {#stuff}\n", + "## Stuff: summarize in a single LLM call {#stuff}\n", "\n", - "When we use `load_summarize_chain` with `chain_type=\"stuff\"`, we will use the [StuffDocumentsChain](https://api.python.langchain.com/en/latest/chains/langchain.chains.combine_documents.stuff.StuffDocumentsChain.html#langchain.chains.combine_documents.stuff.StuffDocumentsChain).\n", + "We can use [create_stuff_documents_chain](https://api.python.langchain.com/en/latest/chains/langchain.chains.combine_documents.stuff.create_stuff_documents_chain.html), especially if using larger context window models such as:\n", + "\n", + "* 128k token OpenAI `gpt-4o` \n", + "* 200k token Anthropic `claude-3-5-sonnet-20240620`\n", "\n", "The chain will take a list of documents, insert them all into a prompt, and pass that prompt to an LLM:" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "id": "ef45585d", "metadata": {}, "outputs": [ @@ -244,30 +255,73 @@ "name": "stdout", "output_type": "stream", "text": [ - "The article discusses the concept of building autonomous agents powered by large language models (LLMs). It explores the components of such agents, including planning, memory, and tool use. The article provides case studies and examples of proof-of-concept demos, highlighting the challenges and limitations of LLM-powered agents. It also includes references to related research papers and projects.\n" + "The article \"LLM Powered Autonomous Agents\" by Lilian Weng discusses the development and capabilities of autonomous agents powered by large language models (LLMs). It outlines a system architecture that includes three main components: planning, memory, and tool use. \n", + "\n", + "1. **Planning**: Agents decompose complex tasks into manageable subgoals and engage in self-reflection to improve their performance over time. Techniques like Chain of Thought (CoT) and Tree of Thoughts (ToT) are highlighted for enhancing reasoning and planning.\n", + "\n", + "2. **Memory**: The article distinguishes between short-term and long-term memory, explaining how agents can utilize in-context learning and external vector stores for information retrieval. Maximum Inner Product Search (MIPS) algorithms are discussed for efficient memory access.\n", + "\n", + "3. **Tool Use**: The integration of external tools allows agents to extend their capabilities beyond their inherent knowledge. Examples include MRKL systems and frameworks like HuggingGPT, which facilitate task planning and execution through API calls.\n", + "\n", + "The article also addresses challenges faced by LLM-powered agents, such as finite context length, difficulties in long-term planning, and the reliability of natural language interfaces. It concludes with case studies demonstrating the practical applications of these agents in scientific discovery and interactive simulations.\n", + "\n", + "Overall, the article emphasizes the potential of LLMs as general problem solvers and their ability to function as autonomous agents in various domains.\n" ] } ], "source": [ - "from langchain.chains.combine_documents.stuff import StuffDocumentsChain\n", + "from langchain.chains.combine_documents import create_stuff_documents_chain\n", "from langchain.chains.llm import LLMChain\n", - "from langchain_core.prompts import PromptTemplate\n", + "from langchain_core.prompts import ChatPromptTemplate\n", "\n", "# Define prompt\n", - "prompt_template = \"\"\"Write a concise summary of the following:\n", - "\"{text}\"\n", - "CONCISE SUMMARY:\"\"\"\n", - "prompt = PromptTemplate.from_template(prompt_template)\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [(\"system\", \"Write a concise summary of the following:\\\\n\\\\n{context}\")]\n", + ")\n", "\n", - "# Define LLM chain\n", - "llm = ChatOpenAI(temperature=0, model_name=\"gpt-3.5-turbo-16k\")\n", - "llm_chain = LLMChain(llm=llm, prompt=prompt)\n", + "# Instantiate chain\n", + "chain = create_stuff_documents_chain(llm, prompt)\n", "\n", - "# Define StuffDocumentsChain\n", - "stuff_chain = StuffDocumentsChain(llm_chain=llm_chain, document_variable_name=\"text\")\n", + "# Invoke chain\n", + "result = chain.invoke({\"context\": docs})\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "id": "02d5a634-203c-4e43-ac55-4e502be095d3", + "metadata": {}, + "source": [ + "### Streaming\n", "\n", - "docs = loader.load()\n", - "print(stuff_chain.invoke(docs)[\"output_text\"])" + "Note that we can also stream the result token-by-token:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b7a89b7a-0141-4689-b768-a2a50cdce7da", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "|The| article| \"|LL|M| Powered| Autonomous| Agents|\"| by| Lil|ian| W|eng| discusses| the| development| and| capabilities| of| autonomous| agents| powered| by| large| language| models| (|LL|Ms|).| It| outlines| a| system| overview| that| includes| three| main| components|:| planning|,| memory|,| and| tool| use|.| \n", + "\n", + "|1|.| **|Planning|**| involves| task| decomposition|,| where| agents| break| down| complex| tasks| into| manageable| sub|go|als|,| and| self|-ref|lection|,| allowing| agents| to| learn| from| past| actions| to| improve| future| performance|.\n", + "\n", + "|2|.| **|Memory|**| is| categorized| into| short|-term| and| long|-term| memory|,| with| techniques| like| Maximum| Inner| Product| Search| (|M|IPS|)| used| for| efficient| information| retrieval|.\n", + "\n", + "|3|.| **|Tool| Use|**| highlights| the| integration| of| external| APIs| to| enhance| the| agent|'s| capabilities|,| illustrated| through| case| studies| like| Chem|Crow| for| scientific| discovery| and| Gener|ative| Agents| for| sim|ulating| human| behavior|.\n", + "\n", + "|The| article| also| addresses| challenges| such| as| finite| context| length|,| difficulties| in| long|-term| planning|,| and| the| reliability| of| natural| language| interfaces|.| It| concludes| with| references| to| various| studies| and| projects| that| contribute| to| the| field| of| L|LM|-powered| agents|.||" + ] + } + ], + "source": [ + "for token in chain.stream({\"context\": docs}):\n", + " print(token, end=\"|\")" ] }, { @@ -275,8 +329,6 @@ "id": "4e4e4a43", "metadata": {}, "source": [ - "Great! We can see that we reproduce the earlier result using the `load_summarize_chain`.\n", - "\n", "### Go deeper\n", "\n", "* You can easily customize the prompt. \n", @@ -288,32 +340,37 @@ "id": "ad6cabee", "metadata": {}, "source": [ - "## Option 2. Map-Reduce {#map-reduce}\n", + "## Map-Reduce: summarize long texts via parallelization {#map-reduce}\n", "\n", - "Let's unpack the map reduce approach. For this, we'll first map each document to an individual summary using an `LLMChain`. Then we'll use a `ReduceDocumentsChain` to combine those summaries into a single global summary.\n", - " \n", - "First, we specify the LLMChain to use for mapping each document to an individual summary:" + "Let's unpack the map reduce approach. For this, we'll first map each document to an individual summary using an LLM. Then we'll reduce or consolidate those summaries into a single global summary.\n", + "\n", + "Note that the map step is typically parallelized over the input documents.\n", + "\n", + "[LangGraph](https://langchain-ai.github.io/langgraph/), built on top of `langchain-core`, suports [map-reduce](https://langchain-ai.github.io/langgraph/how-tos/map-reduce/) workflows and is well-suited to this problem:\n", + "\n", + "- LangGraph allows for individual steps (such as successive summarizations) to be streamed, allowing for greater control of execution;\n", + "- LangGraph's [checkpointing](https://langchain-ai.github.io/langgraph/how-tos/persistence/) supports error recovery, extending with human-in-the-loop workflows, and easier incorporation into conversational applications.\n", + "- The LangGraph implementation is straightforward to modify and extend, as we will see below.\n", + "\n", + "### Map\n", + "Let's first define the prompt associated with the map step, and associated it with the LLM via a [chain](/docs/how_to/sequence/). We can use the same summarization prompt as in the `stuff` approach, above:" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 8, "id": "a1e6773c", "metadata": {}, "outputs": [], "source": [ - "from langchain.chains import MapReduceDocumentsChain, ReduceDocumentsChain\n", - "from langchain_text_splitters import CharacterTextSplitter\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.prompts import ChatPromptTemplate\n", "\n", - "llm = ChatOpenAI(temperature=0)\n", + "map_prompt = ChatPromptTemplate.from_messages(\n", + " [(\"system\", \"Write a concise summary of the following:\\\\n\\\\n{context}\")]\n", + ")\n", "\n", - "# Map\n", - "map_template = \"\"\"The following is a set of documents\n", - "{docs}\n", - "Based on this list of docs, please identify the main themes \n", - "Helpful Answer:\"\"\"\n", - "map_prompt = PromptTemplate.from_template(map_template)\n", - "map_chain = LLMChain(llm=llm, prompt=map_prompt)" + "map_chain = map_prompt | llm | StrOutputParser()" ] }, { @@ -330,15 +387,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 10, "id": "ce48b805-d98b-4e0f-8b9e-3b3e72cad3d3", "metadata": {}, "outputs": [], "source": [ "from langchain import hub\n", "\n", - "map_prompt = hub.pull(\"rlm/map-prompt\")\n", - "map_chain = LLMChain(llm=llm, prompt=map_prompt)" + "map_prompt = hub.pull(\"rlm/map-prompt\")" ] }, { @@ -346,96 +402,49 @@ "id": "bee3c331", "metadata": {}, "source": [ - "The `ReduceDocumentsChain` handles taking the document mapping results and reducing them into a single output. It wraps a generic `CombineDocumentsChain` (like `StuffDocumentsChain`) but adds the ability to collapse documents before passing it to the `CombineDocumentsChain` if their cumulative size exceeds `token_max`. In this example, we can actually re-use our chain for combining our docs to also collapse our docs.\n", + "### Reduce\n", "\n", - "So if the cumulative number of tokens in our mapped documents exceeds 4000 tokens, then we'll recursively pass in the documents in batches of < 4000 tokens to our `StuffDocumentsChain` to create batched summaries. And once those batched summaries are cumulatively less than 4000 tokens, we'll pass them all one last time to the `StuffDocumentsChain` to create the final summary." + "We also define a chain that takes the document mapping results and reduces them into a single output." ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 11, "id": "6a718890-99ab-439a-8f79-b9ae9c58ad24", "metadata": {}, "outputs": [], "source": [ - "# Reduce\n", - "reduce_template = \"\"\"The following is set of summaries:\n", + "# Also available via the hub: `hub.pull(\"rlm/reduce-prompt\")`\n", + "reduce_template = \"\"\"\n", + "The following is a set of summaries:\n", "{docs}\n", - "Take these and distill it into a final, consolidated summary of the main themes. \n", - "Helpful Answer:\"\"\"\n", - "reduce_prompt = PromptTemplate.from_template(reduce_template)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "f189184a-673e-4530-8a6b-57b091045d87", - "metadata": {}, - "outputs": [], - "source": [ - "# Note we can also get this from the prompt hub, as noted above\n", - "reduce_prompt = hub.pull(\"rlm/reduce-prompt\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "c9d1da97-d590-4a96-82b2-8002d27fd7f6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ChatPromptTemplate(input_variables=['docs'], metadata={'lc_hub_owner': 'rlm', 'lc_hub_repo': 'map-prompt', 'lc_hub_commit_hash': 'de4fba345f211a462584fc25b7077e69c1ba6cdcf4e21b7ec9abe457ddb16c87'}, messages=[HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['docs'], template='The following is a set of documents:\\n{docs}\\nBased on this list of docs, please identify the main themes \\nHelpful Answer:'))])" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "reduce_prompt" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "1edb1b0d", - "metadata": {}, - "outputs": [], - "source": [ - "# Run chain\n", - "reduce_chain = LLMChain(llm=llm, prompt=reduce_prompt)\n", + "Take these and distill it into a final, consolidated summary\n", + "of the main themes.\n", + "\"\"\"\n", "\n", - "# Takes a list of documents, combines them into a single string, and passes this to an LLMChain\n", - "combine_documents_chain = StuffDocumentsChain(\n", - " llm_chain=reduce_chain, document_variable_name=\"docs\"\n", - ")\n", + "reduce_prompt = ChatPromptTemplate([(\"human\", reduce_template)])\n", "\n", - "# Combines and iteratively reduces the mapped documents\n", - "reduce_documents_chain = ReduceDocumentsChain(\n", - " # This is final chain that is called.\n", - " combine_documents_chain=combine_documents_chain,\n", - " # If documents exceed context for `StuffDocumentsChain`\n", - " collapse_documents_chain=combine_documents_chain,\n", - " # The maximum number of tokens to group documents into.\n", - " token_max=4000,\n", - ")" + "reduce_chain = reduce_prompt | llm | StrOutputParser()" ] }, { "cell_type": "markdown", - "id": "fdb5ae1a", + "id": "3d7df564-415a-49e2-80b6-743446b40be5", "metadata": {}, "source": [ - "Combining our map and reduce chains into one:" + "### Orchestration via LangGraph\n", + "\n", + "Below we implement a simple application that maps the summarization step on a list of documents, then reduces them using the above prompts.\n", + "\n", + "Map-reduce flows are particularly useful when texts are long compared to the context window of a LLM. For long texts, we need a mechanism that ensures that the context to be summarized in the reduce step does not exceed a model's context window size. Here we implement a recursive \"collapsing\" of the summaries: the inputs are partitioned based on a token limit, and summaries are generated of the partitions. This step is repeated until the total length of the summaries is within a desired limit, allowing for the summarization of arbitrary-length text.\n", + "\n", + "First we chunk the blog post into smaller \"sub documents\" to be mapped:" ] }, { "cell_type": "code", "execution_count": 12, - "id": "22f1cdc2", + "id": "7821efb9-e1de-4234-84d2-75dfe13b5a6c", "metadata": {}, "outputs": [ { @@ -444,242 +453,287 @@ "text": [ "Created a chunk of size 1003, which is longer than the specified 1000\n" ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generated 14 documents.\n" + ] } ], "source": [ - "# Combining documents by mapping a chain over them, then combining results\n", - "map_reduce_chain = MapReduceDocumentsChain(\n", - " # Map chain\n", - " llm_chain=map_chain,\n", - " # Reduce chain\n", - " reduce_documents_chain=reduce_documents_chain,\n", - " # The variable name in the llm_chain to put the documents in\n", - " document_variable_name=\"docs\",\n", - " # Return the results of the map steps in the output\n", - " return_intermediate_steps=False,\n", - ")\n", + "from langchain_text_splitters import CharacterTextSplitter\n", "\n", "text_splitter = CharacterTextSplitter.from_tiktoken_encoder(\n", " chunk_size=1000, chunk_overlap=0\n", ")\n", - "split_docs = text_splitter.split_documents(docs)" + "split_docs = text_splitter.split_documents(docs)\n", + "print(f\"Generated {len(split_docs)} documents.\")" ] }, { - "cell_type": "code", - "execution_count": 16, - "id": "d7e53f93-c5aa-456a-85f4-a6b3301a34ed", + "cell_type": "markdown", + "id": "3e7f1c8a-070e-47f0-bcf2-16d6191051ac", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The main themes identified in the list of documents provided are related to large language models (LLMs), autonomous agents, prompting, steering language models, natural language processing (NLP), the use of tools to augment language models, reinforcement learning, reasoning, acting, self-reflection, and the integration of language models with external knowledge sources.\n" - ] - } - ], "source": [ - "result = map_reduce_chain.invoke(split_docs)\n", - "\n", - "print(result[\"output_text\"])" + "Next, we define our graph. Note that we define an artificially low maximum token length of 1,000 tokens to illustrate the \"collapsing\" step." ] }, { - "cell_type": "markdown", - "id": "e62c21cf", + "cell_type": "code", + "execution_count": 13, + "id": "10ced55c-9e3e-404f-abe9-83ac29ffaa5a", "metadata": {}, + "outputs": [], "source": [ - "If we follow the [Langsmith Trace](https://smith.langchain.com/public/3a1a6d51-68e5-4805-8d90-78920ce60a51/r), we can see the the individual LLM summarizations, including the [final call](https://smith.langchain.com/public/69482813-f0b7-46b0-a99f-86d56fc9644a/r) that summarizes the summaries.\n", + "import operator\n", + "from typing import Annotated, List, Literal, TypedDict\n", "\n", - "### Go deeper\n", - " \n", - "**Customization** \n", + "from langchain.chains.combine_documents.reduce import (\n", + " acollapse_docs,\n", + " split_list_of_docs,\n", + ")\n", + "from langchain_core.documents import Document\n", + "from langgraph.constants import Send\n", + "from langgraph.graph import END, START, StateGraph\n", "\n", - "* As shown above, you can customize the LLMs and prompts for map and reduce stages.\n", + "token_max = 1000\n", "\n", - "**Real-world use-case**\n", "\n", - "* See [this blog post](https://blog.langchain.dev/llms-to-improve-documentation/) case-study on analyzing user interactions (questions about LangChain documentation)! \n", - "* The blog post and associated [repo](https://github.com/mendableai/QA_clustering) also introduce clustering as a means of summarization.\n", - "* This opens up another path beyond the `stuff` or `map-reduce` approaches that is worth considering.\n", + "def length_function(documents: List[Document]) -> int:\n", + " \"\"\"Get number of tokens for input contents.\"\"\"\n", + " return sum(llm.get_num_tokens(doc.page_content) for doc in documents)\n", "\n", - "![Image description](../../static/img/summarization_use_case_3.png)" + "\n", + "# This will be the overall state of the main graph.\n", + "# It will contain the input document contents, corresponding\n", + "# summaries, and a final summary.\n", + "class OverallState(TypedDict):\n", + " # Notice here we use the operator.add\n", + " # This is because we want combine all the summaries we generate\n", + " # from individual nodes back into one list - this is essentially\n", + " # the \"reduce\" part\n", + " contents: List[str]\n", + " summaries: Annotated[list, operator.add]\n", + " collapsed_summaries: List[Document]\n", + " final_summary: str\n", + "\n", + "\n", + "# This will be the state of the node that we will \"map\" all\n", + "# documents to in order to generate summaries\n", + "class SummaryState(TypedDict):\n", + " content: str\n", + "\n", + "\n", + "# Here we generate a summary, given a document\n", + "async def generate_summary(state: SummaryState):\n", + " response = await map_chain.ainvoke(state[\"content\"])\n", + " return {\"summaries\": [response]}\n", + "\n", + "\n", + "# Here we define the logic to map out over the documents\n", + "# We will use this an edge in the graph\n", + "def map_summaries(state: OverallState):\n", + " # We will return a list of `Send` objects\n", + " # Each `Send` object consists of the name of a node in the graph\n", + " # as well as the state to send to that node\n", + " return [\n", + " Send(\"generate_summary\", {\"content\": content}) for content in state[\"contents\"]\n", + " ]\n", + "\n", + "\n", + "def collect_summaries(state: OverallState):\n", + " return {\n", + " \"collapsed_summaries\": [Document(summary) for summary in state[\"summaries\"]]\n", + " }\n", + "\n", + "\n", + "# Add node to collapse summaries\n", + "async def collapse_summaries(state: OverallState):\n", + " doc_lists = split_list_of_docs(\n", + " state[\"collapsed_summaries\"], length_function, token_max\n", + " )\n", + " results = []\n", + " for doc_list in doc_lists:\n", + " results.append(await acollapse_docs(doc_list, reduce_chain.ainvoke))\n", + "\n", + " return {\"collapsed_summaries\": results}\n", + "\n", + "\n", + "# This represents a conditional edge in the graph that determines\n", + "# if we should collapse the summaries or not\n", + "def should_collapse(\n", + " state: OverallState,\n", + ") -> Literal[\"collapse_summaries\", \"generate_final_summary\"]:\n", + " num_tokens = length_function(state[\"collapsed_summaries\"])\n", + " if num_tokens > token_max:\n", + " return \"collapse_summaries\"\n", + " else:\n", + " return \"generate_final_summary\"\n", + "\n", + "\n", + "# Here we will generate the final summary\n", + "async def generate_final_summary(state: OverallState):\n", + " response = await reduce_chain.ainvoke(state[\"collapsed_summaries\"])\n", + " return {\"final_summary\": response}\n", + "\n", + "\n", + "# Construct the graph\n", + "# Nodes:\n", + "graph = StateGraph(OverallState)\n", + "graph.add_node(\"generate_summary\", generate_summary) # same as before\n", + "graph.add_node(\"collect_summaries\", collect_summaries)\n", + "graph.add_node(\"collapse_summaries\", collapse_summaries)\n", + "graph.add_node(\"generate_final_summary\", generate_final_summary)\n", + "\n", + "# Edges:\n", + "graph.add_conditional_edges(START, map_summaries, [\"generate_summary\"])\n", + "graph.add_edge(\"generate_summary\", \"collect_summaries\")\n", + "graph.add_conditional_edges(\"collect_summaries\", should_collapse)\n", + "graph.add_conditional_edges(\"collapse_summaries\", should_collapse)\n", + "graph.add_edge(\"generate_final_summary\", END)\n", + "\n", + "app = graph.compile()" ] }, { "cell_type": "markdown", - "id": "f08ff365", + "id": "f00af5d5-bfac-4c13-9439-aa0b18ac3b44", "metadata": {}, "source": [ - "## Option 3. Refine {#refine}\n", - " \n", - "[RefineDocumentsChain](https://api.python.langchain.com/en/latest/chains/langchain.chains.combine_documents.refine.RefineDocumentsChain.html) is similar to map-reduce:\n", - "\n", - "> The refine documents chain constructs a response by looping over the input documents and iteratively updating its answer. For each document, it passes all non-document inputs, the current document, and the latest intermediate answer to an LLM chain to get a new answer.\n", - "\n", - "This can be easily run with the `chain_type=\"refine\"` specified." + "LangGraph allows the graph structure to be plotted to help visualize its function:" ] }, { "cell_type": "code", - "execution_count": 21, - "id": "de1dc10e", + "execution_count": 14, + "id": "0c8d41e4-664d-46f4-94e9-248971d428a6", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "The existing summary provides detailed instructions for implementing a project's architecture through code, focusing on creating core classes, functions, and methods in different files following best practices for the chosen language and framework. Assumptions about the model, view, and controller components are also outlined. The additional context highlights challenges in long-term planning and task decomposition, as well as the reliability issues with natural language interfaces in LLM-powered autonomous agents. These insights shed light on the limitations and potential pitfalls of using LLMs in agent systems, with references to recent research on LLM-powered autonomous agents and related technologies.\n" - ] + "data": { + "image/jpeg": "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", + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "chain = load_summarize_chain(llm, chain_type=\"refine\")\n", - "result = chain.invoke(split_docs)\n", + "from IPython.display import Image\n", "\n", - "print(result[\"output_text\"])" + "Image(app.get_graph().draw_mermaid_png())" ] }, { "cell_type": "markdown", - "id": "b5dc3052-5873-4ef2-b633-3709ede4131a", + "id": "678c0200-32df-4faf-bc54-a4dd470f199c", "metadata": {}, "source": [ - "Following the [Langsmith trace](https://smith.langchain.com/public/38017fa7-b190-4635-992c-e8554227a4bb/r), we can see the summaries iteratively updated with new information." - ] - }, - { - "cell_type": "markdown", - "id": "5b46f44d", - "metadata": {}, - "source": [ - "It's also possible to supply a prompt and return intermediate steps." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "f86c8072", - "metadata": {}, - "outputs": [], - "source": [ - "prompt_template = \"\"\"Write a concise summary of the following:\n", - "{text}\n", - "CONCISE SUMMARY:\"\"\"\n", - "prompt = PromptTemplate.from_template(prompt_template)\n", - "\n", - "refine_template = (\n", - " \"Your job is to produce a final summary\\n\"\n", - " \"We have provided an existing summary up to a certain point: {existing_answer}\\n\"\n", - " \"We have the opportunity to refine the existing summary\"\n", - " \"(only if needed) with some more context below.\\n\"\n", - " \"------------\\n\"\n", - " \"{text}\\n\"\n", - " \"------------\\n\"\n", - " \"Given the new context, refine the original summary in Italian\"\n", - " \"If the context isn't useful, return the original summary.\"\n", - ")\n", - "refine_prompt = PromptTemplate.from_template(refine_template)\n", - "chain = load_summarize_chain(\n", - " llm=llm,\n", - " chain_type=\"refine\",\n", - " question_prompt=prompt,\n", - " refine_prompt=refine_prompt,\n", - " return_intermediate_steps=True,\n", - " input_key=\"input_documents\",\n", - " output_key=\"output_text\",\n", - ")\n", - "result = chain.invoke({\"input_documents\": split_docs}, return_only_outputs=True)" + "When running the application, we can stream the graph to observe its sequence of steps. Below, we will simply print out the name of the step.\n", + "\n", + "Note that because we have a loop in the graph, it can be helpful to specify a [recursion_limit](https://langchain-ai.github.io/langgraph/reference/errors/#langgraph.errors.GraphRecursionError) on its execution. This will raise a specific error when the specified limit is exceeded." ] }, { "cell_type": "code", - "execution_count": 15, - "id": "d9600b67-79d4-4f85-aba2-9fe81fa29f49", + "execution_count": 17, + "id": "b5e32a3c-f43e-4e18-a32d-466403afa844", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Il presente articolo discute il concetto di costruire agenti autonomi utilizzando LLM (large language model) come controller principale. Esplora i diversi componenti di un sistema di agenti alimentato da LLM, tra cui la pianificazione, la memoria e l'uso degli strumenti. Dimostrazioni di concetto come AutoGPT mostrano il potenziale di LLM come risolutore generale di problemi. Approcci come Chain of Thought, Tree of Thoughts, LLM+P, ReAct e Reflexion consentono agli agenti autonomi di pianificare, riflettere su se stessi e migliorarsi iterativamente. Tuttavia, ci sono sfide da affrontare, come la limitata capacità di contesto che limita l'inclusione di informazioni storiche dettagliate e la difficoltà di pianificazione a lungo termine e decomposizione delle attività. Inoltre, l'affidabilità dell'interfaccia di linguaggio naturale tra LLM e componenti esterni come la memoria e gli strumenti è incerta, poiché i LLM possono commettere errori di formattazione e mostrare comportamenti ribelli. Nonostante ciò, il sistema AutoGPT viene menzionato come esempio di dimostrazione di concetto che utilizza LLM come controller principale per agenti autonomi. Questo articolo fa riferimento a diverse fonti che esplorano approcci e applicazioni specifiche di LLM nell'ambito degli agenti autonomi.\n" + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['generate_summary']\n", + "['collect_summaries']\n", + "['collapse_summaries']\n", + "['collapse_summaries']\n", + "['generate_final_summary']\n" ] } ], "source": [ - "print(result[\"output_text\"])" + "async for step in app.astream(\n", + " {\"contents\": [doc.page_content for doc in split_docs]},\n", + " {\"recursion_limit\": 10},\n", + "):\n", + " print(list(step.keys()))" ] }, { "cell_type": "code", - "execution_count": 16, - "id": "5f91a8eb-daa5-4191-ace4-01765801db3e", + "execution_count": 31, + "id": "b0b28b30-d12b-4a30-a0e2-f897adab68c9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "This article discusses the concept of building autonomous agents using LLM (large language model) as the core controller. The article explores the different components of an LLM-powered agent system, including planning, memory, and tool use. It also provides examples of proof-of-concept demos and highlights the potential of LLM as a general problem solver.\n", - "\n", - "Questo articolo discute del concetto di costruire agenti autonomi utilizzando LLM (large language model) come controller principale. L'articolo esplora i diversi componenti di un sistema di agenti alimentato da LLM, inclusa la pianificazione, la memoria e l'uso degli strumenti. Vengono forniti anche esempi di dimostrazioni di proof-of-concept e si evidenzia il potenziale di LLM come risolutore generale di problemi. Inoltre, vengono presentati approcci come Chain of Thought, Tree of Thoughts, LLM+P, ReAct e Reflexion che consentono agli agenti autonomi di pianificare, riflettere su se stessi e migliorare iterativamente.\n", - "\n", - "Questo articolo discute del concetto di costruire agenti autonomi utilizzando LLM (large language model) come controller principale. L'articolo esplora i diversi componenti di un sistema di agenti alimentato da LLM, inclusa la pianificazione, la memoria e l'uso degli strumenti. Vengono forniti anche esempi di dimostrazioni di proof-of-concept e si evidenzia il potenziale di LLM come risolutore generale di problemi. Inoltre, vengono presentati approcci come Chain of Thought, Tree of Thoughts, LLM+P, ReAct e Reflexion che consentono agli agenti autonomi di pianificare, riflettere su se stessi e migliorare iterativamente. Il nuovo contesto riguarda l'approccio Chain of Hindsight (CoH) che permette al modello di migliorare autonomamente i propri output attraverso un processo di apprendimento supervisionato. Viene anche presentato l'approccio Algorithm Distillation (AD) che applica lo stesso concetto alle traiettorie di apprendimento per compiti di reinforcement learning.\n" + "{'generate_final_summary': {'final_summary': 'The consolidated summary of the main themes from the provided documents is as follows:\\n\\n1. **Integration of Large Language Models (LLMs) in Autonomous Agents**: The documents explore the evolving role of LLMs in autonomous systems, emphasizing their enhanced reasoning and acting capabilities through methodologies that incorporate structured planning, memory systems, and tool use.\\n\\n2. **Core Components of Autonomous Agents**:\\n - **Planning**: Techniques like task decomposition (e.g., Chain of Thought) and external classical planners are utilized to facilitate long-term planning by breaking down complex tasks.\\n - **Memory**: The memory system is divided into short-term (in-context learning) and long-term memory, with parallels drawn between human memory and machine learning to improve agent performance.\\n - **Tool Use**: Agents utilize external APIs and algorithms to enhance problem-solving abilities, exemplified by frameworks like HuggingGPT that manage task workflows.\\n\\n3. **Neuro-Symbolic Architectures**: The integration of MRKL (Modular Reasoning, Knowledge, and Language) systems combines neural and symbolic expert modules with LLMs, addressing challenges in tasks such as verbal math problem-solving.\\n\\n4. **Specialized Applications**: Case studies, such as ChemCrow and projects in anticancer drug discovery, demonstrate the advantages of LLMs augmented with expert tools in specialized domains.\\n\\n5. **Challenges and Limitations**: The documents highlight challenges such as hallucination in model outputs and the finite context length of LLMs, which affects their ability to incorporate historical information and perform self-reflection. Techniques like Chain of Hindsight and Algorithm Distillation are discussed to enhance model performance through iterative learning.\\n\\n6. **Structured Software Development**: A systematic approach to creating Python software projects is emphasized, focusing on defining core components, managing dependencies, and adhering to best practices for documentation.\\n\\nOverall, the integration of structured planning, memory systems, and advanced tool use aims to enhance the capabilities of LLM-powered autonomous agents while addressing the challenges and limitations these technologies face in real-world applications.'}}\n" ] } ], "source": [ - "print(\"\\n\\n\".join(result[\"intermediate_steps\"][:3]))" + "print(step)" ] }, { "cell_type": "markdown", - "id": "0d8a8398-a43c-4f14-933c-c0743ae6ec40", + "id": "a9e33d11-7a2a-4693-8c87-88b88eebc896", "metadata": {}, "source": [ - "## Splitting and summarizing in a single chain\n", - "For convenience, we can wrap both the text splitting of our long document and summarizing in a single [chain](/docs/how_to/sequence):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0ddd522e-30dc-4f6a-b993-c4f97e656c4f", - "metadata": {}, - "outputs": [], - "source": [ - "def split_text(text: str):\n", - " return text_splitter.create_documents([text])\n", + "In the corresponding [LangSmith trace](https://smith.langchain.com/public/9d7b1d50-e1d6-44c9-9ab2-eabef621c883/r) we can see the individual LLM calls, grouped under their respective nodes.\n", + "\n", + "### Go deeper\n", + " \n", + "**Customization** \n", + "\n", + "* As shown above, you can customize the LLMs and prompts for map and reduce stages.\n", + "\n", + "**Real-world use-case**\n", "\n", + "* See [this blog post](https://blog.langchain.dev/llms-to-improve-documentation/) case-study on analyzing user interactions (questions about LangChain documentation)! \n", + "* The blog post and associated [repo](https://github.com/mendableai/QA_clustering) also introduce clustering as a means of summarization.\n", + "* This opens up another path beyond the `stuff` or `map-reduce` approaches that is worth considering.\n", "\n", - "summarize_document_chain = split_text | chain" + "![Image description](../../static/img/summarization_use_case_3.png)" ] }, { "cell_type": "markdown", - "id": "a41e4a81-3e26-4753-95bd-f80633620121", + "id": "e8680f94-c872-4d36-92e5-1462ffeb577d", "metadata": {}, "source": [ "## Next steps\n", "\n", "We encourage you to check out the [how-to guides](/docs/how_to) for more detail on: \n", "\n", + "- Other summarization strategies, such as [iterative refinement](/docs/how_to/summarize_refine)\n", "- Built-in [document loaders](/docs/how_to/#document-loaders) and [text-splitters](/docs/how_to/#text-splitters)\n", "- Integrating various combine-document chains into a [RAG application](/docs/tutorials/rag/)\n", "- Incorporating retrieval into a [chatbot](/docs/how_to/chatbots_retrieval/)\n", "\n", "and other concepts." ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "db01bcf3-0186-4689-8f79-1a577e551cb1", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": {