> ## Documentation Index
> Fetch the complete documentation index at: https://genai.codewithsiva.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 4. Chains Parallel

> Branch and execute multiple chains concurrently using RunnableParallel

In this section, we will learn how to run multiple branches of an LCEL chain in parallel using `RunnableParallel`, then merge their outputs.

## Objectives

1. Split execution streams to multiple downstream chains concurrently.
2. Utilize `RunnableParallel` to define dictionary-structured processing branches.
3. Combine separate analysis branches (such as pros and cons reviews) into a final unified layout.

***

## Parallel Chains Plan

#### Goal

List the main features of a product, then trigger parallel prompt chains to evaluate the pros and cons of those features separately. Finally, merge both review branches into a unified markdown report.

#### Sample Input

```python theme={null}
{"product_name": "MacBook Pro"}
```

#### Sample Output

A combined text report containing a "Pros:" evaluation and a "Cons:" evaluation.

#### Plan

1. Retrieve features of the product using a primary prompt-model chain.
2. Prepare a Pros Analysis chain: takes features as input and outputs pros list.
3. Prepare a Cons Analysis chain: takes features as input and outputs cons list.
4. Execute both analysis chains concurrently on the retrieved features dictionary using `RunnableParallel`.
5. Pipe the parallel outputs into a merge lambda to combine them into a single report.

***

## Step-by-Step Implementation

### Step 1: Base Feature Retrieval

First, we define a prompt template and chain to retrieve the main features of a product.

```python theme={null}
from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.schema.output_parser import StrOutputParser

model = ChatOpenAI(model="gpt-4o")

# Create prompt to list features
prompt_template = ChatPromptTemplate.from_messages([
    ("system", "You are an expert product reviewer."),
    ("human", "List the main features of the product {product_name}."),
])

# Base chain to retrieve product features
base_chain = prompt_template | model | StrOutputParser()
```

### Step 2: Define Branch Chains

Next, we define two downstream helper chains: one to analyze the **pros** of the features, and one to analyze the **cons**. We wrap the prompt creation in functions and load them into `RunnableLambda`.

```python theme={null}
from langchain.schema.runnable import RunnableLambda

# Pros branch helper
def analyze_pros(features):
    pros_template = ChatPromptTemplate.from_messages([
        ("system", "You are an expert product reviewer."),
        ("human", "Given these features: {features}, list the pros of these features."),
    ])
    return pros_template.format_prompt(features=features)

pros_branch_chain = RunnableLambda(lambda x: analyze_pros(x)) | model | StrOutputParser()

# Cons branch helper
def analyze_cons(features):
    cons_template = ChatPromptTemplate.from_messages([
        ("system", "You are an expert product reviewer."),
        ("human", "Given these features: {features}, list the cons of these features."),
    ])
    return cons_template.format_prompt(features=features)

cons_branch_chain = RunnableLambda(lambda x: analyze_cons(x)) | model | StrOutputParser()
```

### Step 3: Run Sub-branches in Parallel

We use `RunnableParallel` to feed the output of our base features chain into both the `pros` and `cons` branches concurrently.

```python theme={null}
from langchain.schema.runnable import RunnableParallel

# RunnableParallel maps key values to parallel branch executions
parallel_chain = RunnableParallel(branches={"pros": pros_branch_chain, "cons": cons_branch_chain})
```

### Step 4: Merge Branch Outputs

Finally, we create a function that takes the parallel outputs dictionary and formats them into a final combined report.

```python theme={null}
def combine_pros_cons(pros, cons):
    return f"Pros:\n{pros}\n\nCons:\n{cons}"

merge_chain = RunnableLambda(lambda x: combine_pros_cons(x["branches"]["pros"], x["branches"]["cons"]))
```

***

## Complete Combined Code

Below is the complete, consolidated Python script uniting all of the steps above:

