import{CheerioWebBaseLoader}from"@langchain/community/document_loaders/web/cheerio";import{RecursiveCharacterTextSplitter}from"@langchain/textsplitters";import{MemoryVectorStore}from"langchain/vectorstores/memory";import{OpenAIEmbeddings}from"@langchain/openai";consturls=["https://lilianweng.github.io/posts/2023-06-23-agent/","https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/","https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/",];constdocs=awaitPromise.all(urls.map((url)=>newCheerioWebBaseLoader(url).load()),);constdocsList=docs.flat();consttextSplitter=newRecursiveCharacterTextSplitter({chunkSize:250,chunkOverlap:0,});constdocSplits=awaittextSplitter.splitDocuments(docsList);// Add to vectorDBconstvectorStore=awaitMemoryVectorStore.fromDocuments(docSplits,newOpenAIEmbeddings(),);constretriever=vectorStore.asRetriever();
We can access this from any graph node as state.key.
import{Annotation}from"@langchain/langgraph";import{DocumentInterface}from"@langchain/core/documents";// Represents the state of our graph.constGraphState=Annotation.Root({documents:Annotation<DocumentInterface[]>({reducer:(x,y)=>y??x??[],}),question:Annotation<string>({reducer:(x,y)=>y??x??"",}),generation:Annotation<string>({reducer:(x,y)=>y??x,}),});
Let's use query re-writing to optimize the query for web search.
Here is our graph flow:
import{TavilySearchResults}from"@langchain/community/tools/tavily_search";import{Document}from"@langchain/core/documents";import{z}from"zod";import{ChatPromptTemplate}from"@langchain/core/prompts";import{pull}from"langchain/hub";import{ChatOpenAI}from"@langchain/openai";import{StringOutputParser}from"@langchain/core/output_parsers";import{formatDocumentsAsString}from"langchain/util/document";// Define the LLM once. We'll reuse it throughout the graph.constmodel=newChatOpenAI({model:"gpt-4o",temperature:0,});/** * Retrieve documents * * @param {typeof GraphState.State} state The current state of the graph. * @param {RunnableConfig | undefined} config The configuration object for tracing. * @returns {Promise<Partial<typeof GraphState.State>>} The new state object. */asyncfunctionretrieve(state:typeofGraphState.State):Promise<Partial<typeofGraphState.State>>{console.log("---RETRIEVE---");constdocuments=awaitretriever.withConfig({runName:"FetchRelevantDocuments"}).invoke(state.question);return{documents,};}/** * Generate answer * * @param {typeof GraphState.State} state The current state of the graph. * @param {RunnableConfig | undefined} config The configuration object for tracing. * @returns {Promise<Partial<typeof GraphState.State>>} The new state object. */asyncfunctiongenerate(state:typeofGraphState.State):Promise<Partial<typeofGraphState.State>>{console.log("---GENERATE---");constprompt=awaitpull<ChatPromptTemplate>("rlm/rag-prompt");// Construct the RAG chain by piping the prompt, model, and output parserconstragChain=prompt.pipe(model).pipe(newStringOutputParser());constgeneration=awaitragChain.invoke({context:formatDocumentsAsString(state.documents),question:state.question,});return{generation,};}/** * Determines whether the retrieved documents are relevant to the question. * * @param {typeof GraphState.State} state The current state of the graph. * @param {RunnableConfig | undefined} config The configuration object for tracing. * @returns {Promise<Partial<typeof GraphState.State>>} The new state object. */asyncfunctiongradeDocuments(state:typeofGraphState.State):Promise<Partial<typeofGraphState.State>>{console.log("---CHECK RELEVANCE---");// pass the name & schema to `withStructuredOutput` which will force the model to call this tool.constllmWithTool=model.withStructuredOutput(z.object({binaryScore:z.enum(["yes","no"]).describe("Relevance score 'yes' or 'no'"),}).describe("Grade the relevance of the retrieved documents to the question. Either 'yes' or 'no'."),{name:"grade",});constprompt=ChatPromptTemplate.fromTemplate(`You are a grader assessing relevance of a retrieved document to a user question. Here is the retrieved document: {context} Here is the user question: {question} If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.`);// Chainconstchain=prompt.pipe(llmWithTool);constfilteredDocs:Array<DocumentInterface>=[];forawait(constdocofstate.documents){constgrade=awaitchain.invoke({context:doc.pageContent,question:state.question,});if(grade.binaryScore==="yes"){console.log("---GRADE: DOCUMENT RELEVANT---");filteredDocs.push(doc);}else{console.log("---GRADE: DOCUMENT NOT RELEVANT---");}}return{documents:filteredDocs,};}/** * Transform the query to produce a better question. * * @param {typeof GraphState.State} state The current state of the graph. * @param {RunnableConfig | undefined} config The configuration object for tracing. * @returns {Promise<Partial<typeof GraphState.State>>} The new state object. */asyncfunctiontransformQuery(state:typeofGraphState.State):Promise<Partial<typeofGraphState.State>>{console.log("---TRANSFORM QUERY---");// Pull in the promptconstprompt=ChatPromptTemplate.fromTemplate(`You are generating a question that is well optimized for semantic search retrieval. Look at the input and try to reason about the underlying sematic intent / meaning. Here is the initial question: \n ------- \n {question} \n ------- \n Formulate an improved question: `);// Promptconstchain=prompt.pipe(model).pipe(newStringOutputParser());constbetterQuestion=awaitchain.invoke({question:state.question});return{question:betterQuestion,};}/** * Web search based on the re-phrased question using Tavily API. * * @param {typeof GraphState.State} state The current state of the graph. * @param {RunnableConfig | undefined} config The configuration object for tracing. * @returns {Promise<Partial<typeof GraphState.State>>} The new state object. */asyncfunctionwebSearch(state:typeofGraphState.State):Promise<Partial<typeofGraphState.State>>{console.log("---WEB SEARCH---");consttool=newTavilySearchResults();constdocs=awaittool.invoke({input:state.question});constwebResults=newDocument({pageContent:docs});constnewDocuments=state.documents.concat(webResults);return{documents:newDocuments,};}/** * Determines whether to generate an answer, or re-generate a question. * * @param {typeof GraphState.State} state The current state of the graph. * @returns {"transformQuery" | "generate"} Next node to call */functiondecideToGenerate(state:typeofGraphState.State){console.log("---DECIDE TO GENERATE---");constfilteredDocs=state.documents;if(filteredDocs.length===0){// All documents have been filtered checkRelevance// We will re-generate a new queryconsole.log("---DECISION: TRANSFORM QUERY---");return"transformQuery";}// We have relevant documents, so generate answerconsole.log("---DECISION: GENERATE---");return"generate";}
The just follows the flow we outlined in the figure above.
import{END,START,StateGraph}from"@langchain/langgraph";constworkflow=newStateGraph(GraphState)// Define the nodes.addNode("retrieve",retrieve).addNode("gradeDocuments",gradeDocuments).addNode("generate",generate).addNode("transformQuery",transformQuery).addNode("webSearch",webSearch);// Build graphworkflow.addEdge(START,"retrieve");workflow.addEdge("retrieve","gradeDocuments");workflow.addConditionalEdges("gradeDocuments",decideToGenerate,);workflow.addEdge("transformQuery","webSearch");workflow.addEdge("webSearch","generate");workflow.addEdge("generate",END);// Compileconstapp=workflow.compile();
constinputs={question:"Explain how the different types of agent memory work.",};constconfig={recursionLimit:50};letfinalGeneration;forawait(constoutputofawaitapp.stream(inputs,config)){for(const[key,value]ofObject.entries(output)){console.log(`Node: '${key}'`);// Optional: log full state at each node// console.log(JSON.stringify(value, null, 2));finalGeneration=value;}console.log("\n---\n");}// Log the final generation.console.log(JSON.stringify(finalGeneration,null,2));
---RETRIEVE---Node: 'retrieve'------CHECK RELEVANCE------GRADE: DOCUMENT RELEVANT------GRADE: DOCUMENT NOT RELEVANT------GRADE: DOCUMENT NOT RELEVANT------GRADE: DOCUMENT RELEVANT------DECIDE TO GENERATE------DECISION: GENERATE---Node: 'gradeDocuments'------GENERATE---Node: 'generate'---{ "generation": "Different types of agent memory include long-term memory, which allows the agent to retain and recall information over extended periods, often using an external vector store for fast retrieval. This enables the agent to remember and utilize vast amounts of information efficiently."}