Developing Applications with LangChain | Master LLMs, RAG, and Agentic Workflows

Adele, Jonathan Bennion, Mary Nova, and Delini Sumanapala walk through building AI applications with LangChain. You'll learn prompt engineering, multi-step chains, React agents, RAG with document chunking and vector stores, graph-based retrieval with Neo4j, and chatbots with LangGraph that use external tools and memory management.
Chapters
What's up everybody? Adele here. Today's video is a collection of course videos from our track on developing applications with LangChain. In these videos, you'll learn all about retrieval, augmented generation with LangChain, including how to use Rag, graph rag as well as other techniques, as well as building Agentix systems using LangChain and more. These videos will only cover the video portion of the courses, so if you're looking to practice your skills right in the browser, make sure to check out the link in the description below.
With that happy learning Hello, I'm Jonathan Bennion, an AI engineer and active contributor to LangChain. Welcome to the course. We now live in a world where we're spoiled for choice when it comes to choosing large language models or LLMs for developing AI powered applications. However, these LLMs differ based on their model architecture, training data, intended use cases, and prompting strategies. Additionally, these models often have to interact with other systems to retrieve data or monitor performance, which adds another layer of complexity. LangChain is an open source framework that helps developers connect LLMs, data sources and other functionality under a single unified syntax. With LangChain, developers can create scalable modular LLM applications with greater ease. This course will cover LangChain and Python, but libraries also exist for JavaScript. LangChain encompasses an entire ecosystem of tools, but in this course we'll focus on the core components of the LangChain LLMs, including open source and proprietary models, prompts, chains, agents and document retrievers. Throughout the course you'll see how these LangChain components can enable some truly awesome LLM applications. Let's first discuss LLMs. Hugging face is a huge repository of open source data sets, tools and Most importantly for US models. Accessing LLMs hosted on hugging Face is free, but using them in LangChain requires creating a Hugging Face API key to create one. Log in or create a Hugging Face account and navigate to the URL shown under Settings here, click on New Token and copy your key. Datacamp won't store the API keys you'll need in this course so they can be directly copied into the exercises. Now we have our key. Let's use LangChain to use a model from Hugging Face and compare it to an OpenAI model. LangChain has OpenAI and Hugging Face classes for interacting with the respective APIs. Once imported, we define the LLM with the model name and API key from Hugging Face, we're using the Falcon 7 billion parameter instruction optimized model. We'll define an unfinished sentence and use both models to predict the next words. Finally, let's print the result to see the output. Compare the two different approaches despite using completely different models from different APIs, LangChain unifies them both into a consistent modular workflow. To summarize, LangChain is a fantastic tool for developing and orchestrating natural language systems in the real world of development for production. It unlocks the ability for intelligent conversations with documents, more opportunities for task automation, and different ways to analyze text data. LangChain makes implementing AI more intuitive and gives us greater control over the entire workflow. Note that we will use a specific LangChain version for this course. If you wish to use a newer version on your own system, be sure to check the LangChain docs for any changes. Welcome back. Let's use LangChain to start implementing prompting strategies for chatbots thousands of LLMs are available in LangChain via the Hugging Face Hub API. To find language models specifically optimized for chat, search the Models section of Hugging Face and filter on question answering. Then take note of the model name so it can be referenced in the code. New models are constantly being released on Hugging Face. Many are also fine tuned on domain specific datasets so they are better at capturing the nuance of a particular region, culture, or task. So it's worthwhile to spend time searching Hugging Face for the most appropriate model. Once we've selected a model, we can begin prompting it by utilizing prompt templates. Prompt templates act as reusable recipes for generating prompts from user inputs in a flexible and modular way. Templates can include instructions, examples, or any additional context that might help the model complete the task. Prompt templates are created using LangChain's prompttemplate class. We start by creating a template string which is structured to prompt the AI to answer a question. The curly braces indicate that we'll use dynamic insertion to insert a variable into the string later in the code. To convert this string into a prompt template, we pass it to prompttemplate, specifying any variables representing inputs using the input variables argument. To show how a variable will be inserted, let's call the Invoke method on the prompt template and pass it a dictionary. To map values to input variables, we see in the input how the question placeholder has been replaced. Let's integrate this with an LLM. We start by choosing a question answering LLM from Hugging Face. To integrate the prompt template and model, we use LangChain Expression Language or lsel. Using a pipe creates a chain which in LangChain are used to connect a series of calls to different components into a sequence. To pass an input to this chain, we call the invoke method, again with the same dictionary as before. This passes the question into the prompt template, then passes the prompt template into the model for a response. Chat models have a different prompt template class Chat Prompt Template this allows us to specify a series of messages to pass to the chat model. This is structured as a list of tuples where each tuple contains a role and message pair. This list is then passed to the FromMessages method to create the template. In this example we can see three roles being system, human, and AI. The system rule is used to define the model behavior. The human role is used for providing inputs, and the AI role is used for defining outputs, which is often used to provide additional examples for the model. The ChatOpenAI class is used to access OpenAI's chat models. When instantiating the model, make sure to provide an OpenAI API key. We create our chain again using an LSL pipe, define a user input and and invoke the chain as before. The output is pretty long, but we could tweak our prompt templates to change this welcome back in this video, we'll take our LLM prompting skills up a level and Master Few Shot Prompting so far we've used Prompt Template and Chat Prompt template to create reusable templates for different prompt inputs. These classes are great for handling prompts containing a single example, but they don't scale well if we need to integrate many examples from a data set. The Few shotpromptemplate class allows us to convert datasets like these into prompt templates to provide more context to the model. Let's say we have a list of dictionaries containing questions and answers. If we have another data structure like a PANDAS dataframe, there's usually a simple transformation to get to this point, like the TODICT method. In this case, before we dive in with creating the Few shot prompt template, we need to decide how we want to structure the examples for the model. We create the prompt template using the prompttemplate class we've used before to specify how the questions and answers should be formatted. Invoking this template with an example question and answer, we can see the question prefix was added and a new line was inserted. Now to put everything together, Few Shot Prompt template takes the examples, list of dictionaries we created, and the template for formatting the examples. Additionally, we can provide a suffix which is used to format the user input and specify what variable the user input will be assigned to. Let's see our prompt in action. We invoke the prompt template with an example user input and extract the text from the resulting prompt. The prompt correctly appended question to the user input, displaying it underneath the example Questions and answers now let's test this prompt template is actually functional in an LLM chain. We instantiate our model and chain the prompt template and model together using the pipe operator from lsel. The model response can be extracted from the response object via the content attribute, which shows the model was able to use the context provided in our few shot prompt. Let's take our LSL chains to the next level. With sequential chains, some problems can only be solved sequentially. Consider a chatbot used to create a travel itinerary. We need to tell the chatbot our destination, receive suggestions on what to see on our trip, and tell the model which activities to select to compile the itinerary. This is a sequential problem as it requires more than one user input, one to specify the destination and another to select the activities. Let's code this out in sequential chains. The output from one chain becomes the input to another. We'll create two prompt templates, one to generate suggestions for activities from the input destination and another to create an itinerary for one day of activities. For from the model's top three suggestions, we define our model and begin our sequential chain. We start by defining a dictionary that passes our destination prompt template to the LLM and parses the output to a string, all using LSEL's pipe. This gets assigned to the activities key, which is important as this is the input variable. To the second prompt template. We pipe the first chain into the second prompt template, then into the LLM and again parse to a string. We also wrap the sequential chain in parentheses so we can split this code across multiple lines. To summarize, the destination prompt is passed to the LLM to generate the activity suggestions, and the output is parsed to a string and assigned to activities. This is passed to the second activities prompt, which is passed to the LLM to generate the itinerary which is parsed as a string. Let's invoke the chain passing roam as our input destination. The model considered that we only had one day to explore and wove in its top suggestions of the Colosseum and Vatican City. Now that we're confident with LLMs, it's time to move on to a hot area of AI Application Agents in LangChain Agents use language models to determine actions. Agents often use tools, which are functions called by the agent to interact with the system. These tools can be high level utilities to transform inputs, or they can be task specific. Agents can even use chains and other agents as tools. In this video we'll discuss a type of agent called React agents. React stands for reasoning and acting and this is exactly how the agent operates. It prompts the model using a repeated loop of thinking, acting and observing. If we were to ask a react agent that had access to a weather tool what is the weather like in Kingston, Jamaica, it would start by thinking about the task and which tool to call. Call that tool using the information and observe the results from the tool call. To implement agents, we'll be using Langgraph, which is a branch of the LangChain ecosystem, specifically for designing agentic systems or systems including agents. Like LangChain's core library, it is built to provide a unified tool agnostic syntax. We'll be using the following version. In this course we'll create a react agent that can solve math problems, something most LLMs struggle with. We import Create React agent from Lane Graph and the Load Tools function. We initialize our LLM and load the LLM math tool using the Load Tools function to create the agent. We pass the LLM and tools to create React Agent. Just like chains, agents can be executed with the Invoke method. Here we pass the CHAT model a message to find the square root of 101, which isn't a whole number. Let's see how the agent approaches the problem. There's a lot of metadata in the output, so we've trimmed it for brevity. We can see that executing the agent resulted in a series of messages. The first is our prompt defining the problem. The second is created by the model to identify the tool to use and to convert our query into mathematical format. The third is the result of the tool call and the final message is the model's response after observing the tool's answer, which it decided to round to two decimal places. If we just want the final response, we can subset the final message and extract its content with the content attribute. Now let's begin implementing agents. Now that we've created our first agent, let's take a closer look at tools so we can design our own tools in LangChain must be formatted in a specific way to be compatible with agents. They must have a name accessible via the name attribute, a description which is used by the LLM to determine when to call the tool. In this case, if the LLM interprets the task as being a math problem, it will likely call this tool based on its description. Finally, the return direct parameter indicates whether the agent should stop after invoking this tool, which it won't in this case. Understanding this required format will help us to understand how to create our own tools. Let's say we want to define a Python function to generate a financial report for a company. It takes three the company name, revenue and expenses, and outputs a string containing the net income. We make the use of this function clear in the docstring defined using triple quotes. Here's what the report looks like. Let's convert this function into a tool our agent can call. To do this, we we import the tool decorator and add it before the function definition. Don't worry if you're not familiar with Python decorators, the tool modifies the function so it's in the correct format to be used by a tool. Like with the built in tool we were looking at, we can now examine the various attributes of our tool. These include its name, which is the function name by default, its description, which is the function's docstring, and return direct, which is set to false by default. We can also print the tool's arguments which lay out the argument names and expected data types. Let's put our tool into action. We'll again use a REACT agent combining the chat LLM with a list of tools to use containing our new custom tool. We invoke the agent with an input containing the required a company name, revenue and expenses. The response from the agent starts with our input, then determines that the financial report tool should be called, which returns a tool message containing the output from our function. And finally the output is passed to the LLM which responds to us. Let's zoom in on this final message. Here's the final output from the LLM. Notice anything? Here's the output from the tool based on the function we defined. Notice that there's slight formatting differences between the two. The LLM received the tool output and put its own slight spin on it, which you may need to watch out for. In this chapter we'll discuss Retrieval, Augmented Generation or rag. Pre trained language models don't have access to external data sources. Their understanding comes purely from their training data. This means that if we require our model to have knowledge that goes beyond its training data, which could be company data or knowledge of more recent world events, we need a way of integrating that data. In rag, a user query is embedded and used to retrieve the most relevant documents from the database. Then these documents are added to the model's prompt so that the model has extra context to inform its response. There are three primary steps to rag development in LangChain. The first is loading the documents into LangChain with document loaders. Next is splitting the documents into chunks. Chunks are units of information that we can index and process individually. The last step is encoding and storing the chunks for retrieval, which could utilize a vector database if that meets the needs of the use case. We'll discuss all of these steps throughout the next chapter, but for now we'll start with document loaders. LangChain Document loaders are classes designed to load and configure documents for integrations with AI systems. LangChain provides document loader classes for common file types such as CSV and PDFs. There are also additional loaders provided by third parties for managing unique document formats, including Amazon S3 files, Jupyter, notebooks, audio transcripts, and many more. In this video, we will practice loading data from three common PDFs, CSVs, and HTML. LangChain has excellent documentation on all of its document loaders, and there's a lot of overlap in syntax, so explore at your leisure. There are a few different types of PDF loaders in LangChain, and there is documentation available online for each. In this video we'll use the PyPDFloader. We instantiate the PyPDFloader class, passing in the path to the PDF file we're loading. Finally, we'll use the load method to load the document into memory and assign the resulting object to the data variable. We can then check the output to confirm we have loaded it. Note that this document loader requires installation of the PYPDF package as a dependency when loading CSVs. The syntax is very similar, but instead we use the CSVloader class. We're seeing a pattern forming. Finally, we can load HTML files using the unstructured HTML loader class. We can access the document's contents again with subsetting and and extract the document's metadata with the metadata attribute. Now that we've loaded documents from different sources, let's learn how to parse the information. Document splitting splits the loaded document into smaller parts, which are also called chunks. Chunking is particularly useful for breaking up long documents that they fit within an LLM's context window. Lets examine the introduction from an academic paper, which is saved as a PDF. One naive splitting option would be to separate the document by line. This would be simple to implement, but because sentences are often split over multiple lines, and because those lines are processed separately, key context might be lost. To counteract lost context during chunk splitting, a chunk overlap is often implemented. We've selected two chunks and a chunk overlap shown in green. Having this extra overlap present in both chunks helps retain context. If a model shows signs of losing context and misunderstanding information when answering from external sources, we may need to increase this chunk overlap. There isn't one document splitting strategy that works for all situations. We should experiment with multiple methods and see which one strikes the right balance between retaining context and managing chunk size. We will compare two document splitting Character Text splitter and recursive Character Text Splitter. Optimizing this document splitting is an active area of research, so keep an eye out for new developments. As an example, let's split this quote by Elbert Hubbard, which contains 103 characters, into chunks. We'll compare how the two methods perform on this quote with a chunk size of 24 characters and a small chunk overlap of 3. Let's start with Character text splitter. This method splits based on the separator first, then evaluates chunk size and chunk overlap to check if it's satisfied. We call CharacterTextSplitter, passing the separator to split on along with the chunk size and chunk overlap. Applying the splitter to the quote with the splittext method and printing the output, we can see that we have a problem. Each of these chunks contains more characters than our specified chunk size. Character text splitter splits on the separator in an attempt to make chunks smaller than chunk size, but in this case, splitting on the separator was unable to return chunks below our chunk size. Let's take a look at a more robust splitting method. Recursive Character text Splitter takes a list of separators to split on, and it works through the list from left to right, splitting the document using each separator in turn and seeing if these chunks can be combined while remaining under chunk size. Let's split the quote using the same chunk size and chunk overlap. Notice how the length of each chunk varies the class split by paragraphs first and found the chunk size was too big. Likewise for Sentences, it got to the third separator splitting words using the space separator and found that words could be combined into chunks while remaining under the chunk size character limit. However, some of these chunks are too small to contain meaningful context. But this recursive implementation may work better on larger documents. We can also split other file formats like HTML. Recall that we can load HTML using unstructured HTML loader. Defining the splitter is the same, but for splitting documents we use the split documents method instead of splittext to perform the split. Welcome back. Now that We've covered document loading and splitting. We'll round out the RAG workflow with learning about storing and retrieving this information using vector databases. We've now loaded documents and split them into chunks using an appropriate chunk size and chunk overlap. All that's left is to store them for retrieval. We'll be using a vector database to store our documents and make them available for retrieval. This requires embedding our text documents to create vectors that capture the semantic meaning of the text. Then a user query can be embedded to retrieve the most similar documents from the database and insert them into the model prompt. There are many vector databases available in LangChain. When making the decision on which solution to use, consider whether an open source solution is required, which may be the case, and high customizability is required. Also consider whether the data can be stored on off premise on third party services. Not all cases will permit this. The amount of storage and latency of retrieving results is also a key consideration. Sometimes a lightweight in memory database will be sufficient, but others will require something more powerful. In this course we will use chromadba because it is lightweight and quick to set up. We'll be storing documents containing guidelines for a company's marketing copy. There's two guidelines, one around brand capitalization and another on how to refer to users. Now that we've parsed the data, it's time to embed it. We'll use an embedding model from OpenAI by instantiating the OpenAI embeddings class passing in our OpenAI API key. To create a chroma database from a set of documents, call the from documents method on the chroma class passing the document and embedding function to use. We'd like to persist this database to disk for future use, so provide a path to the persist directory argument. Finally, to integrate the database with other LangChain components, we need to convert it into a retriever with the asretriever method. Here we specify that we want to perform a similarity search and return the top two most similar documents for each user query so the model knows what to do. We'll construct a prompt template which starts with the instruction to review and fix the copy provided, insert the retrieved guidelines and copy to review and an indication that the model should follow with a fixed version to chain together our retriever, prompt template and LLM. We use LSEL in a similar way as before, using pipes to connect the three components. The only difference is that we create a dictionary that assigns the retrieved documents to guidelines and assigns the copy to review to the runnable passthrough function, which acts as a placeholder to insert our input. When we evoke the chain printing the result, we can see the model fixed the two guideline breaches Congratulations on completing the course. Let's wrap up by reviewing what you've learned. We started by talking about the core components of lamechain models, prompts, chains, agents, and document retrievers. We used open source models from Hugging Face as well as proprietary models from OpenAI. We learned how to combine models, prompts and agents using LSIL chains. Finally, we learned how to integrate external data into LLMs, overcoming the limitations of their training data as you continue your LangChain journey into AI application development, the LangChain hub will be a fantastic resource. Along the way, the hub contains a catalog of prompts for a whole range of different tasks. You can search, use and add to this massive catalog as you progress. LangChain templates are also worth investigating. There are sets of code blocks that are ready to use out of the packet, and they cover many of the most common use cases. They may require minor modifications or additions, but they're often the best place to start. The core LangChain package that you've seen in this course is only a piece of the full LangChain ecosystem, which also includes Langsmith, Lang Serve, and langgraph. As you've also seen in this course, Langsmith is used for troubleshooting and evaluating LLM applications and Lang Serve is used for deploying these applications to production, while Lang Graph, as you know, is used for multi agent knowledge graphs. Continue to explore these tools as the ecosystem evolves.
Hello everyone, I am Mary Nova, a machine learning engineer, founder at Break into Data and a content creator. Welcome to this course on RAG with LangChain, Large language models or LLMs have become integrated in many of the systems and applications that we interact with in our day to day lives. One of their key limitations is is that their knowledge base is constrained by what was included in their training data. In this course we'll explore how Retrieval Augmented Generation, or rag allows us to overcome this by integrating external data sources into LLM applications. This works by embedding user queries to retrieve relevant documents and incorporating them into the model's prompt. RAG provides extra context for more informed LLM responses. This method is commonly used to provide more relevant answers to users based on company's external proprietary data. To enable a RAG workflow, we need to set up our data sources for retrieval, which starts with loading the documents to build up the knowledge base, splitting them into chunks to be processed and creating numerical representations from text called embeddings. These embeddings or vectors are stored in a vector database for future retrieval. In this video, we'll start with loading documents with LangChain. LangChain document loaders facilitate the integration of documents into AI systems. These loaders handle various file types, including standard formats like CSV and PDFs, as well as specialized formats supported by third party providers such as Amazon S3 files, Jupyter notebooks, and audio transcripts. In this video, we'll cover how to use document loaders to import CSV, PDF, and HTML files into LangChain. Let's start by loading CSV files using csvloader. Here's a quick example. We instantiate the csvloader class, passing it the path to the CSV file we want to load, assigning the result to CSVloader. To load these documents into memory, we call the load method on the document loader. Each document has page content and metadata attributes to access the respective data. Next, we'll use the PyPDFloader to load PDF files. PDFs are a commonly used document format that can store text and images. Like CSVloader, this class takes a file path to create the document loader and has a method to load the document into memory. We're starting to see a pattern here. Lastly, we'll look at loading HTML files using unstructured HTML loader. HTML files can be tricky due to their complex and highly nested structure, but this LangChain class simplifies the process. Loading the documents into memory and viewing the page content and metadata of the first document. We can see that the HTML tags used to structure the page have been removed, leaving only the plain text. Welcome back. Previously, we learned how to load our documents for retrieval. Next, we need to split documents into chunks that can be quickly retrieved and integrated into the model prompt. This requires embedding these chunks so they can be retrieved based on their semantic similarity and storing them in a vector store. Let's cover these final three, starting with splitting, sometimes called chunking. Ideally, documents are split into chunks that contain sufficient context to be useful to the LLM. However, larger doesn't always mean better. If the chunks are huge, retrieval will be slow and the LLM may struggle to extract the most relevant context from the chunk to respond to the query. The chunksize parameter is used to control this balance. Another parameter, chunkoverlap, is used to capture important information that may be lost around the boundaries between chunks. Let's try our first Splitting method Character textsplitter on this text string first instantiate a splitter using the class we specify the separator to split on. In our case, it will split the text with each new paragraph with chunk size below 110 overlapping characters. To apply the splitter, call the dot splitttext method on the text. Let's view the chunks and their lengths which we obtain using a list comprehension. Keep in mind that character text splitter frequently creates chunks that lack sufficient context to be useful in retrieval. Like the first chunk here, the method was also unable to create chunks that were all below the chunk size by splitting by paragraph. We can improve on this with recursive character text splitter. It takes a list of separators and recursively splits using each one attempting to create chunks below chunk size. For example, if the first separator creates chunks that exceed 100 characters, it will be split further using the next separator. Recursive character text splitter often preserves more context, which will result in more coherent responses. From our RAG application, extending splitting strings to splitting documents requires just one change. Swapping the dot split text method to dot split documents. Each document has page content and metadata attributes for extracting the respective information. Calculating the number of characters in each chunk, we can see that we were able to stay under the chunk size. Now we've split documents into chunks. Let's embed and store them for retrieval. Remember that embeddings are numerical representations of text. Embedding models aim to capture the meaning of the text, and these numbers map the text position in a high dimensional or vector space. Vector stores are databases specifically designed to store and retrieve this high dimensional vector data. When documents are embedded and stored, similar documents are located closer together in the vector space. When the RAG application receives a user input, it will be embedded and used to query the database returning the most similar documents. We'll use an embedding model from OpenAI and store the vectors in a chroma vector database. Let's start by initializing the model. Then to embed and store the chunks in one operation, we call the dot from documents method on the chroma class passing the chunks and model. Note that if we were embedding string chunks, we'd use the dot from text method instead. Great work so far. Now we have our external data stored and ready for retrieval. It's time to create the retrieval chain to integrate it with an LLM LangChain. Expression Language, or lcel, is a declarative syntax for defining chains suited from prototypes to production. It is particularly relevant to RAC systems because it creates modular reusable pipelines that can combine retrieval and generation components together. The retrieval chain will take a question input, insert it into the chain using rannable passthrough and assign it to question. Runnable passthrough allows inputs to be inserted into chain unchanged. We retrieve the relevant documents from the vector store and assign to context. Integrate both of these into a prompt template, pass the prompt to the model to generate an output, and parse the output into our favored format, such as a string. Before building the chain, we need to create three a retriever which is derived from our vector store, a prompt template for combining the user question and retrieved context, and a model to generate the response. Previously, we created a vector store from embedding document chunks and using the dot from documents method. To convert this into a retriever suitable for our chain, we call the sretriever method on vectorstore. The arguments dictate what sort of search should be conducted during a retrieval, a similarity search in this case, and how many chunks to retrieve when queried k, which is 2. To construct a prompt template for the model, we'll use chat prompt template Inside, we instruct the model to use the context provided to answer the question. We use curly brackets to indicate placeholders for the two input variables, context and question. We'll define an OpenAI chat model to generate the responses. Now let's explore how to build a chain using LCAL and Runnable passthrough. We start by opening parentheses so we can define our chain over multiple lines. Then create a dictionary that takes the input from runnable passthrough, assigns it to question, and uses it to query and retrieve chunks from the retriever which are assigned to context. This might look complex, but Runnable passthrough is essentially a placeholder in our chain that allows us to pass data through without modifying it. The retrieved context and user question are then passed into the prompt using the LCL pipe and then into the LLM. Finally, a string output parser is used to parse the model output as a string. Given that our vector store contains chunks from RAG academic paper, let's invoke the chain to request the paper's findings. We can see that context from the paper was successfully retrieved and used by the model to respond to the question. At this stage, we might wonder how we can evaluate and improve our model's performance based on its response. Stick around to find out. In this chapter, we'll dive deeper into building more sophisticated RAG architectures with LangChain Starting with methods for loading and splitting code files. We've previously looked at a few common file formats, including PDFs, CSVs, and HTML files. Let's extend this a little further to Python and Markdown files, which are both common in software engineering and data science projects. Let's start with Markdown files. Markdown is a lightweight markup language for creating formatted documents, and it's often the tool of choice for writing code documentation. When rendered, Markdown can show links, images, code blocks, and much more. The unstructured MarkDownloader class can be used to load Markdown files the same way as other file formats we've looked at before. By instantiating the class on the file path and using the load method to load it into memory, we could integrate these documents into a RAC application to read code documentation and make recommendations. Now, onto Python files. Imagine we have a code base and would like to have a way to talk with it and ask it questions. We could achieve this by integrating Python files into a rag application. The PythonLoader class and the load method can be used to load these files into memory. The resulting documents have page content and metadata attributes for accessing the document's details. Remember, parsing Python files can be tricky because it has its own syntax with imports, classes, functions, and much more that needs to be preserved during chunking. Let's see how we can address that. Let's start splitting this Python file with our current best tool for document recursive character text splitter. We set a chunk size and chunk overlap to control the number of characters in each chunk. Splitting the documents with dot split documents method, we can print the content of each chunk. Here's the result. Notice that the split between chunks 2 and 3 splits the anthropic class, and because chunks are processed separately, key context has been lost. Our current strategy is naive because it doesn't even consider structures like classes and functions. Let's change this. Let's split our loaded Python file using a recursive character text splitter again, but this time let's use the dot from language method. This method has a language argument which refers to coding languages that we can set to language Python and the rest of the arguments stay the same. This will modify the default separators list from the hierarchy of paragraphs, sentences and words to try splitting on classes and function definitions before moving on to the standard separators. Now let's call SplitDocuments to perform the split and view the first three chunks again. As we can see, the splitter was able to split on class definitions so all of that context is kept together. Note that this approach isn't final and depending on the size of the classes and functions relative to the chunk size, we may get a differing result. Now let's level up our document splitting. Our current approach for splitting documents with character text splitters has a few limitations. These splits are effectively naive because they are executed without considering the context of the surrounding text. This means related information will potentially be stored and processed separately, which will lower the quality of our rag. Application splits are also made using characters rather than tokens. Recall that the language models we are using break text into tokens or smaller units of text for processing. If we split documents using characters rather than tokens, we risk retrieving chunks and creating a retrieval prompt that exceeds the maximum amount of text the model can process at once, also called the model context window. We'll introduce methods to make our splitter more aware of the document's context and enable splitting with tokens. When we use a character text splitter with a chunk size and chunk overlap, we get chunks containing groups of characters that satisfy the chunking parameters. When we split on tokens, the chunk size and chunk overlap refer to the number of tokens in the chunk rather than characters. So A chunk size of 5 means so we we can have a maximum of 5 tokens in the chunk. Here there are 5 tokens in the first chunk, each colored 4 in the second chunk, and a 2 chunk overlap. The token text splitter can be used to perform token splitting. It requires the name of the encoding to use, which is the encoding used by the large language model and can be retrieved using the TikToken encodingformodel method and extracting the name with the name attribute. Remember, the chunk size and chunk overlap now represent tokens rather than characters. We'll use the splittext method to split the example string and view the chunks. This looks good, but we're able to keep the chunk size of 10 tokens. Let's check. We can loop through our chunks, encoding them into tokens with encoding encode and and calculating the number of tokens with len. The first chunk contains 10 tokens, the second chunk has 6, and there is an overlap of 2 tokens. Fleece was so everything worked as expected. Let's now learn about a splitting strategy to split in a more context aware semantic splitting. Take this block of text for example. A character or token text splitter will split naively which result in loss context. A semantic splitter will detect shifts in semantic meaning and perform the splits in those locations. In this example when the discussion shifts from RAG to dogs to perform semantic splitting, we'll need an embedding model to generate text embeddings. To determine the shift in topic, we'll use a model from OpenAI. We instantiate this semantic splitting class. Passing the embedding model, we pass two additional parameters breakpoint threshold type, which sets the metric at which embeddings are compared, and breakpoint threshold amount, which sets the metrics threshold at which to perform the split. Like other splitters, we use the dot split documents method to apply the splitter in this case to an academic paper. The semantic splitter reached the threshold of 0.8 and performed the splits for the first chunk splitting after the first two sentences of the abstract. Now that we've upgraded our document loading and splitting, let's see how the document retrieval process can be optimized. So far our document retrieval has consisted of a vector database containing embedded documents. The input to the RAG application is then used to query the vectors using a distance metric to determine which vectors are closest and therefore most similar and relevant. This type of retrieval is known as dense retrieval. Dense retrieval methods encode the entire chunk as a single vector that is said to be dense, I.e. most of its component values are non zero. Dense retrieval excels at capturing semantic meaning, but the embeddings can be computationally intensive to create and query and may struggle with capturing rare words or highly specific technical terms. There is also sparse retrieval, which is a method of finding information by matching specific keywords or terms in a query with those in documents. The resulting vectors contain many zeros with only a few non zero terms, which is why they are said to be sparse. Sparse retrieval allows for precise retrieval matching on exact words. The resulting vectors are also more explainable due to the alignment with specific terms, and rare words are better represented in the embeddings. The trade off here is that sparse retrieval methods are less generalizable as they aren't extracting the semantic meaning from the text. TF IDF and BM25 are the two popular methods for encoding text as sparse vectors. TF IDF or term frequency Inverse document frequency creates a sparse vector that measures a term's frequency in a document and rarity in other documents. This helps in identifying words that best represent the document's unique content. BM25 or best matching 25 is an improvement on TF IDF that prevents high frequency words from being overemphasized in the encoding. Let's try out BM25 for rag retrieval. The BM25 retriever class can be used to create a retriever from documents or text, just like the retrievers we have already used. Let's start with a small example of three statements about Python. We can use the dot from text method to create the retriever from these strings. The k value sets the number of items returned by the retriever when invoked. Invoking the retriever with an input and returning the page content of the most relevant result according to BM25 reveals that the statement about when Python was created was correctly returned. Looking at all three statements again, we we can see that BM25 returned the statement with similar terms to the input that were also unique to the other statements. Now that we've tested out the BM25 retriever, let's integrate it into Rag. We'll create a RAG system to integrate a datacamp blog post on RAG with an LLM. The first step is the same as before. We're using the dot from documents method as we're dealing with document chunks and not strings this time. Then we use the same LCL syntax as a standard dance retrieval rack to integrate the retriever with a prompt template and LLM. Remember that runnable passthrough allows us to insert the input unchanged into the chain. Finally, we can invoke the chain on the input and return the results. Welcome back. Because our RAG architecture is made up of several processes, there are a few places where performance can be measured. We can evaluate the retrieval process to check if the retrieved documents are relevant to the query, the generation process to see if the LLM hallucinated or misinterpreted the prompt, or the final output to measure the performance of the whole system. Let's start with the final output. We can use LLMs to measure the correctness of the final output by comparing it to a reference answer. We'll assign the query model's answer and reference answer to compare with the following variables. To perform string evaluation, we need to define a prompt template and large language model to use for evaluation. The prompt template instructs the model to compare the strings and evaluate the model output for correctness, returning correct or incorrect. The model temperature is also set to zero to minimize variability. We initialize LangChain String Evaluator from langsmith, which is LangChain's platform for evaluating LLM applications. This evaluator first takes qa, which sets the evaluator to assess correctness, and also the LLM and prompt template to use. We Then call the EvaluateStrings method on the model prediction, reference answer and input query to perform the evaluation. The score of 0 indicates that predicted response was incorrect when compared to the reference answer, and we can see here that the model response was deemed incorrect, which makes sense on reviewing it again. Now let's explore further with ragas framework Ragas was designed to evaluate both the retrieval and generation components of a RAG application. We'll cover one metric for each faithfulness and context precision. Faithfulness assesses whether the generated output represents the retrieved documents well. It is calculated using LLMs to assess the ratio of faithful claims that can be derived from the context to the total number of claims. Because faithfulness is a proportion, it is normalized to between 0 and 1, where a higher score indicates greater faithfulness. RAGAS integrates nicely with LangChain, and the first step involves defining the models for the evaluator to use, one for generation and another for embeddings. Next, we define an evaluation chain, passing it the faithfulness metric from ragas in the two models we defined. To evaluate a model's response, we instantiate the chain, passing it a dictionary with question, answer, and context keys. Question is the query sent to the RAG application. Answer is the response and context are the document chunks available to the model. A perfect faithfulness score of 1 indicates that the model's response could be fully inferred from the context provided. Context precision measures how relevant the retrieved documents are to the query. The context precision score closer to one means means the retrieved context is highly relevant. The only change we need to make to the faithfulness evaluation chain is to import and use the content precision metric instead. The context precision chain similarly takes a dictionary with question, context and ground truth keys representing the input query, the retrieved documents, and the ground truth document that should have been retrieved. Printing the results, we can see that we achieved a high context precision, indicating the retrieval process is returning highly relevant documents. In this chapter, we'll discuss how to use a graph database in our RAG architecture to replace vector storage and retrieval. The RAG architectures we've explored involve embedding a user input and querying a vector store to return relevant documents based on their semantic similarity. Although powerful, this approach does have some limitations. Firstly, document embedding captures semantic meaning but struggles to capture themes and relationships between entities in the document corpus. Moreover, as the volume of the database grows, the retrieval process can become less efficient as the computational load increases with the search space. Lastly, vector RAC systems don't easily accommodate structured or diverse data, which are harder to embed. We can address all of those challenges with graphs. Graphs are great at representing and storing diverse and interconnected information in a structured manner. Entities like people, places and sports teams are represented by nodes, and relationships between entities are represented by labeled edges. Notice that edges are directional, so relationships can apply from one entity to another, but not necessarily the other way around. We'll look at this more closely in a later video. Neo4j is a powerful graph database option designed to store and efficiently query complex relationships. Throughout this chapter, we'll integrate near 4J with LangChain to build a graph rag architecture. Let's return to our example and build a near 4J graph using this information. Our entities are represented as nodes where the color indicates the entity type, such as the person. The relationships are represented by edges with types like located and interested. Each node also has a type and unique identifier. Nodes can contain any number of properties represented as key value pairs. So how do we go from the unstructured text data we've seen to a nice structured graph? There's a few ways to do this, but we'll be using LLMs let's load the Wikipedia results from searching large language model using the wikipedialoader class and split the first few documents into chunks. Each document has page content and metadata as seen here. We begin by defining the LLM to use for the transformation and use it to create an LLM graph transformer. Note that we use temperature equals zero to produce more deterministic graphs for greater reliability. The LLM creates structured graph documents by parsing and categorizing entities and their relationships, which it infers from the documents. The transformation is performed using the converttographdocuments method on the documents in the output. We can see how the model inferred many entities from the text and created nodes with IDs and types to match. Relationships between the entities were also inferred and mapped using edges going from a source node to a target node. Now that we've created our graph documents, we'll need to store them for querying. We'll be using Neo4J to store and query our graph documents. We won't be going through the Neo 4J setup in this course, but feel free to check out the link shown for information on how to do this. Neo4J has both cloud based and local versions to suit your use cases. For our purposes, we'll assume that database is already locally available, which will also be the case in the exercises. We instantiate a graph with the Neo4J graph class specifying the URL to the Neo4J database server and the credentials needed to access it in a production Setting these credentials should be saved as environment variables rather than being committed to a code base for better security. Carrying on from the graph documents we created from Wikipedia results on large language models, we can add these graph documents to our database using the AddGraphDocuments method. The include source parameter links nodes to their source documents, which are also represented as nodes. By including a Mentions relationship in the graph, you can enabling better traceability and context preservation. The base entitylabel parameter assigns an additional entity label to each node, improving query performance. Here's what our graph looks like. There's a lot here, so let's zoom in to look at subsections of this graph. We can see lots of familiar nodes and relationships representing the different entities in the search results. The pink nodes are the source documents we specified when adding the graph documents. Each one has a Mentions relationship from the source to the entity mentioned. We can also view the database schema with the getschema attribute. Here we can see the different node types and relationships, including their direction. Now let's talk about querying our database. Neo4j introduced Cypher query language in 2011 as a declarative query language for intuitively navigating and manipulating graph data using a SQL like syntax. Consider the example of a social network using graphs to map social connections. We could use Cypher to find who James is friends with. Here's what the cipher code looks like. Let's break it down. The code looks for a match between a node with name James and another node indicated with the friend variable joined with a friend relationship. Notice that the relationship sits in the center of an arrow that indicates the direction of the relationship from one node to another person is the node label. We're declaring the source node using the name property and and friend is a variable we'll use to search for the missing name. Notice the code finishes with return friend, so the mystery friend is revealed. This query reveals the mystery friend's name. Let's query our database of wikipedia results about LLMs. We'll query the database to find out who developed the GPT4 model. Our query looks for a match between a model and an organization joined by the developed participants by relationship. There we have it. Now that we've created our graph documents, stored them, and queried them using Cypher, let's bring everything together to create a graph rag chain with LangChain. Let's start to put together a graph rag architecture using what we know so far. We have our Neo4J graph database which we've populated with graph documents by using LLMs to Infer the nodes and relationships. We also know that our application will require a user input and that cypher is required to query the Neo4J database and return the relevant documents. So what's missing? We need a way to translate our user input into a cipher query analogous to how we embed user queries in Vector RAC applications and then return the graph result in natural language. How do we do this? You guessed it, LLMs. So how does the LLM know what cypher query to generate? We give it access to the graph schema containing the node properties and relationships. So for a given user input, we can infer from this information which properties and relationships should be included. LangChain provides a graph cipher qachain class that performs this translation for us. The user submits a natural language input, it gets translated into a cipher query, and the generated cipher is used to query the Neo4j database. Finally, the results obtained from the Neo4j database query are converted back into the natural language for the user. This graph cipher QA chain is really two sequential chains under the hood. A generate cipher chain to generate the cipher query and a summarize results chain to return the natural language response. Let's continue our previous example where we created a graph containing Wikipedia documents about large language models. We can refresh the schema to see the most recent nodes and relationships, which is particularly useful when the graph is being auto populated. Now to the fun part. We import graph cipher QA chain called the fromllm method and pass it in LLM and our graph database containing the graph documents. We'll also specify verbose true so we can see the cipher query and result returned from the database in the output. As before, we use temperature zero to generate more deterministic responses. We'll invoke the chain on a query to find the most accurate model and save the results. Let's view the output generated by the verbose true so we can see that the chain successfully created a cipher query that finds all model nodes in the database, sorts them by their accuracy property in the descending order, and returns the node with the highest accuracy. The context shows the query result which is also reflected in the chain result. Note that here we are using the same LLM for generating the cypher query in the final natural language result. We're also not changing the default prompt templates for either of these steps that are used under the hood. It is possible to use different models for the two steps and specify our own prompt templates using the arguments shown, which we'll investigate in the next video. Let's now look at a few techniques to improve our graph rag architecture. Since large language models are non deterministic by nature, the main limitation of our approach is the reliability of the user query decipherquery translation. We'll take a look at a few techniques to improve this process, including filtering the graph, validating the cypher query, and a few shop prompting. One way to improve the reliability of our cypher queries is to filter down the search space within the dot from LLM method we can use the exclude types argument to specify nodes with given types to exclude from the database. Here we'll remove the nodes with the concept property. Removing unneeded node properties from the schema so the model can focus on a specific subset of often improves the quality of generated cipher statements. One thing LLMs have particular difficulty with is inferring the direction of relationships when generating cipher statements. Because the graph schema is predefined when instantiating the chain after the cipher is generated, we can validate the query against the graph schema. The validate cipher argument inside the graph cipher QA chain is can perform this check and if necessary, correct the relationship directions in these generated statements. The cipher validation detects the nodes and their properties and relationships, determines the direction of a given relationship in the cipher query, verifies if the node labels and relationship types match the provided schema, and finally updates the direction of relationships based on the schema. Another great technique to improve the cipher query generation is to provide the model with a few examples of user questions and their corresponding cipher query. Few shot prompting improves model performance in more complex queries, and it allows us to include more relevant examples for our use case. To set these examples up, create a list of dictionaries with questions, question, and query key value pairs. We can turn these examples into a prompt template using LangChain's prompt template classes. We create an example prompt to dictate the structure of each example and pass this example prompt to the example prompt argument of few shot prompt template along with the examples list of dictionaries, we also include a prefix message to guide the model on the task at hand and a suffix message to clarify and desired output structure. Here's how the complete prompt looks before an input is inserted and gets sent to the LLM. This prompt template can be passed to the cipher prompt argument of the graph chain to override the default template used by the class in the generate cipher chain.
Hello, I'm Delini Sumanapala, an AI engineer with a background in cognitive neuroscience, natural language applications, and founder of GenVerve Ltd. Welcome to the course. We'll be working with Agents and Tools Agents prominent across many industries are intelligent, autonomous systems that make decisions and take actions much like humans. Tools are functions that agents use to perform tasks such as data query, research reports, or data analysis. Before starting, you should be familiar with large language models such as ChatGPT, prompts tools, and using APIs for data retrieval and communication. LangChain is a framework for building powerful AI agents. Using these components, our agents will use tools to solve math, use APIs to search Wikipedia, and determine when to switch between LLMs or tools. Based on a given task, agents can reason and act. For instance, an agent could reason that it needs a currency tool to search for a specific currency and return the response. This agentic framework is called React, short for reasoning and action. Combining tools with agents can also improve accuracy in domains like coding and math. Here we see an earlier version of ChatGPT having trouble with the order of operations. LangChain uses specific tools to break problems into smaller steps, reducing errors. For example, we can use a tool to handle the order of operations in math. Langgraph can enhance tool use even further by structuring tasks in workflows called graphs. In these graphs, tasks called nodes are connected by rules called edges. For example, a database query node can link to a document retrieval node with an edge pointing to which document is retrieved. Before building such complex workflows, let's create a basic agent that does math. The tool module imported from LangChain's core tools lets us use custom functions. ChatOpenAI imported from LangChain. OpenAI enables communication with OpenAI's language models. The Create React agent module imported from Langgraph's pre built functions helps us create a react agent that can reason and use tools. Finally, the Math module lets us perform standard math. Then, once our model has been defined, we'll create our react agent by passing the model and a predefined tool to the Create React Agent function. Since our tool is a basic calculator, we'll define a math query as a string. We'll pass this query labeled human to the agent using the invoke method, storing the output as a response. We'll then use the content attribute to print the last item in messages of response to get the agent's answer. Perfect. Great work. Now that we know how to create an agent, let's imagine a use case for a real estate agency. The agency staff may be interested in calculating the square footage of of rectangular one bed apartments. We can build a math tool that does this using the lengths of the sides of the apartment supplied by the staff using casual natural language. By default, LangChain accepts such natural language queries as strings. Internally, it uses the LLM to extract the necessary input from the query. Here, LangChain might extract the two numbers 5 and 7 as string inputs representing rectangle lengths. Before performing calculations, let's create a function that can handle this kind of input. We'll start by using a decorator called that LangChain uses to recognize custom functions as tools. After the decorator, we'll create a function called Rectangle area that takes in the string and as an input. We then include a docstring to describe the function's purpose, specifying that it calculates the area of a rectangle given the lengths of two sides A and B inside the function. We split the input string extracted from the query into the two values representing sides A and b. To multiply the sides, we strip any white space around the string inputs using Python's strip method and then convert each string to a float. We then multiply sides A and B together to calculate and return the area of the rectangle. Now that we have our tool, let's make sure LangChain can access it by passing it within a list called tools. Although we're only using one tool here, it's possible to list more depending on our workflow. Then we'll create a variable called query that accepts a question from the user in the form of natural language, perhaps passed from a real estate chatbot used by staff. We'll then create our react agent called App Passing in the model and the tool we just built to test that the agent works will invoke the agent we just created passing in the query we defined, and then print the agent's response by identifying the last item in messages using the content attribute. Looks like it works. So far we've reviewed math tools in Agentic workflows. LangChain also has an extensive library of pre built tools for solving many other problems such as database querying, web scraping, and image generation, which can be incorporated by referencing LangChain's API guide. We can also reference LangChain's guide for using tool decorators to build other custom tools. Nicely done. So far we've just been printing the agent's outputs. It's also useful to know that our agent is responding correctly by printing both the user's query as well as the response. First we'll define our tools and query. Then we'll set up our agent using a preloaded model and tools. Next we'll invoke the agent and store the response before printing both the query labeled User input and the agent's response as Agent output. When we examine the output, it looks like our agent is working. To verify that our agent is working, we can ask follow up questions while answering. LangChain will update the whole conversation before printing the answer again as a separate output. When looking at the output, we'll see a new query for a new rectangle. Then we'll see the full conversation listing both the old and new queries with their respective answers. The human message refers to our own queries, while the AI message refers to the agent's answers. If the conversation is up to date, printing the last output will repeat the most recent query and answer, ensuring that the agent works. To set up our conversation history, we'll import the Human Message and AI message modules from LangChain's Core Messages module. Then we'll set up a variable called messagehistory that will store all of our messages. Next, we'll define a new query that will ask a new question without providing any additional contextual information. Here we want to know the area of a new rectangle with different dimensions. Next we'll invoke the app object again, this time passing both the message history and new query to the agent within a dictionary. We'll then filter out only the relevant messages from the agent's response. Here we'll use List Comprehension to select both Human Message and AI message instances that contain actual quantities. Content when applied to message content, the strip method removes any trailing white spaces. Finally, we'll format and print the conversation extracted from message content with each message labeled using its proper class name. The user input is our new query, while the agent output will print the full conversation and repeat the agent's most recent output, which is useful for debugging. Let's see if we can produce the elements we need. We have our new query labeled User input. Then we have our agent output listing the full conversation with labeled human and AI messages. When we ask the agent to list the last message, we have our most recent query and answer. Everything is in good shape. Great job. Now that we're familiar with using LangChain's pre built agents, we'll learn how to create custom chatbots using Langgraph. We'll first learn about graph and agent states that manage chatbot workflows before building chatbots using nodes and edges. In Langgraph, the graph state organizes the agent's order of tasks such as tool usage and LLM calls into a workflow. The graph uses an agent state to track the agent's progress as text to help determine when a task is complete. Let's start by building an education chatbot that uses an LLM to answer questions. We only need three nodes and two edges to get started. In Langgraph, nodes represent functions like generating a response or calling a tool, while edges define the connections between these nodes. Here, an edge connects the start of the workflow to the Chatbot node, and another edge connects the Chatbot node to the end of the workflow. The start and end nodes to define the start and end of the workflow will be directly imported from Langgraph. Let's build the full graph. We'll need the following modules from Typing and typing extensions. The annotated and typedict modules will help organize the chatbot's text data. Next, we'll require the state graph, Start, end and Add Messages modules from langgraph to set up our Chatbots workflow and add text messages. Chat OpenAI from LangChain. OpenAI will also be imported to help us set up our LLM. To get started, we'll set OpenAI as our LLM. Then we'll create an agent state called state that accepts type dict to structure messages. We'll define messages as a list that will store all text interactions with the chatbot. The annotated type combined with Add messages ensures that new messages are added to this list with metadata. Next, we'll initialize a state graph called Graph Builder and pass in our state structure. The graph builder will organize the nodes and edges in the chatbot's workflow. Let's now define the main function node called chatbot that accepts the state class using the LLM.invoke method. It'll use the message's content in the state dictionary to generate a response based on the conversation so far. This Chatbot node can then be added to the graph builder using the Add node method coupled with the label Chatbot. Next, we'll connect the start node to the Chatbot node to begin the conversation, and then connect the Chatbot node to the end node to finish it. Now that the graph builder is ready, we can compile the graph as an application using the Compile method. Now that you've built your Chatbot's graph, it's time to test it out in Langgraph. Streaming events from a graph allows us to process the steps in an agent's workflow in real time as we stream. Each event represents a single step in the graph, like generating a response or calling a tool. This lets us track the chatbot agent's progress by displaying responses as soon as they're ready. Here we've printed four events which include the user's query. Let's first test the chatbot with just the LLM. We'll start by defining a function called streamgraphupdates to run the chatbot based on queries supplied as a string called userinput inside the function, we'll apply the stream method to the graph, passing in the user input labeled user to a dictionary key called messages. This lets the graph start processing the user's question. Next, we'll loop through each event generated by the graph, retrieving the last chatbot response by accessing event values and printing the messages in the value dictionary. This displays the chatbot response as soon as it's ready. Finally, we'll define a test query with the string whoismary Shelley, passing it to stream graph updates to return a response based just on the LLM. Since our chatbot currently doesn't use any tools, it'll use the LLM's knowledge base to return an answer about the celebrated science fiction author of Frankenstein, Mary Shelley, in the AI message field labeled Content. When viewing the rest of the response, we see the LLM alone has detailed information on the topic of Mary Shelley. We can also view the response metadata attached to the message, including the name of the model used to generate the Response. When using LLMs, always check original sources, as even powerful LLMs can be prone to hallucinations. Here, NASA engineer Judith Love Cohen's famous son is actually Jack Black, not Adam Cohen. To limit errors, we can ask Langgraph to show us how the nodes and edges are connected by displaying the graph. We'll first import the modules image and display from IPython display. Then we'll create a Try and Accept block where the getgraph method is applied to graph to retrieve its structure. The drawmermadepng method is then used to render this structure as a visual diagram. Finally, the entire statement is wrapped in image and passed to display to show the diagram. We'll then define an exception that returns an error message if other dependencies for Python visualization are required, depending on your coding environment. Now that you're familiar with basic chatbots, let's try incorporating an external API tool into our Chatbot. Tools with API capabilities help augment Chatbot agents by enabling access to external resources such as news sites, databases, social media, and many others using Langgraph, let's expand our education Chatbots knowledge by including a Wikipedia API. We'll start with two modules. Wikipedia API Wrapper allows us to interact with the Wikipedia API while Wikipedia Query Run makes the API a tool for running queries. Next, we'll initialize Wikipedia API Wrapper, setting top kresults to 1 to keep responses relevant. Then we'll create Wikipedia Tool with Wikipedia Query Run, passing in the API wrapper to connect directly to Wikipedia and retrieve detailed information when needed. Finally, we'll store Wikipedia tool in a list called Tools, which can hold multiple tools. If required, we can bind our Tools list to the language model using Bind Tools and store it in a variable called llmwithtools. Next, we'll update our original Chatbot function to use llmwithtools instead of LLM, enabling responses from the Wikipedia tool when needed rather than relying on the language model alone. This modified function passes the full conversation to stored in messages to LLM with Tools, allowing the language model to decide when to pull information from Wikipedia to enhance its responses. For more details on how to add different tools with external APIs, be sure to reference LangChain's API documentation. Now that we have our Wikipedia tool, the next module is imported for us, called Tool Node, and Tools Condition will help us add the tool to our Chatbots graph. As with the basic Chatbot, we'll start by adding our Chatbot node, labeled Chatbot to the graph builder using the Add Node method. Then we'll define a tool node by passing in the Wikipedia tool to the Tools argument in Langgraph's Tool Node class. Before adding this node, label Tools to the graph builder. Next, before adding an end node explicitly, we'll use the addconditional Edges method with Tools condition to let the Chatbot decide if a tool is needed. If it is, the Chatbot will call a tool. If not, the Chatbot will end without a response. For LLM or tool calls that generate a response, we'll connect Tools back to the Chatbot using the Add Edge method, then add a Start node which connects to the Chatbot before finally connecting the Chatbot to the added end node. Brilliant work. Next, we'll test our tool use before adding memory for more complexity, we'll see if our Chatbot can respond with the Wikipedia tool. We'll use our graphdisplay function to check the graph without the Try Accept block, since we've already tested this feature. Then we'll set up our streaming function, now labeled streamtool Responses, to see if the tool was correctly called. Inside this function, we'll access the messages stored in the Chatbot's events, which should refer to specific tools. Finally, we'll pass a test query House of lords. The diagram shows the Tools node added with all nodes and edges correctly implemented, reflecting all of the possible conversation outcomes. The abbreviated output shows the test query passed to the chatbot along with the Wikipedia tool call in the metadata field called name. The next response shows a summary generated from the House of Lords Wikipedia page. The chatbot's final answer modifies the content from the Wikipedia summary, using the LLM to enhance the clarity of its response. Additional details appear in the Response metadata field. Rather than independently responding to different queries, let's add memory to our chatbot so it can maintain a conversation. First, we'll import Memory saver from Langgraph checkpoint memory to handle memory storage within our graph. Then we'll create a memory saver instance called Memory, which will act as our checkpoint for storing messages. Next, we'll compile the graph with GraphBuilder compiler, passing memory as the checkpoint so the chatbot retains conversation context. To set up a chatbot that remembers context, we'll define a streaming function called Stream memory responses for a single conversation. First we'll create a config dictionary with a unique thread ID single session memory which lets us maintain message history. Next, we'll use a for loop to stream events from the graph using stream passing in the user's input message along with the config dictionary. Finally, we'll extract and print the agent's last response by checking for messages in each event's values, allowing the chatbot to answer follow up questions with context. Lets inspect the output. First, the chatbot processes the query and calls the Wikipedia tool to retrieve information on the Colosseum. Then it provides a summary describing the Colosseum as Rome's largest ancient amphitheater. Next, the chatbot answers explaining it was built by Emperor Vespasian for gladiatorial events. Since we passed config parameters for a single session of memory, the chatbot answers the follow up question with the same tool, noting completion under Titus and modifications under Domitian. Great work. Now that we know how to integrate external tools with chatbots, let's learn how to build a multiple tool chatbot that can automate selecting a tool per quote query. Let's imagine our school wants to enhance their chatbot. Further, for history lessons, they'd like to incorporate a tool that looks up historical events. For English lessons, they'd like to feature a palindrome tool that checks whether a specific string is the same when reversed, for example the word level or the phrase top spot. We can use a different approach for each tool. For historical events, we'll create a tool that directly invokes the LLM to look up natural language dates, such as 5th of November. To build a palindrome checker, we'll use standard Python to compare a reverse string to its original let's code the Historical Events tool. First, we'll label our tool with the LangChain tool Decorator, helping the LLM to identify custom tools. Next, we'll call this tool datechecker, setting it to expect dates as strings. We'll then write a docstring that provides the LLM with a general instruction to return important historical events by date. Next will set up a Try Accept block for different outputs within the Try Accept block, we'll use the Invoke method to invoke the LLM with the given date, passing another instruction that events must relate to the provided date. After saving the response in a variable called answer, we'll return its content. If the LLM fails to respond, we'll define an exception that returns an error message. Next, we'll create our palindrome tool. Starting with a tool decorator, we'll name the tool Check Palindrome, then set the expected input to string. Then we'll create a docstring with an instruction to check for a palindrome. We'll clean our text using isalnum to remove any non alphanumeric characters and use lower to turn the remaining characters into lowercase, saving the result as a variable called cleaned. Next, we set up an if else statement that checks if the reversed version of cleaned is identical to the original, returning the appropriate response. Now we'll add these tools to a new chatbot. We'll import the tool node submodule from langgraph prebuilt defining our tools list with the original Wikipedia tool date checker and check palindrome tools. This list could feature any tools you'd like your chatbot to have. We'll then create a tool node passing tools to the tool node class. Finally, we'll bind the tools to our LLM creating model with tools using the Bind Tools method. You're on a roll now that our palindrome, historical events, and Wikipedia tools are ready to go, let's incorporate them into a new Chatbot workflow. Lets start building the first of two workflow functions, which is a stopping function that decides whether to call tools or end the conversation if there is no tool call in the last message. The next function is a dynamic tool caller that returns a tool response if a tool call is present or invokes the LLM with the Chatbot node if there are no tool calls. Once these functions are set up, we'll define the full graph. To build the workflow, we'll import the message's state, start and end modules from langgraph Graph. Then we'll define the stopcondition function called shouldcontinue that accepts messages state to define the graph state. We'll then extract the last message from messages in state and save it as last message. Then we'll check if this message has tool calls such as Wikipedia, returning tools if the condition is true. If not, the conversation is ended using end. Next, we'll create our dynamic tool Caller Named Call model, which also accepts messages state as the graph state, saving the last message as a variable called lastmessage. Then we'll use isinstance to check if this message is an AI message from the chatbot and whether it features tool calls. If so, we'll return the response from tool calls passed to the content field of the AI message. If there are no tool calls, we will invoke the model using all of the messages in the state passed as an argument to the invoke method before it's applied to modelwithtools. Since we've now got our functions, let's create the full graph. Our graph state called workflow will be created for us using the messagestate passed to state graph. Then we'll add our nodes to the workflow, mapping the Chatbot to the callmodel function and tools to our tool node. We'll then add a Start node that initializes the Chatbot node before adding conditional edges from the Chatbot to the End or Tools nodes using our shouldcontinue function. Next, we'll add another edge from the Tools node back to the Chatbot node, completing the conversation loop. We'll then create a memory saver object called Memory, which we'll use to enhance our Chatbot with memory. Next, we'll apply the compile method to our workflow, passing in the memory to the check pointer to create the Chatbot application before displaying the langgraph diagram. Later on, we'll test this workflow with different queries. Super now that our workflow is ready, we can test it. We'll first see whether the Chatbot can answer a query by picking the correct tool. Then we'll determine if the chatbot can interleave the user's queries with answers to follow up questions. To get started, we'll import the AI message and Human message modules from the LangChain core with a config variable set to one session we'll create a function called multi tooloutput to handle queries for different tools. First, we'll define an inputs dictionary including the user's query as a human message passed within the content field. Next, we'll stream messages and metadata using App stream, which accepts the inputs and config with stream mode set to messages to enable real time output for each message. If message content is not a human message, we'll print its contents to access just the chatbot responses. We'll set the end parameter to an empty string to avoid excess line breaks, with flush set to true to ensure live outputs. A line space will separate the answers for different queries. Let's test this function with two queries using different tools. The first query should trigger the Palindrome tool since the question asks is stella one no wallets a palindrome? The second query should trigger the Historical Events tool, asking what happened on April 12, 1955? For each query, the chatbot will return a direct tool response before the LLM refines the output. Here, the phrase in the first query is labeled a palindrome with additional comments from the LLM. The next query, referencing a date, returns information about the polio vaccine breakthrough, which we've abbreviated. Once again, the tool response is followed with an LLM refinement. We can modify this function to handle follow up questions as well as multiple tools. We'll call the function useragentmultitern, which accepts multiple queries for each user and chatbot interaction. We'll print the user's query first. Then we'll stream message content and metadata using the App Stream method, which accepts the query and config parameters. With the stream mode set to messages for each message, we'll filter out human messages to return just the chatbot's responses before concatenating and printing the responses. Finally, we'll add a new line to keep responses separated. To use this function, we'll pass our queries as a list called queries, where every second question is a follow up. We now have a full conversation with different queries answered using different tools, our user and chatbot responses clearly marked and follow up questions enabled. Congratulations on completing the course. Let's take a moment to recap your exciting journey through Agentix systems. You started by diving into the essentials of LangChain agents, where you created react agents, enhanced them with custom tools, and configured them to answer follow up questions. You also learned how LangChain uses reasoning and LLMs to provide detailed navigation natural language responses. Next, you explored building chatbots with langgraph, integrating external APIs so your agents could access external sources of information such as Wikipedia. You learned how to simplify building these chatbots by defining graph and agent states, adding nodes and edges, and generating chatbot responses. You then incorporated memory and conversation capabilities, transforming your chatbot into an intelligent context aware assistant. Finally, you experimented with dynamic chatbots capable of switching between tools or making LLM calls based on a user's query. You defined multiple tools, built flexible workflows for function calling, and organized chatbot outputs with memory that can handle dynamic tool calls. You wrapped up with multi turn conversations, creating sophisticated chatbots capable of handling substantial workloads such as answering questions related to a school curriculum. Throughout this course, you've seen how Langgraph plays a vital role in the LangChain ecosystem, enabling you to create scalable and flexible AI workflows. Workflows you've mastered, building foundational agents, integrating tools, and adding advanced memory capabilities to create powerful AI applications. LangChain offers a suite of packages designed to test these systems in real world settings before putting them into production. Lang Smith helps debug your workflows by evaluating agent responses. Langgraph allows customization of agentic workflows and Langgraph platform supports agent deployment. Be sure to explore all of their documentation to build robust agents capable of impressive workloads. Thanks again for joining this course.
Sam.
More from Richie

#359 My Best Friend is AI with Valerie Tiberius, Professor of Philosophy at University of Minnesota

How AI Agents Will Work While You Sleep | Ruslan Salakhutdinov, Professor at Carnegie Mellon

#358 How AI Agents Will Work While You Sleep | Ruslan Salakhutdinov, Professor at Carnegie Mellon