```python theme={null}
from dotenv import load_dotenv
from langchain.prompts import ChatPromptTemplate
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import RunnableParallel, RunnableLambda
from langchain_openai import ChatOpenAI

# Load environment variables from .env
load_dotenv()

# Create a ChatOpenAI model
model = ChatOpenAI(model="gpt-4o")

# Define prompt template
prompt_template = ChatPromptTemplate.from_messages(
    [
        ("system", "You are an expert product reviewer."),
        ("human", "List the main features of the product {product_name}."),
    ]
)


# Define pros analysis step
def analyze_pros(features):
    pros_template = ChatPromptTemplate.from_messages(
        [
            ("system", "You are an expert product reviewer."),
            (
                "human",
                "Given these features: {features}, list the pros of these features.",
            ),
        ]
    )
    return pros_template.format_prompt(features=features)


# Define cons analysis step
def analyze_cons(features):
    cons_template = ChatPromptTemplate.from_messages(
        [
            ("system", "You are an expert product reviewer."),
            (
                "human",
                "Given these features: {features}, list the cons of these features.",
            ),
        ]
    )
    return cons_template.format_prompt(features=features)


# Combine pros and cons into a final review
def combine_pros_cons(pros, cons):
    return f"Pros:\n{pros}\n\nCons:\n{cons}"


# Simplify branches with LCEL
pros_branch_chain = (
    RunnableLambda(lambda x: analyze_pros(x)) | model | StrOutputParser()
)

cons_branch_chain = (
    RunnableLambda(lambda x: analyze_cons(x)) | model | StrOutputParser()
)

# Create the combined chain using LangChain Expression Language (LCEL)
chain = (
    prompt_template
    | model
    | StrOutputParser()
    | RunnableParallel(branches={"pros": pros_branch_chain, "cons": cons_branch_chain})
    | RunnableLambda(lambda x: combine_pros_cons(x["branches"]["pros"], x["branches"]["cons"]))
)

# Run the chain
result = chain.invoke({"product_name": "MacBook Pro"})

# Output
print(result)
```

## RunnablePassthrough Example

`RunnablePassthrough` allows you to pass inputs through unchanged, or combine them with extra values. It is very useful when you want to forward user inputs to multiple downstream steps (like sending a raw question both to a retriever and directly to the final prompt context).

Below is a simple example showing how to pass a single input value through while dynamically injecting extra arguments:

```python theme={null}
from langchain_core.runnables import RunnablePassthrough
from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.schema.output_parser import StrOutputParser

# Initialize model
model = ChatOpenAI(model="gpt-4o")

# Define template requiring 'topic' and 'style'
prompt = ChatPromptTemplate.from_template("Write a short poem about {topic} in the style of {style}.")

# Construct chain
# RunnablePassthrough() passes the input 'topic' through as-is,
# while the lambda dynamically assigns a static value for 'style'.
mapping_chain = {
    "topic": RunnablePassthrough(),
    "style": lambda x: "Shakespearean English"
}

chain = mapping_chain | prompt | model | StrOutputParser()

# Invoke the chain by passing the topic directly as a string
result = chain.invoke("artificial intelligence")
print(result)
```

> \[!NOTE]
> **Implicit Dict Coercion to `RunnableParallel`**
>
> You might notice that in LCEL, we sometimes pipe into a raw Python dictionary containing multiple keys and downstream chains, such as:
>
> ```python theme={null}
> chain = (
>     prompt_template
>     | model
>     | StrOutputParser()
>     | {"pros": pros_branch_chain, "cons": cons_branch_chain}  # Raw dictionary
> )
> ```
>
> Under the hood, whenever a dictionary is used within an LCEL pipeline, LangChain **implicitly coerces** it into a `RunnableParallel`.
>
> During this coercion, any standard Python callables (such as functions or lambdas) are automatically wrapped inside a `RunnableLambda`.

***

## Practice & Exercises

To reinforce what you've learned in this section, practice with the interactive notebook:

<CardGroup cols={1}>
  <Card title="Practice & Exercises" icon="laptop-code">
    Practice setting up parallel processing threads, mapping schemas dynamically, and merging concurrent branch results.

    [💻 VS Code](vscode://file/Users/sivaprasad/Downloads/GenAI%20With%20Python/public/notebooks/langchain/4_chains_parallel.ipynb) | [🚀 Colab](https://colab.research.google.com/github/prasad230776/genai-course/blob/master/public/notebooks/langchain/4_chains_parallel.ipynb) | <a href="/public/notebooks/langchain/4_chains_parallel.ipynb" download>📥 Download</a>
  </Card>
</CardGroup>
