├── langgraph_engineer ├── __init__.py ├── constants.py ├── code_utils.py ├── main.py ├── ingest.py ├── system.py └── docs.json ├── .gitignore ├── notebooks ├── CRAG.jpg └── ntbk_code_examples │ ├── langgraph_crag_code_only.md │ └── langgraph_self_rag_code_only.md ├── README.md └── pyproject.toml /langgraph_engineer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .ipynb_checkpoints 3 | Untitled*.ipynb 4 | 5 | -------------------------------------------------------------------------------- /notebooks/CRAG.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/langchain-ai/langgraph-engineer/HEAD/notebooks/CRAG.jpg -------------------------------------------------------------------------------- /langgraph_engineer/constants.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | DOCS_DIR = Path(__file__).parent / "data" 4 | DOCS_PATH = DOCS_DIR / "docs.json" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Langgraph-Engineer 2 | 3 | 4 | A (very alpha) CLI and corresponding notebook for langgraph app generation. 5 | 6 | To use, install: 7 | 8 | ```bash 9 | pip install -U langgraph-engineer 10 | ``` 11 | 12 | You can generate from only a description, or you can pass in a diagram image. 13 | 14 | ```bash 15 | langgraph-engineer create --description "A RAG app over my local PDF" --diagram "path/to/diagram.png" 16 | ``` 17 | 18 | For example: 19 | 20 | ```bash 21 | langgraph-engineer create --description "A corrective RAG app" --diagram "CRAG.jpg" 22 | ``` -------------------------------------------------------------------------------- /langgraph_engineer/code_utils.py: -------------------------------------------------------------------------------- 1 | from tempfile import NamedTemporaryFile 2 | from typing_extensions import TypedDict 3 | from ruff.__main__ import find_ruff_bin 4 | import subprocess 5 | 6 | 7 | class LintOutput(TypedDict): 8 | out: str 9 | error: str 10 | 11 | def run_ruff(code: str) -> LintOutput: 12 | with NamedTemporaryFile(mode="w", suffix=".py") as f: 13 | f.write(code) 14 | f.seek(0) 15 | ruff_binary = find_ruff_bin() 16 | res = subprocess.run([ruff_binary, f.name], capture_output=True) 17 | output, err = res.stdout, res.stderr 18 | # Replace the temp file name 19 | result = output.decode().replace(f.name, "code.py") 20 | error = err.decode().replace(f.name, "code.py") 21 | return { 22 | "out": result, 23 | "error": error, 24 | } 25 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "langgraph-engineer" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com>"] 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = ">=3.11,<3.12" 11 | typer = "^0.9.0" 12 | langgraph = "^0.0.26" 13 | langchain-community = "^0.0.24" 14 | langchain-openai = "^0.0.7" 15 | langchain-core = "^0.1.27" 16 | bs4 = "^0.0.2" 17 | ruff = "^0.2.2" 18 | jupyter = "^1.0.0" 19 | unstructured = "^0.12.6" 20 | markdown = "^3.6" 21 | langchain-anthropic = "^0.1.4" 22 | 23 | [tool.poetry.scripts] 24 | langgraph-engineer = "langgraph_engineer.main:app" 25 | 26 | [tool.poetry.group.dev.dependencies] 27 | ruff = "^0.2.2" 28 | black = "^24.2.0" 29 | mypy = "^1.8.0" 30 | 31 | [build-system] 32 | requires = ["poetry-core"] 33 | build-backend = "poetry.core.masonry.api" 34 | -------------------------------------------------------------------------------- /langgraph_engineer/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | from typing import List, Optional 4 | 5 | import typer 6 | from langchain_core.messages import BaseMessage, HumanMessage 7 | from langchain_core.utils import image as image_utils 8 | from langgraph.graph import END 9 | from langgraph_engineer import ingest, system 10 | from typing_extensions import Annotated 11 | 12 | 13 | logging.basicConfig(level=logging.INFO) 14 | 15 | app = typer.Typer(no_args_is_help=True, add_completion=True) 16 | 17 | 18 | @app.command(name="create") 19 | def create( 20 | description: str = typer.Argument( 21 | ..., help="Description of the application to be created." 22 | ), 23 | diagram: Annotated[ 24 | Optional[Path], 25 | typer.Option( 26 | help="Path to the image file to be used as the base for the graph" 27 | ), 28 | ] = None, 29 | output: Annotated[ 30 | Optional[Path], 31 | typer.Option( 32 | help="Path to the file where the graph should be saved. Default is stdout.", 33 | ), 34 | ] = None, 35 | ): 36 | """ 37 | Create a graph from an image file. 38 | """ 39 | graph_ = system.build_graph() 40 | if diagram: 41 | image = image_utils.image_to_data_url(str(diagram)) 42 | content = [{"type": "image_url", "image_url": image}] 43 | content.append({"type": "text", "text": description}) 44 | last_chunk = None 45 | for chunk in graph_.stream(HumanMessage(content=content)): 46 | typer.echo(f"Running step {next(iter(chunk))}...") 47 | last_chunk = chunk 48 | code_content = "" 49 | if last_chunk: 50 | messages: List[BaseMessage] = last_chunk[END] 51 | code_content = messages[-1].content 52 | if output: 53 | with output.open("w") as f: 54 | f.write(code_content) 55 | else: 56 | typer.echo(code_content) 57 | 58 | 59 | @app.command(name="ingest") 60 | def ingest_docs( 61 | dry_run: bool = typer.Option( 62 | False, help="Print the ingested documents instead of writing them to file." 63 | ) 64 | ): 65 | """ 66 | Ingest a file into the graph. 67 | """ 68 | ingest.ingest(dry_run=dry_run) 69 | 70 | 71 | if __name__ == "__main__": 72 | app() 73 | -------------------------------------------------------------------------------- /langgraph_engineer/ingest.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | 4 | from bs4 import BeautifulSoup 5 | from langchain_community.document_loaders.recursive_url_loader import \ 6 | RecursiveUrlLoader 7 | from langchain_core.load import dumps, loads 8 | from langgraph_engineer.constants import DOCS_PATH 9 | import warnings 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def html_to_markdown(tag): 15 | if tag.name in ["h1", "h2", "h3", "h4", "h5", "h6"]: 16 | level = int(tag.name[1]) 17 | return f"{'#' * level} {tag.get_text()}\n\n" 18 | elif tag.name == "pre": 19 | code_content = tag.find("code") 20 | if code_content: 21 | return f"```\n{code_content.get_text()}\n```\n\n" 22 | else: 23 | return f"```\n{tag.get_text()}\n```\n\n" 24 | elif tag.name == "p": 25 | return f"{tag.get_text()}\n\n" 26 | return "" 27 | 28 | 29 | def clean_document(html_content): 30 | soup = BeautifulSoup(html_content, "html.parser") 31 | markdown_content = "" 32 | for child in soup.recursiveChildGenerator(): 33 | if child.name: 34 | markdown_content += html_to_markdown(child) 35 | return markdown_content 36 | 37 | 38 | def ingest(dry_run: bool = False): 39 | logger.info("Ingesting documents...") 40 | # LangGraph docs 41 | url = "https://python.langchain.com/docs/langgraph/" 42 | loader = RecursiveUrlLoader( 43 | url=url, max_depth=20, extractor=lambda x: clean_document(x) 44 | ) 45 | docs = loader.load() 46 | 47 | # Sort the list based on the URLs in 'metadata' -> 'source' 48 | d_sorted = sorted(docs, key=lambda x: x.metadata["source"]) 49 | d_reversed = list(reversed(d_sorted)) 50 | 51 | if dry_run: 52 | print(_format_docs(d_reversed)) 53 | return 54 | # Dump the documents to 'DOCS_PATH' 55 | docs_str = dumps(d_reversed) 56 | with DOCS_PATH.open("w") as f: 57 | f.write(docs_str) 58 | logger.info("Documents ingested.") 59 | 60 | 61 | def _format_docs(docs): 62 | return "\n\n\n --- \n\n\n".join([doc.page_content for doc in docs]) 63 | 64 | 65 | @functools.lru_cache 66 | def load_docs() -> str: 67 | # Load the documents from 'DOCS_PATH' 68 | if not DOCS_PATH.exists(): 69 | logger.warning("No documents found. Ingesting documents...") 70 | ingest() 71 | with DOCS_PATH.open("r") as f: 72 | # Suppress warnings 73 | with warnings.catch_warnings(): 74 | warnings.simplefilter("ignore") 75 | d_reversed = loads(f.read()) 76 | 77 | # Concatenate the 'page_content' 78 | return _format_docs(d_reversed) 79 | -------------------------------------------------------------------------------- /langgraph_engineer/system.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import textwrap 3 | from typing import List, Union 4 | 5 | from langchain_core.messages import AIMessage, AnyMessage, BaseMessage 6 | from langchain_core.output_parsers.openai_tools import PydanticToolsParser 7 | from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder 8 | from langchain_core.pydantic_v1 import BaseModel, Field 9 | from langchain_core.runnables import Runnable 10 | from langchain_openai import ChatOpenAI 11 | from langgraph.graph import END, MessageGraph 12 | from langgraph_engineer import code_utils, ingest 13 | 14 | Messages = Union[list[AnyMessage], AnyMessage] 15 | 16 | 17 | def wrap_state(state: List[BaseMessage]) -> dict: 18 | return {"messages": state} 19 | 20 | 21 | def create_image_interpreter() -> Runnable: 22 | 23 | template = """Here are the full LangGrah docs: \n --- --- --- \n {docs} \n --- --- --- \n 24 | You will be shown an image of a graph with nodes as circles and edges \n 25 | as squares. Each node and edge has a label. Use the provided LangGraph docs to convert \n 26 | the image into a LangGraph graph. This will have 3 things: (1) create a dummy \n 27 | state value. (2) Define a dummy function for each each node or edge. (3) finally \n 28 | create the graph workflow that connects all edges and nodes together. \n 29 | Structure your answer with a description of the code solution. \n 30 | Then list the imports. And finally list the functioning code block.""" 31 | 32 | prompt = ChatPromptTemplate.from_messages( 33 | [ 34 | ( 35 | "system", 36 | "You are an expert in converting graph visualizations into LangGraph," 37 | " a library for building stateful, multi-actor applications with LLMs.\n" 38 | + textwrap.dedent(template), 39 | ), 40 | MessagesPlaceholder(variable_name="messages"), 41 | ] 42 | ).partial(docs=ingest.load_docs()) 43 | 44 | # Multi-modal LLM 45 | model = ChatOpenAI(temperature=0, model="gpt-4-vision-preview", max_tokens="1028") 46 | 47 | return (wrap_state | prompt | model).with_config(run_name="image_to_graph") 48 | 49 | 50 | # Data model 51 | class code(BaseModel): 52 | """Code output""" 53 | 54 | module_docstring: str = Field(description="Description of the problem and approach") 55 | imports: str = Field(description="Code block import statements") 56 | code: str = Field(description="Code block not including import statements") 57 | 58 | 59 | def format_code(tools: list[code], name: str = "Junior Developer") -> BaseMessage: 60 | invoked = tools[0] 61 | return AIMessage( 62 | content=f'"""\n{invoked.module_docstring}\n"""\n\n{invoked.imports}\n\n{invoked.code}', 63 | name=name, 64 | ) 65 | 66 | 67 | def create_code_formatter() -> Runnable: 68 | 69 | # Structured output prompt 70 | template = """You are an expert a code formatting, starting with a code solution. 71 | 72 | Structure the solution in three parts: 73 | (1) a prefix that defines the problem, 74 | (2) list the imports, and 75 | (3) list the functioning code block.""" 76 | 77 | prompt = ChatPromptTemplate.from_messages( 78 | [ 79 | ("system", template), 80 | MessagesPlaceholder(variable_name="messages"), 81 | ("system", "Extract the code from the last message and format it."), 82 | ] 83 | ) 84 | 85 | # LLM 86 | model = ChatOpenAI(temperature=0, model="gpt-4-0125-preview").bind_tools( 87 | [code], tool_choice="code" 88 | ) 89 | # Parser 90 | parser_tool = PydanticToolsParser(tools=[code]) 91 | 92 | def get_last_message(state: List[BaseMessage]) -> dict: 93 | return {"messages": [state[-1]]} 94 | 95 | return (get_last_message | prompt | model | parser_tool | format_code).with_config( 96 | run_name="code_formatter" 97 | ) 98 | 99 | 100 | def create_code_generator() -> Runnable: 101 | 102 | # Structured output prompt 103 | template = """You are an expert python developer. Develop an application for the user's problem using 104 | LangGraph. Reference the LangGraph docs below for the necessary information. 105 | 106 | {docs} 107 | 108 | """ 109 | 110 | prompt = ChatPromptTemplate.from_messages( 111 | [ 112 | ("system", template), 113 | MessagesPlaceholder(variable_name="messages"), 114 | ] 115 | ).partial(docs=ingest.load_docs()) 116 | 117 | # LLM 118 | model = ChatOpenAI(temperature=0, model="gpt-4-0125-preview").bind_tools( 119 | [code], tool_choice="code" 120 | ) 121 | # Parser 122 | parser_tool = PydanticToolsParser(tools=[code]) 123 | 124 | return (format_code | prompt | model | parser_tool | format_code).with_config( 125 | run_name="code_generator" 126 | ) 127 | 128 | 129 | def lint_code(state: List[BaseMessage]) -> List[BaseMessage]: 130 | synthetic_code = state[-1].content 131 | res = code_utils.run_ruff(synthetic_code) 132 | if res["error"]: 133 | result = [ 134 | AIMessage( 135 | content=f"{res['error']}\n\nOutput:\n{res['out']}", name="Code Reviewer" 136 | ) 137 | ] 138 | else: 139 | result = [] 140 | return result 141 | 142 | 143 | def should_regenerate(state: List[BaseMessage], max_tries: int = 3) -> str: 144 | num_code_reviewer_messages = sum( 145 | 1 for message in state if message.name == "Code Reviewer" 146 | ) 147 | if ( 148 | num_code_reviewer_messages == num_code_reviewer_messages 149 | or num_code_reviewer_messages >= max_tries 150 | ): 151 | # Either no errors or too many attempts 152 | return END 153 | 154 | return "fix_code" 155 | 156 | 157 | def create_code_fixer() -> Runnable: 158 | template = """You are an expert python developer, knowledgeable in LangGraph. 159 | Fix the junior developer's draft code to ensure it is free of errors. 160 | Consult the following docs for the necessary information. 161 | 162 | {docs} 163 | 164 | """ 165 | prompt = ChatPromptTemplate.from_messages( 166 | [ 167 | ("system", template), 168 | MessagesPlaceholder(variable_name="messages"), 169 | ] 170 | ).partial(docs=ingest.load_docs()) 171 | 172 | llm = ChatOpenAI(temperature=0, model="gpt-4-0125-preview").bind_tools( 173 | [code], tool_choice="code" 174 | ) 175 | parser = PydanticToolsParser(tools=[code]) 176 | 177 | def format_messages(state: List[BaseMessage]): 178 | # Remove any images here 179 | messages = [] 180 | for message in state: 181 | if isinstance(message.content, str): 182 | messages.append(message) 183 | continue 184 | if any(message["type"] == "image_url" for message in message.content): 185 | new_content = [ 186 | content 187 | for content in message.content 188 | if content["type"] != "image_url" 189 | ] 190 | messages.append( 191 | message.__class__( 192 | **message.dict(exclude={"content"}), content=new_content 193 | ) 194 | ) 195 | continue 196 | messages.append(message) 197 | return {"messages": messages} 198 | 199 | return ( 200 | format_messages 201 | | prompt 202 | | llm 203 | | parser 204 | | functools.partial(format_code, name="Senior Developer") 205 | ).with_config(run_name="code_fixer") 206 | 207 | 208 | def pick_route(state: List[BaseMessage]) -> str: 209 | message_content = state[-1].content 210 | if isinstance(message_content, list) and any( 211 | message["type"] == "image_url" for message in message_content 212 | ): 213 | return "understand_image" 214 | return "generate_code" 215 | 216 | 217 | def build_graph() -> Runnable: 218 | builder = MessageGraph() 219 | builder.add_node( 220 | "enter", 221 | lambda _: [], 222 | ) 223 | builder.add_node("understand_image", create_image_interpreter()) 224 | builder.add_node("format_code", create_code_formatter()) 225 | builder.add_node("generate_code", create_code_generator()) 226 | builder.add_node("lint_code", lint_code) 227 | builder.add_node("fix_code", create_code_fixer()) 228 | 229 | builder.add_conditional_edges("enter", pick_route) 230 | builder.add_edge("understand_image", "format_code") 231 | builder.add_edge("generate_code", "lint_code") 232 | builder.add_edge("format_code", "lint_code") 233 | builder.add_edge("fix_code", "lint_code") 234 | builder.set_entry_point("enter") 235 | builder.add_conditional_edges("lint_code", should_regenerate) 236 | return builder.compile() 237 | -------------------------------------------------------------------------------- /notebooks/ntbk_code_examples/langgraph_crag_code_only.md: -------------------------------------------------------------------------------- 1 | ```python 2 | ### Index 3 | 4 | from langchain.text_splitter import RecursiveCharacterTextSplitter 5 | from langchain_community.document_loaders import WebBaseLoader 6 | from langchain_community.vectorstores import Chroma 7 | from langchain_openai import OpenAIEmbeddings 8 | 9 | urls = [ 10 | "https://lilianweng.github.io/posts/2023-06-23-agent/", 11 | "https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/", 12 | "https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/", 13 | ] 14 | 15 | docs = [WebBaseLoader(url).load() for url in urls] 16 | docs_list = [item for sublist in docs for item in sublist] 17 | 18 | text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( 19 | chunk_size=250, chunk_overlap=0 20 | ) 21 | doc_splits = text_splitter.split_documents(docs_list) 22 | 23 | # Add to vectorDB 24 | vectorstore = Chroma.from_documents( 25 | documents=doc_splits, 26 | collection_name="rag-chroma", 27 | embedding=OpenAIEmbeddings(), 28 | ) 29 | retriever = vectorstore.as_retriever() 30 | ``` 31 | 32 | 33 | ```python 34 | ### State 35 | 36 | from typing import Dict, TypedDict 37 | 38 | from langchain_core.messages import BaseMessage 39 | 40 | 41 | class GraphState(TypedDict): 42 | """ 43 | Represents the state of our graph. 44 | 45 | Attributes: 46 | keys: A dictionary where each key is a string. 47 | """ 48 | 49 | keys: Dict[str, any] 50 | ``` 51 | 52 | 53 | ```python 54 | ### Nodes and edges 55 | 56 | import json 57 | import operator 58 | from typing import Annotated, Sequence, TypedDict 59 | 60 | from langchain import hub 61 | from langchain.output_parsers.openai_tools import PydanticToolsParser 62 | from langchain.prompts import PromptTemplate 63 | from langchain.schema import Document 64 | from langchain_community.tools.tavily_search import TavilySearchResults 65 | from langchain_community.vectorstores import Chroma 66 | from langchain_core.messages import BaseMessage, FunctionMessage 67 | from langchain_core.output_parsers import StrOutputParser 68 | from langchain_core.pydantic_v1 import BaseModel, Field 69 | from langchain_core.runnables import RunnablePassthrough 70 | from langchain_core.utils.function_calling import convert_to_openai_tool 71 | from langchain_openai import ChatOpenAI, OpenAIEmbeddings 72 | 73 | ### Nodes ### 74 | 75 | 76 | def retrieve(state): 77 | """ 78 | Retrieve documents 79 | 80 | Args: 81 | state (dict): The current graph state 82 | 83 | Returns: 84 | state (dict): New key added to state, documents, that contains retrieved documents 85 | """ 86 | print("---RETRIEVE---") 87 | state_dict = state["keys"] 88 | question = state_dict["question"] 89 | documents = retriever.get_relevant_documents(question) 90 | return {"keys": {"documents": documents, "question": question}} 91 | 92 | 93 | def generate(state): 94 | """ 95 | Generate answer 96 | 97 | Args: 98 | state (dict): The current graph state 99 | 100 | Returns: 101 | state (dict): New key added to state, generation, that contains LLM generation 102 | """ 103 | print("---GENERATE---") 104 | state_dict = state["keys"] 105 | question = state_dict["question"] 106 | documents = state_dict["documents"] 107 | 108 | # Prompt 109 | prompt = hub.pull("rlm/rag-prompt") 110 | 111 | # LLM 112 | llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0, streaming=True) 113 | 114 | # Post-processing 115 | def format_docs(docs): 116 | return "\n\n".join(doc.page_content for doc in docs) 117 | 118 | # Chain 119 | rag_chain = prompt | llm | StrOutputParser() 120 | 121 | # Run 122 | generation = rag_chain.invoke({"context": documents, "question": question}) 123 | return { 124 | "keys": {"documents": documents, "question": question, "generation": generation} 125 | } 126 | 127 | 128 | def grade_documents(state): 129 | """ 130 | Determines whether the retrieved documents are relevant to the question. 131 | 132 | Args: 133 | state (dict): The current graph state 134 | 135 | Returns: 136 | state (dict): Updates documents key with relevant documents 137 | """ 138 | 139 | print("---CHECK RELEVANCE---") 140 | state_dict = state["keys"] 141 | question = state_dict["question"] 142 | documents = state_dict["documents"] 143 | 144 | # Data model 145 | class grade(BaseModel): 146 | """Binary score for relevance check.""" 147 | 148 | binary_score: str = Field(description="Relevance score 'yes' or 'no'") 149 | 150 | # LLM 151 | model = ChatOpenAI(temperature=0, model="gpt-4-0125-preview", streaming=True) 152 | 153 | # Tool 154 | grade_tool_oai = convert_to_openai_tool(grade) 155 | 156 | # LLM with tool and enforce invocation 157 | llm_with_tool = model.bind( 158 | tools=[convert_to_openai_tool(grade_tool_oai)], 159 | tool_choice={"type": "function", "function": {"name": "grade"}}, 160 | ) 161 | 162 | # Parser 163 | parser_tool = PydanticToolsParser(tools=[grade]) 164 | 165 | # Prompt 166 | prompt = PromptTemplate( 167 | template="""You are a grader assessing relevance of a retrieved document to a user question. \n 168 | Here is the retrieved document: \n\n {context} \n\n 169 | Here is the user question: {question} \n 170 | If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \n 171 | Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.""", 172 | input_variables=["context", "question"], 173 | ) 174 | 175 | # Chain 176 | chain = prompt | llm_with_tool | parser_tool 177 | 178 | # Score 179 | filtered_docs = [] 180 | search = "No" # Default do not opt for web search to supplement retrieval 181 | for d in documents: 182 | score = chain.invoke({"question": question, "context": d.page_content}) 183 | grade = score[0].binary_score 184 | if grade == "yes": 185 | print("---GRADE: DOCUMENT RELEVANT---") 186 | filtered_docs.append(d) 187 | else: 188 | print("---GRADE: DOCUMENT NOT RELEVANT---") 189 | search = "Yes" # Perform web search 190 | continue 191 | 192 | return { 193 | "keys": { 194 | "documents": filtered_docs, 195 | "question": question, 196 | "run_web_search": search, 197 | } 198 | } 199 | 200 | 201 | def transform_query(state): 202 | """ 203 | Transform the query to produce a better question. 204 | 205 | Args: 206 | state (dict): The current graph state 207 | 208 | Returns: 209 | state (dict): Updates question key with a re-phrased question 210 | """ 211 | 212 | print("---TRANSFORM QUERY---") 213 | state_dict = state["keys"] 214 | question = state_dict["question"] 215 | documents = state_dict["documents"] 216 | 217 | # Create a prompt template with format instructions and the query 218 | prompt = PromptTemplate( 219 | template="""You are generating questions that is well optimized for retrieval. \n 220 | Look at the input and try to reason about the underlying sematic intent / meaning. \n 221 | Here is the initial question: 222 | \n ------- \n 223 | {question} 224 | \n ------- \n 225 | Formulate an improved question: """, 226 | input_variables=["question"], 227 | ) 228 | 229 | # Grader 230 | model = ChatOpenAI(temperature=0, model="gpt-4-0125-preview", streaming=True) 231 | 232 | # Prompt 233 | chain = prompt | model | StrOutputParser() 234 | better_question = chain.invoke({"question": question}) 235 | 236 | return {"keys": {"documents": documents, "question": better_question}} 237 | 238 | 239 | def web_search(state): 240 | """ 241 | Web search based on the re-phrased question using Tavily API. 242 | 243 | Args: 244 | state (dict): The current graph state 245 | 246 | Returns: 247 | state (dict): Updates documents key with appended web results 248 | """ 249 | 250 | print("---WEB SEARCH---") 251 | state_dict = state["keys"] 252 | question = state_dict["question"] 253 | documents = state_dict["documents"] 254 | 255 | tool = TavilySearchResults() 256 | docs = tool.invoke({"query": question}) 257 | web_results = "\n".join([d["content"] for d in docs]) 258 | web_results = Document(page_content=web_results) 259 | documents.append(web_results) 260 | 261 | return {"keys": {"documents": documents, "question": question}} 262 | 263 | 264 | ### Edges 265 | 266 | 267 | def decide_to_generate(state): 268 | """ 269 | Determines whether to generate an answer or re-generate a question for web search. 270 | 271 | Args: 272 | state (dict): The current state of the agent, including all keys. 273 | 274 | Returns: 275 | str: Next node to call 276 | """ 277 | 278 | print("---DECIDE TO GENERATE---") 279 | state_dict = state["keys"] 280 | question = state_dict["question"] 281 | filtered_documents = state_dict["documents"] 282 | search = state_dict["run_web_search"] 283 | 284 | if search == "Yes": 285 | # All documents have been filtered check_relevance 286 | # We will re-generate a new query 287 | print("---DECISION: TRANSFORM QUERY and RUN WEB SEARCH---") 288 | return "transform_query" 289 | else: 290 | # We have relevant documents, so generate answer 291 | print("---DECISION: GENERATE---") 292 | return "generate" 293 | ``` 294 | 295 | 296 | ```python 297 | ### Build graph 298 | 299 | import pprint 300 | 301 | from langgraph.graph import END, StateGraph 302 | 303 | workflow = StateGraph(GraphState) 304 | 305 | # Define the nodes 306 | workflow.add_node("retrieve", retrieve) # retrieve 307 | workflow.add_node("grade_documents", grade_documents) # grade documents 308 | workflow.add_node("generate", generate) # generatae 309 | workflow.add_node("transform_query", transform_query) # transform_query 310 | workflow.add_node("web_search", web_search) # web search 311 | 312 | # Build graph 313 | workflow.set_entry_point("retrieve") 314 | workflow.add_edge("retrieve", "grade_documents") 315 | workflow.add_conditional_edges( 316 | "grade_documents", 317 | decide_to_generate, 318 | { 319 | "transform_query": "transform_query", 320 | "generate": "generate", 321 | }, 322 | ) 323 | workflow.add_edge("transform_query", "web_search") 324 | workflow.add_edge("web_search", "generate") 325 | workflow.add_edge("generate", END) 326 | 327 | # Compile 328 | app = workflow.compile() 329 | ``` 330 | 331 | 332 | ```python 333 | ### Run 334 | 335 | inputs = {"keys": {"question": "Explain how the different types of agent memory work?"}} 336 | for output in app.stream(inputs): 337 | for key, value in output.items(): 338 | # Node 339 | pprint.pprint(f"Node '{key}':") 340 | # Optional: print full state at each node 341 | # pprint.pprint(value["keys"], indent=2, width=80, depth=None) 342 | pprint.pprint("\n---\n") 343 | 344 | # Final generation 345 | pprint.pprint(value["keys"]["generation"]) 346 | ``` 347 | 348 | ---RETRIEVE--- 349 | "Node 'retrieve':" 350 | '\n---\n' 351 | ---CHECK RELEVANCE--- 352 | ---GRADE: DOCUMENT RELEVANT--- 353 | ---GRADE: DOCUMENT RELEVANT--- 354 | ---GRADE: DOCUMENT RELEVANT--- 355 | ---GRADE: DOCUMENT RELEVANT--- 356 | "Node 'grade_documents':" 357 | '\n---\n' 358 | ---DECIDE TO GENERATE--- 359 | ---DECISION: GENERATE--- 360 | ---GENERATE--- 361 | "Node 'generate':" 362 | '\n---\n' 363 | "Node '__end__':" 364 | '\n---\n' 365 | ('There are several types of memory in human brains, including sensory memory, ' 366 | 'which retains impressions of sensory information for a few seconds after the ' 367 | 'original stimuli have ended. Short-term memory is utilized for in-context ' 368 | 'learning, while long-term memory allows the agent to retain and recall ' 369 | 'information over extended periods by leveraging an external vector store and ' 370 | 'fast retrieval. Additionally, agents can use tool use to call external APIs ' 371 | 'for extra information that is missing from the model weights.') 372 | 373 | -------------------------------------------------------------------------------- /notebooks/ntbk_code_examples/langgraph_self_rag_code_only.md: -------------------------------------------------------------------------------- 1 | ```python 2 | ### Load 3 | 4 | from langchain.text_splitter import RecursiveCharacterTextSplitter 5 | from langchain_community.document_loaders import WebBaseLoader 6 | from langchain_community.vectorstores import Chroma 7 | from langchain_openai import OpenAIEmbeddings 8 | 9 | urls = [ 10 | "https://lilianweng.github.io/posts/2023-06-23-agent/", 11 | "https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/", 12 | "https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/", 13 | ] 14 | 15 | docs = [WebBaseLoader(url).load() for url in urls] 16 | docs_list = [item for sublist in docs for item in sublist] 17 | 18 | text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( 19 | chunk_size=250, chunk_overlap=0 20 | ) 21 | doc_splits = text_splitter.split_documents(docs_list) 22 | 23 | # Add to vectorDB 24 | vectorstore = Chroma.from_documents( 25 | documents=doc_splits, 26 | collection_name="rag-chroma", 27 | embedding=OpenAIEmbeddings(), 28 | ) 29 | retriever = vectorstore.as_retriever() 30 | ``` 31 | 32 | 33 | ```python 34 | ### State 35 | 36 | from typing import Dict, TypedDict 37 | 38 | from langchain_core.messages import BaseMessage 39 | 40 | 41 | class GraphState(TypedDict): 42 | """ 43 | Represents the state of our graph. 44 | 45 | Attributes: 46 | keys: A dictionary where each key is a string. 47 | """ 48 | 49 | keys: Dict[str, any] 50 | ``` 51 | 52 | 53 | ```python 54 | ### Graph 55 | 56 | import json 57 | import operator 58 | from typing import Annotated, Sequence, TypedDict 59 | 60 | from langchain import hub 61 | from langchain.output_parsers.openai_tools import PydanticToolsParser 62 | from langchain.prompts import PromptTemplate 63 | from langchain_community.vectorstores import Chroma 64 | from langchain_core.messages import BaseMessage, FunctionMessage 65 | from langchain_core.output_parsers import StrOutputParser 66 | from langchain_core.pydantic_v1 import BaseModel, Field 67 | from langchain_core.runnables import RunnablePassthrough 68 | from langchain_core.utils.function_calling import convert_to_openai_tool 69 | from langchain_openai import ChatOpenAI, OpenAIEmbeddings 70 | 71 | ### Nodes ### 72 | 73 | 74 | def retrieve(state): 75 | """ 76 | Retrieve documents 77 | 78 | Args: 79 | state (dict): The current graph state 80 | 81 | Returns: 82 | state (dict): New key added to state, documents, that contains retrieved documents 83 | """ 84 | print("---RETRIEVE---") 85 | state_dict = state["keys"] 86 | question = state_dict["question"] 87 | documents = retriever.get_relevant_documents(question) 88 | return {"keys": {"documents": documents, "question": question}} 89 | 90 | 91 | def generate(state):a 92 | """ 93 | Generate answer 94 | 95 | Args: 96 | state (dict): The current graph state 97 | 98 | Returns: 99 | state (dict): New key added to state, generation, that contains LLM generation 100 | """ 101 | print("---GENERATE---") 102 | state_dict = state["keys"] 103 | question = state_dict["question"] 104 | documents = state_dict["documents"] 105 | 106 | # Prompt 107 | prompt = hub.pull("rlm/rag-prompt") 108 | 109 | # LLM 110 | llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0) 111 | 112 | # Post-processing 113 | def format_docs(docs): 114 | return "\n\n".join(doc.page_content for doc in docs) 115 | 116 | # Chain 117 | rag_chain = prompt | llm | StrOutputParser() 118 | 119 | # Run 120 | generation = rag_chain.invoke({"context": documents, "question": question}) 121 | return { 122 | "keys": {"documents": documents, "question": question, "generation": generation} 123 | } 124 | 125 | 126 | def grade_documents(state): 127 | """ 128 | Determines whether the retrieved documents are relevant to the question. 129 | 130 | Args: 131 | state (dict): The current graph state 132 | 133 | Returns: 134 | state (dict): Updates documents key with relevant documents 135 | """ 136 | 137 | print("---CHECK RELEVANCE---") 138 | state_dict = state["keys"] 139 | question = state_dict["question"] 140 | documents = state_dict["documents"] 141 | 142 | # Data model 143 | class grade(BaseModel): 144 | """Binary score for relevance check.""" 145 | 146 | binary_score: str = Field(description="Relevance score 'yes' or 'no'") 147 | 148 | # LLM 149 | model = ChatOpenAI(temperature=0, model="gpt-4-0125-preview", streaming=True) 150 | 151 | # Tool 152 | grade_tool_oai = convert_to_openai_tool(grade) 153 | 154 | # LLM with tool and enforce invocation 155 | llm_with_tool = model.bind( 156 | tools=[convert_to_openai_tool(grade_tool_oai)], 157 | tool_choice={"type": "function", "function": {"name": "grade"}}, 158 | ) 159 | 160 | # Parser 161 | parser_tool = PydanticToolsParser(tools=[grade]) 162 | 163 | # Prompt 164 | prompt = PromptTemplate( 165 | template="""You are a grader assessing relevance of a retrieved document to a user question. \n 166 | Here is the retrieved document: \n\n {context} \n\n 167 | Here is the user question: {question} \n 168 | If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \n 169 | Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.""", 170 | input_variables=["context", "question"], 171 | ) 172 | 173 | # Chain 174 | chain = prompt | llm_with_tool | parser_tool 175 | 176 | # Score 177 | filtered_docs = [] 178 | for d in documents: 179 | score = chain.invoke({"question": question, "context": d.page_content}) 180 | grade = score[0].binary_score 181 | if grade == "yes": 182 | print("---GRADE: DOCUMENT RELEVANT---") 183 | filtered_docs.append(d) 184 | else: 185 | print("---GRADE: DOCUMENT NOT RELEVANT---") 186 | continue 187 | 188 | return {"keys": {"documents": filtered_docs, "question": question}} 189 | 190 | 191 | def transform_query(state): 192 | """ 193 | Transform the query to produce a better question. 194 | 195 | Args: 196 | state (dict): The current graph state 197 | 198 | Returns: 199 | state (dict): Updates question key with a re-phrased question 200 | """ 201 | 202 | print("---TRANSFORM QUERY---") 203 | state_dict = state["keys"] 204 | question = state_dict["question"] 205 | documents = state_dict["documents"] 206 | 207 | # Create a prompt template with format instructions and the query 208 | prompt = PromptTemplate( 209 | template="""You are generating questions that is well optimized for retrieval. \n 210 | Look at the input and try to reason about the underlying sematic intent / meaning. \n 211 | Here is the initial question: 212 | \n ------- \n 213 | {question} 214 | \n ------- \n 215 | Formulate an improved question: """, 216 | input_variables=["question"], 217 | ) 218 | 219 | # Grader 220 | model = ChatOpenAI(temperature=0, model="gpt-4-0125-preview", streaming=True) 221 | 222 | # Prompt 223 | chain = prompt | model | StrOutputParser() 224 | better_question = chain.invoke({"question": question}) 225 | 226 | return {"keys": {"documents": documents, "question": better_question}} 227 | 228 | 229 | def prepare_for_final_grade(state): 230 | """ 231 | Passthrough state for final grade. 232 | 233 | Args: 234 | state (dict): The current graph state 235 | 236 | Returns: 237 | state (dict): The current graph state 238 | """ 239 | 240 | print("---FINAL GRADE---") 241 | state_dict = state["keys"] 242 | question = state_dict["question"] 243 | documents = state_dict["documents"] 244 | generation = state_dict["generation"] 245 | 246 | return { 247 | "keys": {"documents": documents, "question": question, "generation": generation} 248 | } 249 | 250 | 251 | ### Edges ### 252 | 253 | 254 | def decide_to_generate(state): 255 | """ 256 | Determines whether to generate an answer, or re-generate a question. 257 | 258 | Args: 259 | state (dict): The current state of the agent, including all keys. 260 | 261 | Returns: 262 | str: Next node to call 263 | """ 264 | 265 | print("---DECIDE TO GENERATE---") 266 | state_dict = state["keys"] 267 | question = state_dict["question"] 268 | filtered_documents = state_dict["documents"] 269 | 270 | if not filtered_documents: 271 | # All documents have been filtered check_relevance 272 | # We will re-generate a new query 273 | print("---DECISION: TRANSFORM QUERY---") 274 | return "transform_query" 275 | else: 276 | # We have relevant documents, so generate answer 277 | print("---DECISION: GENERATE---") 278 | return "generate" 279 | 280 | 281 | def grade_generation_v_documents(state): 282 | """ 283 | Determines whether the generation is grounded in the document. 284 | 285 | Args: 286 | state (dict): The current state of the agent, including all keys. 287 | 288 | Returns: 289 | str: Binary decision 290 | """ 291 | 292 | print("---GRADE GENERATION vs DOCUMENTS---") 293 | state_dict = state["keys"] 294 | question = state_dict["question"] 295 | documents = state_dict["documents"] 296 | generation = state_dict["generation"] 297 | 298 | # Data model 299 | class grade(BaseModel): 300 | """Binary score for relevance check.""" 301 | 302 | binary_score: str = Field(description="Supported score 'yes' or 'no'") 303 | 304 | # LLM 305 | model = ChatOpenAI(temperature=0, model="gpt-4-0125-preview", streaming=True) 306 | 307 | # Tool 308 | grade_tool_oai = convert_to_openai_tool(grade) 309 | 310 | # LLM with tool and enforce invocation 311 | llm_with_tool = model.bind( 312 | tools=[convert_to_openai_tool(grade_tool_oai)], 313 | tool_choice={"type": "function", "function": {"name": "grade"}}, 314 | ) 315 | 316 | # Parser 317 | parser_tool = PydanticToolsParser(tools=[grade]) 318 | 319 | # Prompt 320 | prompt = PromptTemplate( 321 | template="""You are a grader assessing whether an answer is grounded in / supported by a set of facts. \n 322 | Here are the facts: 323 | \n ------- \n 324 | {documents} 325 | \n ------- \n 326 | Here is the answer: {generation} 327 | Give a binary score 'yes' or 'no' to indicate whether the answer is grounded in / supported by a set of facts.""", 328 | input_variables=["generation", "documents"], 329 | ) 330 | 331 | # Chain 332 | chain = prompt | llm_with_tool | parser_tool 333 | 334 | score = chain.invoke({"generation": generation, "documents": documents}) 335 | grade = score[0].binary_score 336 | 337 | if grade == "yes": 338 | print("---DECISION: SUPPORTED, MOVE TO FINAL GRADE---") 339 | return "supported" 340 | else: 341 | print("---DECISION: NOT SUPPORTED, GENERATE AGAIN---") 342 | return "not supported" 343 | 344 | 345 | def grade_generation_v_question(state): 346 | """ 347 | Determines whether the generation addresses the question. 348 | 349 | Args: 350 | state (dict): The current state of the agent, including all keys. 351 | 352 | Returns: 353 | str: Binary decision 354 | """ 355 | 356 | print("---GRADE GENERATION vs QUESTION---") 357 | state_dict = state["keys"] 358 | question = state_dict["question"] 359 | documents = state_dict["documents"] 360 | generation = state_dict["generation"] 361 | 362 | # Data model 363 | class grade(BaseModel): 364 | """Binary score for relevance check.""" 365 | 366 | binary_score: str = Field(description="Useful score 'yes' or 'no'") 367 | 368 | # LLM 369 | model = ChatOpenAI(temperature=0, model="gpt-4-0125-preview", streaming=True) 370 | 371 | # Tool 372 | grade_tool_oai = convert_to_openai_tool(grade) 373 | 374 | # LLM with tool and enforce invocation 375 | llm_with_tool = model.bind( 376 | tools=[convert_to_openai_tool(grade_tool_oai)], 377 | tool_choice={"type": "function", "function": {"name": "grade"}}, 378 | ) 379 | 380 | # Parser 381 | parser_tool = PydanticToolsParser(tools=[grade]) 382 | 383 | # Prompt 384 | prompt = PromptTemplate( 385 | template="""You are a grader assessing whether an answer is useful to resolve a question. \n 386 | Here is the answer: 387 | \n ------- \n 388 | {generation} 389 | \n ------- \n 390 | Here is the question: {question} 391 | Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question.""", 392 | input_variables=["generation", "question"], 393 | ) 394 | 395 | # Prompt 396 | chain = prompt | llm_with_tool | parser_tool 397 | 398 | score = chain.invoke({"generation": generation, "question": question}) 399 | grade = score[0].binary_score 400 | 401 | if grade == "yes": 402 | print("---DECISION: USEFUL---") 403 | return "useful" 404 | else: 405 | print("---DECISION: NOT USEFUL---") 406 | return "not useful" 407 | ``` 408 | 409 | 410 | ```python 411 | ### Build Graph 412 | 413 | import pprint 414 | 415 | from langgraph.graph import END, StateGraph 416 | 417 | workflow = StateGraph(GraphState) 418 | 419 | # Define the nodes 420 | workflow.add_node("retrieve", retrieve) # retrieve 421 | workflow.add_node("grade_documents", grade_documents) # grade documents 422 | workflow.add_node("generate", generate) # generatae 423 | workflow.add_node("transform_query", transform_query) # transform_query 424 | workflow.add_node("prepare_for_final_grade", prepare_for_final_grade) # passthrough 425 | 426 | # Build graph 427 | workflow.set_entry_point("retrieve") 428 | workflow.add_edge("retrieve", "grade_documents") 429 | workflow.add_conditional_edges( 430 | "grade_documents", 431 | decide_to_generate, 432 | { 433 | "transform_query": "transform_query", 434 | "generate": "generate", 435 | }, 436 | ) 437 | workflow.add_edge("transform_query", "retrieve") 438 | workflow.add_conditional_edges( 439 | "generate", 440 | grade_generation_v_documents, 441 | { 442 | "supported": "prepare_for_final_grade", 443 | "not supported": "generate", 444 | }, 445 | ) 446 | workflow.add_conditional_edges( 447 | "prepare_for_final_grade", 448 | grade_generation_v_question, 449 | { 450 | "useful": END, 451 | "not useful": "transform_query", 452 | }, 453 | ) 454 | 455 | # Compile 456 | app = workflow.compile() 457 | ``` 458 | 459 | 460 | ```python 461 | ### Run 462 | 463 | inputs = {"keys": {"question": "Explain how the different types of agent memory work?"}} 464 | for output in app.stream(inputs): 465 | for key, value in output.items(): 466 | # Node 467 | pprint.pprint(f"Node '{key}':") 468 | # Optional: print full state at each node 469 | # pprint.pprint(value["keys"], indent=2, width=80, depth=None) 470 | pprint.pprint("\n---\n") 471 | 472 | # Final generation 473 | pprint.pprint(value["keys"]["generation"]) 474 | ``` 475 | 476 | ---RETRIEVE--- 477 | "Node 'retrieve':" 478 | '\n---\n' 479 | ---CHECK RELEVANCE--- 480 | ---GRADE: DOCUMENT RELEVANT--- 481 | ---GRADE: DOCUMENT RELEVANT--- 482 | ---GRADE: DOCUMENT RELEVANT--- 483 | ---GRADE: DOCUMENT RELEVANT--- 484 | "Node 'grade_documents':" 485 | '\n---\n' 486 | ---DECIDE TO GENERATE--- 487 | ---DECISION: GENERATE--- 488 | ---GENERATE--- 489 | "Node 'generate':" 490 | '\n---\n' 491 | ---GRADE GENERATION vs DOCUMENTS--- 492 | ---DECISION: SUPPORTED, MOVE TO FINAL GRADE--- 493 | ---FINAL GRADE--- 494 | "Node 'prepare_for_final_grade':" 495 | '\n---\n' 496 | ---GRADE GENERATION vs QUESTION--- 497 | ---DECISION: USEFUL--- 498 | "Node '__end__':" 499 | '\n---\n' 500 | ('Short-term memory is the stage of memory that stores information that we are ' 501 | 'currently aware of and needed to carry out complex cognitive tasks. It has a ' 502 | 'limited capacity and lasts for a short duration. Long-term memory, on the ' 503 | 'other hand, can store information for a long time and has unlimited storage ' 504 | 'capacity. It includes explicit/declarative memory for facts and events, and ' 505 | 'implicit/procedural memory for unconscious skills and routines.') 506 | 507 | -------------------------------------------------------------------------------- /langgraph_engineer/docs.json: -------------------------------------------------------------------------------- 1 | [{"lc": 1, "type": "constructor", "id": ["langchain", "schema", "document", "Document"], "kwargs": {"page_content": "\n\n\n\n\n\ud83e\udd9c\ud83d\udd78\ufe0fLangGraph | \ud83e\udd9c\ufe0f\ud83d\udd17 Langchain\n\n\n\n\n\n\n\nSkip to main contentDocsUse casesIntegrationsGuidesAPIMorePeopleVersioningChangelogContributingTemplatesCookbooksTutorialsYouTube\ud83e\udd9c\ufe0f\ud83d\udd17LangSmithLangSmith DocsLangServe GitHubTemplates GitHubTemplates HubLangChain HubJS/TS DocsChatSearchGet startedIntroductionInstallationQuickstartSecurityLangChain Expression LanguageGet startedWhy use LCELInterfaceStreamingHow toCookbookLangChain Expression Language (LCEL)ModulesModel I/ORetrievalAgentsChainsMoreLangServeLangSmithLangGraphLangGraphOn this page\ud83e\udd9c\ud83d\udd78\ufe0fLangGraph\u26a1 Building language agents as graphs \u26a1Overview\u200bLangGraph is a library for building stateful, multi-actor applications with LLMs, built on top of (and intended to be used with) LangChain.\nIt extends the LangChain Expression Language with the ability to coordinate multiple chains (or actors) across multiple steps of computation in a cyclic manner.\nIt is inspired by Pregel and Apache Beam.\nThe current interface exposed is one inspired by NetworkX.The main use is for adding cycles to your LLM application.\nCrucially, this is NOT a DAG framework.\nIf you want to build a DAG, you should just use LangChain Expression Language.Cycles are important for agent-like behaviors, where you call an LLM in a loop, asking it what action to take next.Installation\u200bpip install langgraphQuick Start\u200bHere we will go over an example of creating a simple agent that uses chat models and function calling.\nThis agent will represent all its state as a list of messages.We will need to install some LangChain packages, as well as Tavily to use as an example tool.pip install -U langchain langchain_openai tavily-pythonWe also need to export some environment variables for OpenAI and Tavily API access.export OPENAI_API_KEY=sk-...export TAVILY_API_KEY=tvly-...Optionally, we can set up LangSmith for best-in-class observability.export LANGCHAIN_TRACING_V2=\"true\"export LANGCHAIN_API_KEY=ls__...Set up the tools\u200bWe will first define the tools we want to use.\nFor this simple example, we will use a built-in search tool via Tavily.\nHowever, it is really easy to create your own tools - see documentation here on how to do that.from langchain_community.tools.tavily_search import TavilySearchResultstools = [TavilySearchResults(max_results=1)]We can now wrap these tools in a simple LangGraph ToolExecutor.\nThis is a simple class that receives ToolInvocation objects, calls that tool, and returns the output.\nToolInvocation is any class with tool and tool_input attributes.from langgraph.prebuilt import ToolExecutortool_executor = ToolExecutor(tools)Set up the model\u200bNow we need to load the chat model we want to use.\nImportantly, this should satisfy two criteria:It should work with lists of messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.It should work with the OpenAI function calling interface. This means it should either be an OpenAI model or a model that exposes a similar interface.Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.from langchain_openai import ChatOpenAI# We will set streaming=True so that we can stream tokens# See the streaming section for more information on this.model = ChatOpenAI(temperature=0, streaming=True)After we've done this, we should make sure the model knows that it has these tools available to call.\nWe can do this by converting the LangChain tools into the format for OpenAI function calling, and then bind them to the model class.from langchain.tools.render import format_tool_to_openai_functionfunctions = [format_tool_to_openai_function(t) for t in tools]model = model.bind_functions(functions)Define the agent state\u200bThe main type of graph in langgraph is the StatefulGraph.\nThis graph is parameterized by a state object that it passes around to each node.\nEach node then returns operations to update that state.\nThese operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\nWhether to set or add is denoted by annotating the state object you construct the graph with.For this example, the state we will track will just be a list of messages.\nWe want each node to just add messages to that list.\nTherefore, we will use a TypedDict with one key (messages) and annotate it so that the messages attribute is always added to.from typing import TypedDict, Annotated, Sequenceimport operatorfrom langchain_core.messages import BaseMessageclass AgentState(TypedDict): messages: Annotated[Sequence[BaseMessage], operator.add]Define the nodes\u200bWe now need to define a few different nodes in our graph.\nIn langgraph, a node can be either a function or a runnable.\nThere are two main nodes we need for this:The agent: responsible for deciding what (if any) actions to take.A function to invoke tools: if the agent decides to take an action, this node will then execute that action.We will also need to define some edges.\nSome of these edges may be conditional.\nThe reason they are conditional is that based on the output of a node, one of several paths may be taken.\nThe path that is taken is not known until that node is run (the LLM decides).Conditional Edge: after the agent is called, we should either:a. If the agent said to take an action, then the function to invoke tools should be calledb. If the agent said that it was finished, then it should finishNormal Edge: after the tools are invoked, it should always go back to the agent to decide what to do nextLet's define the nodes, as well as a function to decide how what conditional edge to take.from langgraph.prebuilt import ToolInvocationimport jsonfrom langchain_core.messages import FunctionMessage# Define the function that determines whether to continue or notdef should_continue(state): messages = state['messages'] last_message = messages[-1] # If there is no function call, then we finish if \"function_call\" not in last_message.additional_kwargs: return \"end\" # Otherwise if there is, we continue else: return \"continue\"# Define the function that calls the modeldef call_model(state): messages = state['messages'] response = model.invoke(messages) # We return a list, because this will get added to the existing list return {\"messages\": [response]}# Define the function to execute toolsdef call_tool(state): messages = state['messages'] # Based on the continue condition # we know the last message involves a function call last_message = messages[-1] # We construct an ToolInvocation from the function_call action = ToolInvocation( tool=last_message.additional_kwargs[\"function_call\"][\"name\"], tool_input=json.loads(last_message.additional_kwargs[\"function_call\"][\"arguments\"]), ) # We call the tool_executor and get back a response response = tool_executor.invoke(action) # We use the response to create a FunctionMessage function_message = FunctionMessage(content=str(response), name=action.tool) # We return a list, because this will get added to the existing list return {\"messages\": [function_message]}Define the graph\u200bWe can now put it all together and define the graph!from langgraph.graph import StateGraph, END# Define a new graphworkflow = StateGraph(AgentState)# Define the two nodes we will cycle betweenworkflow.add_node(\"agent\", call_model)workflow.add_node(\"action\", call_tool)# Set the entrypoint as `agent`# This means that this node is the first one calledworkflow.set_entry_point(\"agent\")# We now add a conditional edgeworkflow.add_conditional_edges( # First, we define the start node. We use `agent`. # This means these are the edges taken after the `agent` node is called. \"agent\", # Next, we pass in the function that will determine which node is called next. should_continue, # Finally we pass in a mapping. # The keys are strings, and the values are other nodes. # END is a special node marking that the graph should finish. # What will happen is we will call `should_continue`, and then the output of that # will be matched against the keys in this mapping. # Based on which one it matches, that node will then be called. { # If `tools`, then we call the tool node. \"continue\": \"action\", # Otherwise we finish. \"end\": END })# We now add a normal edge from `tools` to `agent`.# This means that after `tools` is called, `agent` node is called next.workflow.add_edge('action', 'agent')# Finally, we compile it!# This compiles it into a LangChain Runnable,# meaning you can use it as you would any other runnableapp = workflow.compile()Use it!\u200bWe can now use it!\nThis now exposes the same interface as all other LangChain runnables.\nThis runnable accepts a list of messages.from langchain_core.messages import HumanMessageinputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}app.invoke(inputs)This may take a little bit - it's making a few calls behind the scenes.\nIn order to start seeing some intermediate results as they happen, we can use streaming - see below for more information on that.Streaming\u200bLangGraph has support for several different types of streaming.Streaming Node Output\u200bOne of the benefits of using LangGraph is that it is easy to stream output as it's produced by each node.inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}for output in app.stream(inputs): # stream() yields dictionaries with output keyed by node name for key, value in output.items(): print(f\"Output from node '{key}':\") print(\"---\") print(value) print(\"\\n---\\n\")Output from node 'agent':---{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}})]}---Output from node 'action':---{'messages': [FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json')]}---Output from node 'agent':---{'messages': [AIMessage(content=\"I couldn't find the current weather in San Francisco. However, you can visit [WeatherSpark](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States) to check the historical weather data for January 2024 in San Francisco.\")]}---Output from node '__end__':---{'messages': [HumanMessage(content='what is the weather in sf'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"query\": \"weather in San Francisco\"\\n}', 'name': 'tavily_search_results_json'}}), FunctionMessage(content=\"[{'url': 'https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States', 'content': 'January 2024 Weather History in San Francisco California, United States Daily Precipitation in January 2024 in San Francisco Observed Weather in January 2024 in San Francisco San Francisco Temperature History January 2024 Hourly Temperature in January 2024 in San Francisco Hours of Daylight and Twilight in January 2024 in San FranciscoThis report shows the past weather for San Francisco, providing a weather history for January 2024. It features all historical weather data series we have available, including the San Francisco temperature history for January 2024. You can drill down from year to month and even day level reports by clicking on the graphs.'}]\", name='tavily_search_results_json'), AIMessage(content=\"I couldn't find the current weather in San Francisco. However, you can visit [WeatherSpark](https://weatherspark.com/h/m/557/2024/1/Historical-Weather-in-January-2024-in-San-Francisco-California-United-States) to check the historical weather data for January 2024 in San Francisco.\")]}---Streaming LLM Tokens\u200bYou can also access the LLM tokens as they are produced by each node.\nIn this case only the \"agent\" node produces LLM tokens.\nIn order for this to work properly, you must be using an LLM that supports streaming as well as have set it when constructing the LLM (e.g. ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True))inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}async for output in app.astream_log(inputs, include_types=[\"llm\"]): # astream_log() yields the requested logs (here LLMs) in JSONPatch format for op in output.ops: if op[\"path\"] == \"/streamed_output/-\": # this is the output from .stream() ... elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith( \"/streamed_output/-\" ): # because we chose to only include LLMs, these are LLM tokens print(op[\"value\"])content='' additional_kwargs={'function_call': {'arguments': '', 'name': 'tavily_search_results_json'}}content='' additional_kwargs={'function_call': {'arguments': '{\\n', 'name': ''}}content='' additional_kwargs={'function_call': {'arguments': ' ', 'name': ''}}content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}content='' additional_kwargs={'function_call': {'arguments': 'query', 'name': ''}}content='' additional_kwargs={'function_call': {'arguments': '\":', 'name': ''}}content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}content='' additional_kwargs={'function_call': {'arguments': 'weather', 'name': ''}}content='' additional_kwargs={'function_call': {'arguments': ' in', 'name': ''}}content='' additional_kwargs={'function_call': {'arguments': ' San', 'name': ''}}content='' additional_kwargs={'function_call': {'arguments': ' Francisco', 'name': ''}}content='' additional_kwargs={'function_call': {'arguments': '\"\\n', 'name': ''}}content='' additional_kwargs={'function_call': {'arguments': '}', 'name': ''}}content=''content=''content='I'content=\"'m\"content=' sorry'content=','content=' but'content=' I'content=' couldn'content=\"'t\"content=' find'content=' the'content=' current'content=' weather'content=' in'content=' San'content=' Francisco'content='.'content=' However'content=','content=' you'content=' can'content=' check'content=' the'content=' historical'content=' weather'content=' data'content=' for'content=' January'content=' 'content='202'content='4'content=' in'content=' San'content=' Francisco'content=' ['content='here'content=']('content='https'content='://'content='we'content='athers'content='park'content='.com'content='/h'content='/m'content='/'content='557'content='/'content='202'content='4'content='/'content='1'content='/H'content='istorical'content='-'content='Weather'content='-in'content='-Jan'content='uary'content='-'content='202'content='4'content='-in'content='-S'content='an'content='-F'content='r'content='anc'content='isco'content='-Cal'content='ifornia'content='-'content='United'content='-'content='States'content=').'content=''When to Use\u200bWhen should you use this versus LangChain Expression Language?If you need cycles.Langchain Expression Language allows you to easily define chains (DAGs) but does not have a good mechanism for adding in cycles.\nlanggraph adds that syntax.Examples\u200bChatAgentExecutor: with function calling\u200bThis agent executor takes a list of messages as input and outputs a list of messages.\nAll agent state is represented as a list of messages.\nThis specifically uses OpenAI function calling.\nThis is recommended agent executor for newer chat based models that support function calling.Getting Started Notebook: Walks through creating this type of executor from scratchHigh Level Entrypoint: Walks through how to use the high level entrypoint for the chat agent executor.ModificationsWe also have a lot of examples highlighting how to slightly modify the base chat agent executor. These all build off the getting started notebook so it is recommended you start with that first.Human-in-the-loop: How to add a human-in-the-loop componentForce calling a tool first: How to always call a specific tool firstRespond in a specific format: How to force the agent to respond in a specific formatDynamically returning tool output directly: How to dynamically let the agent choose whether to return the result of a tool directly to the userManaging agent steps: How to more explicitly manage intermediate steps that an agent takesAgentExecutor\u200bThis agent executor uses existing LangChain agents.Getting Started Notebook: Walks through creating this type of executor from scratchHigh Level Entrypoint: Walks through how to use the high level entrypoint for the chat agent executor.ModificationsWe also have a lot of examples highlighting how to slightly modify the base chat agent executor. These all build off the getting started notebook so it is recommended you start with that first.Human-in-the-loop: How to add a human-in-the-loop componentForce calling a tool first: How to always call a specific tool firstManaging agent steps: How to more explicitly manage intermediate steps that an agent takesAsync\u200bIf you are running LangGraph in async workflows, you may want to create the nodes to be async by default.\nFor a walkthrough on how to do that, see this documentationStreaming Tokens\u200bSometimes language models take a while to respond and you may want to stream tokens to end users.\nFor a guide on how to do this, see this documentationPersistence\u200bLangGraph comes with built-in persistence, allowing you to save the state of the graph at point and resume from there.\nFor a walkthrough on how to do that, see this documentationHuman-in-the-loop\u200bLangGraph comes with built-in support for human-in-the-loop workflows. This is useful when you want to have a human review the current state before proceeding to a particular node.\nFor a walkthrough on how to do that, see this documentationPlanning Agent Examples\u200bThe following notebooks implement agent architectures prototypical of the \"plan-and-execute\" style, where an LLM planner decomposes a user request into a program, an executor executes the program, and an LLM synthesizes a response (and/or dynamically replans) based on the program outputs.Plan-and-execute: a simple agent with a planner that generates a multi-step task list, an executor that invokes the tools in the plan, and a replanner that responds or generates an updated plan. Based on the Plan-and-solve paper by Wang, et. al.Reasoning without Observation: planner generates a task list whose observations are saved as variables. Variables can be used in subsequent tasks to reduce the need for further re-planning. Based on the ReWOO paper by Xu, et. al.LLMCompiler: planner generates a DAG of tasks with variable responses. Tasks are streamed and executed eagerly to minimize tool execution runtime. Based on the paper by Kim, et. al.Reflection / Self-Critique\u200bWhen output quality is a major concern, it's common to incorporate some combination of self-critique or reflection and external validation to refine your system's outputs. The following examples demonstrate research that implement this type of design.Basic Reflection: add a simple \"reflect\" step in your graph to prompt your system to revise its outputs.Reflexion: critique missing and superflous aspects of the agent's response to guide subsequent steps. Based on Reflexion, by Shinn, et. al.Language Agent Tree Search: execute multiple agents in parallel, using reflection and environmental rewards to drive a Monte Carlo Tree Search. Based on LATS, by Zhou, et. al.Multi-agent Examples\u200bMulti-agent collaboration: how to create two agents that work together to accomplish a taskMulti-agent with supervisor: how to orchestrate individual agents by using an LLM as a \"supervisor\" to distribute workHierarchical agent teams: how to orchestrate \"teams\" of agents as nested graphs that can collaborate to solve a problemChatbot Evaluation via Simulation\u200bIt can often be tough to evaluation chat bots in multi-turn situations. One way to do this is with simulations.Chat bot evaluation as multi-agent simulation: how to simulate a dialogue between a \"virtual user\" and your chat botMultimodal Examples\u200bWebVoyager: vision-enabled web browsing agent that uses Set-of-marks prompting to navigate a web browser and execute tasksDocumentation\u200bThere are only a few new APIs to use.StateGraph\u200bThe main entrypoint is StateGraph.from langgraph.graph import StateGraphThis class is responsible for constructing the graph.\nIt exposes an interface inspired by NetworkX.\nThis graph is parameterized by a state object that it passes around to each node.__init__\u200b def __init__(self, schema: Type[Any]) -> None:When constructing the graph, you need to pass in a schema for a state.\nEach node then returns operations to update that state.\nThese operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\nWhether to set or add is denoted by annotating the state object you construct the graph with.The recommended way to specify the schema is with a typed dictionary: from typing import TypedDictYou can then annotate the different attributes using from typing imoport Annotated.\nCurrently, the only supported annotation is import operator; operator.add.\nThis annotation will make it so that any node that returns this attribute ADDS that new result to the existing value.Let's take a look at an example:from typing import TypedDict, Annotated, Unionfrom langchain_core.agents import AgentAction, AgentFinishimport operatorclass AgentState(TypedDict): # The input string input: str # The outcome of a given call to the agent # Needs `None` as a valid type, since this is what this will start as agent_outcome: Union[AgentAction, AgentFinish, None] # List of actions and corresponding observations # Here we annotate this with `operator.add` to indicate that operations to # this state should be ADDED to the existing values (not overwrite it) intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]We can then use this like:# Initialize the StateGraph with this stategraph = StateGraph(AgentState)# Create nodes and edges...# Compile the graphapp = graph.compile()# The inputs should be a dictionary, because the state is a TypedDictinputs = { # Let's assume this the input \"input\": \"hi\" # Let's assume agent_outcome is set by the graph as some point # It doesn't need to be provided, and it will be None by default # Let's assume `intermediate_steps` is built up over time by the graph # It doesn't need to provided, and it will be empty list by default # The reason `intermediate_steps` is an empty list and not `None` is because # it's annotated with `operator.add`}.add_node\u200b def add_node(self, key: str, action: RunnableLike) -> None:This method adds a node to the graph.\nIt takes two arguments:key: A string representing the name of the node. This must be unique.action: The action to take when this node is called. This should either be a function or a runnable..add_edge\u200b def add_edge(self, start_key: str, end_key: str) -> None:Creates an edge from one node to the next.\nThis means that output of the first node will be passed to the next node.\nIt takes two arguments.start_key: A string representing the name of the start node. This key must have already been registered in the graph.end_key: A string representing the name of the end node. This key must have already been registered in the graph..add_conditional_edges\u200b def add_conditional_edges( self, start_key: str, condition: Callable[..., str], conditional_edge_mapping: Dict[str, str], ) -> None:This method adds conditional edges.\nWhat this means is that only one of the downstream edges will be taken, and which one that is depends on the results of the start node.\nThis takes three arguments:start_key: A string representing the name of the start node. This key must have already been registered in the graph.condition: A function to call to decide what to do next. The input will be the output of the start node. It should return a string that is present in conditional_edge_mapping and represents the edge to take.conditional_edge_mapping: A mapping of string to string. The keys should be strings that may be returned by condition. The values should be the downstream node to call if that condition is returned..set_entry_point\u200b def set_entry_point(self, key: str) -> None:The entrypoint to the graph.\nThis is the node that is first called.\nIt only takes one argument:key: The name of the node that should be called first..set_finish_point\u200b def set_finish_point(self, key: str) -> None:This is the exit point of the graph.\nWhen this node is called, the results will be the final result from the graph.\nIt only has one argument:key: The name of the node that, when called, will return the results of calling it as the final outputNote: This does not need to be called if at any point you previously created an edge (conditional or normal) to ENDGraph\u200bfrom langgraph.graph import Graphgraph = Graph()This has the same interface as StateGraph with the exception that it doesn't update a state object over time, and rather relies on passing around the full state from each step.\nThis means that whatever is returned from one node is the input to the next as is.END\u200bfrom langgraph.graph import ENDThis is a special node representing the end of the graph.\nThis means that anything passed to this node will be the final output of the graph.\nIt can be used in two places:As the end_key in add_edgeAs a value in conditional_edge_mapping as passed to add_conditional_edgesPrebuilt Examples\u200bThere are also a few methods we've added to make it easy to use common, prebuilt graphs and components.ToolExecutor\u200bfrom langgraph.prebuilt import ToolExecutorThis is a simple helper class to help with calling tools.\nIt is parameterized by a list of tools:tools = [...]tool_executor = ToolExecutor(tools)It then exposes a runnable interface.\nIt can be used to call tools: you can pass in an AgentAction and it will look up the relevant tool and call it with the appropriate input.chat_agent_executor.create_function_calling_executor\u200bfrom langgraph.prebuilt import chat_agent_executorThis is a helper function for creating a graph that works with a chat model that utilizes function calling.\nCan be created by passing in a model and a list of tools.\nThe model must be one that supports OpenAI function calling.from langchain_openai import ChatOpenAIfrom langchain_community.tools.tavily_search import TavilySearchResultsfrom langgraph.prebuilt import chat_agent_executorfrom langchain_core.messages import HumanMessagetools = [TavilySearchResults(max_results=1)]model = ChatOpenAI()app = chat_agent_executor.create_function_calling_executor(model, tools)inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}for s in app.stream(inputs): print(list(s.values())[0]) print(\"----\")create_agent_executor\u200bfrom langgraph.prebuilt import create_agent_executorThis is a helper function for creating a graph that works with LangChain Agents.\nCan be created by passing in an agent and a list of tools.from langgraph.prebuilt import create_agent_executorfrom langchain_openai import ChatOpenAIfrom langchain import hubfrom langchain.agents import create_openai_functions_agentfrom langchain_community.tools.tavily_search import TavilySearchResultstools = [TavilySearchResults(max_results=1)]# Get the prompt to use - you can modify this!prompt = hub.pull(\"hwchase17/openai-functions-agent\")# Choose the LLM that will drive the agentllm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\")# Construct the OpenAI Functions agentagent_runnable = create_openai_functions_agent(llm, tools, prompt)app = create_agent_executor(agent_runnable, tools)inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}for s in app.stream(inputs): print(list(s.values())[0]) print(\"----\")PreviousLangSmith WalkthroughOverviewInstallationQuick StartSet up the toolsSet up the modelDefine the agent stateDefine the nodesDefine the graphUse it!StreamingStreaming Node OutputStreaming LLM TokensWhen to UseExamplesChatAgentExecutor: with function callingAgentExecutorAsyncStreaming TokensPersistenceHuman-in-the-loopPlanning Agent ExamplesReflection / Self-CritiqueMulti-agent ExamplesChatbot Evaluation via SimulationMultimodal ExamplesDocumentationStateGraphGraphENDPrebuilt ExamplesToolExecutorchat_agent_executor.create_function_calling_executorcreate_agent_executorCommunityDiscordTwitterGitHubPythonJS/TSMoreHomepageBlogYouTubeCopyright \u00a9 2024 LangChain, Inc.\n\n\n\n", "metadata": {"source": "https://python.langchain.com/docs/langgraph/", "title": "\ud83e\udd9c\ud83d\udd78\ufe0fLangGraph | \ud83e\udd9c\ufe0f\ud83d\udd17 Langchain", "description": "\u26a1 Building language agents as graphs \u26a1", "language": "en"}}}] --------------------------------------------------------------------------------