Introduction to LangChain Expression Language in JavaScript

This guide explores how to extend the wonderful features of LangChain to the JavaScript Programming Language for building LLM-powered applications.

Introduction to LangChain Expression Language in JavaScript

Introduction

Being in the AI development space for long enough means coming across several tools and technologies. LangChain is one such framework that provides a host of features, components, and third-party integrations that are needed to build LLM-powered applications.

While Python is mainly used to develop applications with Langchain, a Javascript-compatible version known as Langchain.js, allows developers to build applications like Chatbots, Agents, and RAGs with Javascript. In this tutorial, we will go through the basics of LangChain Expression Language in Javascript, and discuss concepts, syntax, and a very important feature known as chaining.

LangChain Expression Language

The concept of LangChain, as the name implies, revolves around the chaining or stringing together of elements or commands in a programming language. The resulting chains, composed with the LangChain Expression Language (LCEL), become arbitrary sequences called Runnables that play a lot of helpful roles in working with LLMs.

Diagram of a single component and a Chain

The diagram above represents the structure of a basic chain. Within the chain, the output from the previous component are passed in as input into the next component. The chain essentially serves as a single runnable sequence that can be called with a input, processes the input with it's contained components and returns an output.

Basic Example Showing LCEL in use

With the basic definitions out of the way, let's get to actually seeing LCEL in Javascript in action. Keep in mind that LangChain itself does not provide language learning models. For this reason, we usually need to source for models from third-parties like OpenAI, Gemini, Llamma, or Anthropic.

Integrating the Gemini Model Into The Application.

The examples showcased in this tutorial use the Gemini model provided by Google, mainly because it is free and easy to access. Head over to the Google AI docs to generate an API key for use with the Gemini model.

LangChain has numerous third-party integrations for accessing external components like models. So instead of using the official Google AI SDK for JavaScript, we will use the Langchain Google-GenAI package, which contains the Langchain.js integrations for Gemini.

Run npm install @langchain/google-genai in the terminal, and open an index.js file with the following code:

const { ChatGoogleGenerativeAI } = require("@langchain/google-genai");

const gemini = async () => {
    const model = new ChatGoogleGenerativeAI({
        modelName: "gemini-1.5-flash",
        apiKey: "<YOUR_API_KEY>", // Input your API key
    });

    const result = await model.invoke(
        "Write me a short report about toothbrushes" // Random test prompt
    );
    // Actual response can be found in `response.content`
    console.log(result);
};

gemini();

The code above imports the Langchain Google-GenAI package, and runs a function to generate a simple response. This example used the gemini-1.5-flash model, but there are numerous other options available on this page.

Implementing A Basic Chain With A Prompt, A Model, and A Parser

Sure enough, prompting the model directly works for most simple usecases. However, more complex applications will require a few extra components. In addition the Gemini model from the previous example, let's add a prompt and an output parser. The combination of all three of these components will form the basis of our very first chain. The role of each individual components are listed below:

  • Prompt Template: Allows prompts to be dynamic by enabling variable inputs.

  • Generative Model: Provides the reasonining model for processing prompts and generating answers.

  • Output Parsers: Parses the output of the model from the AIMessage object into a more readable format.

Now, back to the index.js file, let's modify the code to implement the three components listed above.

const { ChatGoogleGenerativeAI } = require("@langchain/google-genai");
const { ChatPromptTemplate } = require("@langchain/core/prompts");
const { StringOutputParser } = require("@langchain/core/output_parsers");

require("dotenv").config();

// NORMAL PIPING METHODS -> PROMPT MODEL PARSER
async function main() {
    const model = new ChatGoogleGenerativeAI({
        modelName: "gemini-1.5-flash",
        apiKey: process.env.GOOGLE_API_KEY,
    });
    const prompt = ChatPromptTemplate.fromTemplate(
        // The string in curly braces represents a variable
        "Write me a short report about {topic}"
    );
    const parser = new StringOutputParser();

    // Utilizing the components (traditional method)
    const fullPrompt = await prompt.format({
        topic: "Artificial Intelligence",
    });
    const output = await model.invoke(fullPrompt);
    const result = await parser.invoke(output);

    console.log(result);
}

main();

Here, we import the prompts and parsers modules from the general @langchain/core package. This package contains core abstractions and sche mas of LangChain.js, unlike third-party parties like @langchain/google-genai that integrates external components from Google's Generative AI sdk.

In the modified main function, we create a prompt template and an output parser in addition to the chat model. The prompt template takes in an object with a string, and replaces it with the variable in curly braces within the prompt, while the output parser returns the output from the model in an appropriate string format.

The code above follows the traditional approach to LangChain; treating the different components as individual entities which are invoked separately. While there isn't exactly an issue with this approach, using LCEL makes the code more concise and readable.

// Utilizaing the components (LCEL method)
const chain = prompt.pipe(model).pipe(parser);
const result = await chain.invoke({ topic: "Artificial Intelligence" });
console.log(result);

The above code block creates a chain of components with the pipe method, and then invoked the entire chain with the input object for the prompt. The LCEL approach is a much more elaborate way of using LCEL in javascript, and it has additional features that we will explore in the next section.

