Runnable Context Provider
Labels: name: auto:enhancement, name: area: models

<!-- Thank you for contributing to LangChain!

Replace this entire comment with:
  - **Description:** a description of the change, 
  - **Issue:** the issue # it fixes (if applicable),
  - **Dependencies:** any dependencies required for this change,
  - **Tag maintainer:** for a quicker response, tag the relevant maintainer (see below),
  - **Twitter handle:** we announce bigger features on Twitter. If your PR gets announced, and you'd like a mention, we'll gladly shout you out!

Please make sure your PR is passing linting and testing before submitting. Run `make format`, `make lint` and `make test` to check this locally.

See contribution guidelines for more information on how to write/run tests, lint, etc: 
https://github.com/langchain-ai/langchain/blob/master/.github/CONTRIBUTING.md

If you're adding a new integration, please include:
  1. a test for the integration, preferably unit tests that do not rely on network access,
  2. an example notebook showing its use. It lives in `docs/extras` directory.

If no one reviews your PR within a few days, please @-mention one of @baskaryan, @eyurtsev, @hwchase17.
 -->

(Note: still WIP, would appreciate some feedback from maintainer)

**Description**: This PR adds a new core Runnable component called `RunnableContextProvider`. The motivation behind this runnable is that sometimes when writing long and complex chains, developers need to pass some core piece of data across multiple stages of the chain. For example, when working with a naive RAG where the retriever retrieves context (say `List[str]` for simplicity), one common case is to pass the retrieved `context` and original `question` as part of the output (say for the sake of doing evals or other data manipulations). The original way of achieving this might look like below:
```python
retriever = RunnableLambda(lambda _: ["doc 1 ...", ..., "doc n"])  # mock retriever

retrieval_chain =  {
    "context": retriever,
    "question": RunnablePassthrough(),
}

format_chain = {
    "original_context": itemgetter("context"),
    "formatted_context": itemgetter("context") | RunnableLambda(lambda x: "## Documents" + "\n\n".join(x)),
    "question": itemgetter("question"),
}

generation_chain = {
    "result": { 
        "question": itemgetter("question"), 
        "formatted_context": itemgetter("formatted_context") 
    } | prompt | llm,
    "context": itemgetter("original_context"),
    "question": itemgetter("question"),
}

rag_chain = retrieval_chain | format_chain | generation_chain
```
Look at how complex and unreadable the chain becomes even for a naive RAG example. Most of the complexity is due to passing extra data around, which adds lots of `itemgetter` and data `Passthrough` which should be unnecessary.

`RunnableContextProvider` solves this issue by allowing data sharing across different stages of chain without having to explicitly wire up the data connection pipeline. Here's the basic API usage for implementing the same naive RAG as above:
```python
from langchain.schema.runnable import RunnableContextProvider

format_context = RunnableLambda(lambda x: "## Documents" + "\n\n".join(x))
rag_chain = RunnableContextProvider(
    lambda getter, setter: {
        "question": RunnablePassthrough() | setter("question"),
        "formatted_context": retriever | setter("context") | format_context,
    }
    | prompt
    | llm
    | {
        "result": RunnablePassthrough(),
        "context": getter("context"),
        "question": getter("question"),
    }
)
```
- `getter`: an instance of `RunnableContextGetter`, which retrieves data from a key-value source automatically initialized in the background. It's a `Runnable` that outputs the value retrieved from the shared key-value source identified by the given key (input to `RunnableContextGetter` is ignored).
- `setter`: an instance of `RunnableContextSetter`, which updates value into the shared source given the key. The value written into the key is the output from the previous piped Runnable chain. Output from `RunnableContextSetter` is connected to the output of the chain immediately before the `setter`. Therefore, in the case of
```python
retriever | setter("context") | format_context
```
The lambda `format_context` would take the output from `retriever` as its input.
- batching: `batch` and `abatch` work out-of-box. Each single chain call inside the batch has a unique source. In other words, chains across different batch call do not share the same source ideally.

Also, support the decorator's pattern.
```python
@context_provider
def rag_chain(getter, setter):
    retriever = RunnableLambda(lambda _: ["doc 1 ...", ..., "doc n"])  # mock retriever
    format_context = RunnableLambda(lambda x: "## Documents" + "\n\n".join(x))
    
    return (
        {
            "question": RunnablePassthrough() | setter("question"),
            "formatted_context": retriever | setter("context") | format_context,
        }
        | prompt
        | llm
        | {
            "result": RunnablePassthrough(),
            "context": getter("context"),
            "question": getter("question"),
        }
    )

rag_chain.invoke(...)
```

## Some Improvement Considerations
- Allows `setter` to set multiple keys based on the same input. Potential API usage:
```python
retriever | setter(lambda x: {
   "list_context": x,
    "string_context": "\n\n".join(x),
    "context_size": len(x)
}) | format_context
```
- Naming: should we rename `getter` and `setter` to be `inject` and `provide` instead?
- Whether we should allow each key to be set only once.
- One caveat of `RunnableContextProvider` is that both `getter` and `setter` on the same key cannot appear inside the `RunnableParallel` because the order of execution is not guaranteed.

**Issue**: None.
**Dependencies**: None.
**Tag Maintainers**: @nfcampos 
**Twitter handle**: [will add in the future]