Additional LCEL Features and Techniques

Chaining components is just one of the many features LangChain provides. Since this is an introductory tutorial, we will only discuss a few more helpful techniques and usages for using LCEL in Javascript.

  • Streaming Output:

    Most AI applications have a feature that returns the output from the model bit by bit, making it appear like the model is typing out the input, this feature is made possible through streaming. By calling the LangChain stream method on a chain, the output from the model can be passed out in chunks as they are generated.

      const result = await chain.stream({ topic: "Artificial Intelligence" });
      for await (const chunk of result) {
          console.log(chunk); // The text contained in the chunk
          console.log("---"); // A demacator to separate the chunks
      }
    
  • Making Batch Requests:

    Rather than invoking a chain multiple times with different topics, LangChain's batch method provides a more efficient method of simultaneously processing multiple requests. With this method, the chain takes in an array of topics, processes them in parallel, and returns an array of corresponding results.

      const topics = [
          { topic: "Artificial Intelligence" }, // Each input has to be an object
          { topic: "5G Technology" },
          { topic: "Apple Phones" },
      ];
      const result = await chain.batch(topics); // Takes in an array of inputs
      console.log(result); // Array of results
    
  • Functions to Runnables:

    Some applications may need to perform actions that cannot be executed with LangChain's preexisting methods. However, LangChain allows developers to create custom runnable functions with the RunnableLambda class.

      const { RunnableLambda } = require("@langchain/core/runnables");
    
      // Modify the prompt template to always contain a conclusion
      const prompt = ChatPromptTemplate.fromTemplate(
          "Give me a short report about {topic}, it must include a conclusion."
      );
    
      const extractConclusion = RunnableLambda.from((text) => {
          // Looks for the position of the string "Conclusion" in the text
          const conclusionIndex = text.indexOf("Conclusion");
          // Removes everything that comes before it, leaving only the conclusion
          const conclusion = text.slice(conclusionIndex);
    
          return conclusion;
      });
    
      const chain = prompt.pipe(model).pipe(parser).pipe(extractConclusion);
      const result = await chain.invoke({ topic: "Artificial Intelligence" });
    

    The extractConclusion runnable is obtained by converting a function that takes in text input and extract its conclusion. In the chain, the prompt is modified to ensure the response from the model always includes a conclusion. After the model generates the response, the results from the parser is piped into the extractConclsuion component.

  • Parsing JSON Responses:

    Obtaining the results from a model in JSON format reduces the complexity in working with it. While this can be done with strict and specific prompts, a more convenient and consistent way is to make use of LangChain's JSONOutputParser.

      const { JsonOutputParser } = require("@langchain/core/output_parsers");
      ...
      const prompt = ChatPromptTemplate.fromTemplate(
          "Give me a short report about {topic}, return your response in json with 'text' as the key."
      ) // We still need to specify a json response in the prompt
      const jsonParser = new JsonOutputParser()
      ...
      const chain = prompt.pipe(model).pipe(jsonParser) // Change the parser to jsonParser
      const result = await chain.invoke({ topic: "Artificial Intellignece "})
    

    When we specifically request for a json response, most models return their response in a markdown codeblock.

      Model output:
          ```json
              { text: ... }
    
    
      What the `jsonParser` essentially does is to strip off the markdown codeblock and return the results as an actual JSON object.
    
      ```xml
      Model output:
          { text: ...}
    
  • Extending a chain:

    There is no limit to the number of components that can be added to a chain, so long as the output from from one runnable is properly passed as input into the next. For example, we could extend our current chain by restructuring its output into a prompt, and passing it into the model again to do some additional processing.

      const prompt = ChatPromptTemplate.fromTemplate(
          "Give me a short report about {topic}, return your response in json with 'text' as the key."
      );
      const listPrompt = ChatPromptTemplate.fromTemplate(
          "Extract the key points in this text and return it as a list:\n{text}"
      );
    
      const chain = prompt
          .pipe(model)
          .pipe(jsonParser)
          .pipe(checkBenefits)
          .pipe(model)
          .pipe(parser);
    
      const result = await chain.invoke({ topic: "Artificial Intelligence" });
    

While these examples may be impractical, they serve the purpose of demonstrating how these features can be used in a real-world Javascript application. For more examples, check out the LCEL CheatSheet and how-to guides that covers common ways of using LangChain Expression Language in Javascript.

Conclusion

LangChain unarguably makes developing LLM-powered applications less demanding with it's provied classes and third-party integrations. The framework also provides a host of features such as chaining components into Runnable Sequences, streaming output, making batch requests, converting functions to runnables, and parsing JSON responses.

The introduction of these set of functionalities in Javascript, expands the capabilities of the programming language in an AI space dominated by the Python. Through LCEL, Javascript developers also gain the ability to build AI applications more comfortably and effectively.

Next steps:

If you enjoyed this article or found it helpful, please check out my other articles on the Handlebars Template Engine and Building a Simple API with Node.js and Express. The file containing all the code examples used can also be found in this GitHub repo, along with a practice project.

Feel free to drop a comment, or reach out to me directly on my socials:

Cheers! ❤️