Repository: MaartenGr/KeyBERT
Pull request #261: Remove dependency on deprecated langchain apis
Author: login: shengbo-ma
Created: 2025-03-09T23:17:36Z | Merged: 2025-03-25T11:51:56Z

Issue: #259

Description:

- [x] replace deprecated lagnchain api `.run()` with `.invoke()`
- [x] replace deprecated `load_qa_chain` with a simple chain of langchain
- [x] a new default prompt 
- [x] update docstring for documentation use


I did not add a test for this change, since I don't see langchain installed in KeyBERT test env, so posted the test below (Same as in the `keybert.llm.LangChain` docsting)

New user experience, also how to test this change 

```python
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

from keybert import KeyLLM
from keybert.llm import LangChain

_llm = ChatOpenAI(
    model="gpt-4o",
    api_key="my-openai-api-key",
    temperature=0,
)
_prompt = ChatPromptTemplate(
    [
        ("human", LangChain.DEFAULT_PROMPT_TEMPLATE),  # the default prompt from KeyBERT
    ]
)
chain = _prompt | _llm | StrOutputParser()


# Create your LLM
llm = LangChain(chain)

# Load it in KeyLLM
kw_model = KeyLLM(llm)

# Extract keywords
docs = [
    "KeyBERT: A minimal method for keyword extraction with BERT. The keyword extraction is done by finding the sub-phrases in a document that are the most similar to the document itself. First, document embeddings are extracted with BERT to get a document-level representation. Then, word embeddings are extracted for N-gram words/phrases. Finally, we use cosine similarity to find the words/phrases that are the most similar to the document. The most similar words could then be identified as the words that best describe the entire document.",
    "KeyLLM: A minimal method for keyword extraction with Large Language Models (LLM). The keyword extraction is done by simply asking the LLM to extract a number of keywords from a single piece of text.",
]
candidates = [
    ["keyword extraction", "Large Language Models", "LLM", "BERT", "transformer", "embeddings"],
    ["keyword extraction", "Large Language Models", "LLM", "BERT", "transformer", "embeddings"],
]
keywords = kw_model.extract_keywords(docs=docs, candidate_keywords=candidates)
print(keywords)
# [['keyword extraction', 'BERT', 'embeddings'], ['keyword extraction', 'Large Language Models', 'LLM']]

```

# Discussion
@MaartenGr Please review these concerns below. 

- While implementing this PR, I feel it a bit hard to identify what keybert wants to provide on top of langchain, so please review if the new user experience above makes sense. My understanding is that 
  - `keybert.llm.LangChain` provides
    - a default prompt template that nicely extract keywords from a doc using candidate keywords
    - keywords output parsed as list[str] such that can be integrated with KeyLLM
  - I am assuming that users have basic understanding of writing simple langchain chain below since they choose to use langchain with keybert.
  - I am assume the motivation of `keybert.llm.LangChain` is to support a chain, not a LLM model. 
However, a chain can be so flexible, containing prompts, llm, output formats / parsers, etc, that it is very tricky trying to change the prompt template of a given chain inside our own `keybert.llm.LangChain` by taking a prompt arg. So in this PR, I am suggesting users to use `LangChain.DEFAULT_PROMPT_TEMPLATE` when they create the chain. And prompt arg is removed from `keybert.llm.LangChain.__init__`.

- I see `keybert.llm.LangChain` imports langchain in source code [this line](https://github.com/MaartenGr/KeyBERT/blob/master/keybert/llm/_langchain.py#L3), but does not list it as a dependency in `pyproject.toml`. Are we assuming before` keybert.llm.LangChain` is called, user must have installed langchain since they have to write their only chain?


Diff:
diff --git a/docs/guides/llms.md b/docs/guides/llms.md
index b7b07f0d..a98237fc 100644
--- a/docs/guides/llms.md
+++ b/docs/guides/llms.md
@@ -172,32 +172,43 @@ kw_model = KeyLLM(llm)
 
 ### **LangChain**
 
-To use LangChain, we can simply load in any LLM and pass that as a QA-chain to KeyLLM.
+To use `langchain` LLM client in KeyLLM, we can simply load in any LLM in `langchain` and pass that to KeyLLM.
 
-We install the package first:
+We install langchain and corresponding LLM provider package first. Take OpenAI as an example:
 
 ```bash
 pip install langchain
+pip install langchain-openai # LLM provider package
 ```
+> [!NOTE]
+> KeyBERT only supports `langchain >= 0.1`
+
 
-Then we run LangChain as follows:
+Then create your LLM client with `langchain`
 
 
 ```python
-from langchain.chains.question_answering import load_qa_chain
-from langchain.llms import OpenAI
-chain = load_qa_chain(OpenAI(temperature=0, openai_api_key=my_openai_api_key), chain_type="stuff")
+from langchain_openai import ChatOpenAI
+
+_llm = ChatOpenAI(
+    model="gpt-4o",
+    api_key="my-openai-api-key",
+    temperature=0,
+)
 ```
 
-Finally, you can pass the chain to KeyBERT as follows:
+Finally, pass the `langchain` llm client to KeyBERT as follows:
 
 ```python
 from keybert.llm import LangChain
 from keybert import KeyLLM
 
 # Create your LLM
-llm = LangChain(chain)
+llm = LangChain(_llm)
 
 # Load it in KeyLLM
 kw_model = KeyLLM(llm)
+
+# Extract keywords
+keywords = kw_model.extract_keywords(MY_DOCUMENTS)
 ```
diff --git a/keybert/llm/__init__.py b/keybert/llm/__init__.py
index abe7c7d6..b222d953 100644
--- a/keybert/llm/__init__.py
+++ b/keybert/llm/__init__.py
@@ -32,9 +32,16 @@
 # LangChain Generator
 try:
     from keybert.llm._langchain import LangChain
-except ModuleNotFoundError:
-    msg = "`pip install langchain` \n\n"
-    LangChain = NotInstalled("langchain", "langchain", custom_msg=msg)
+except ModuleNotFoundError as e:
+    if e.name == "langchain":
+        msg = "`pip install langchain` \n\n"
+        LangChain = NotInstalled("langchain", "langchain", custom_msg=msg)
+    elif e.name == "langchain_core":
+        msg = "`pip install -U langchain` to upgrade to langchain>=0.1\n\n"
+        LangChain = NotInstalled("langchain", "langchain", custom_msg=msg)
+    else:
+        # not caused by importing langchain or langchain_core
+        raise e
 
 # LiteLLM
 try:
diff --git a/keybert/llm/_langchain.py b/keybert/llm/_langchain.py
index f786109e..681f2536 100644
--- a/keybert/llm/_langchain.py
+++ b/keybert/llm/_langchain.py
@@ -1,29 +1,37 @@
-from tqdm import tqdm
 from typing import List
-from langchain.docstore.document import Document
+
+from langchain.prompts import ChatPromptTemplate, PromptTemplate
+from langchain_core.language_models.chat_models import BaseChatModel as LangChainBaseChatModel
+from langchain_core.language_models.llms import BaseLLM as LangChainBaseLLM
+from langchain_core.output_parsers import StrOutputParser
+from tqdm import tqdm
+
 from keybert.llm._base import BaseLLM
 from keybert.llm._utils import process_candidate_keywords
 
-
-DEFAULT_PROMPT = "What is this document about? Please provide keywords separated by commas."
+"""NOTE
+KeyBERT only supports `langchain >= 0.1` which features:
+- [Runnable Interface](https://python.langchain.com/docs/concepts/runnables/)
+- [LangChain Expression Language (LCEL)](https://python.langchain.com/docs/concepts/lcel/)
+"""
 
 
 class LangChain(BaseLLM):
     """Using chains in langchain to generate keywords.
 
-    Currently, only chains from question answering is implemented. See:
-    https://langchain.readthedocs.io/en/latest/modules/chains/combine_docs_examples/question_answering.html
-
-    NOTE: The resulting keywords are expected to be separated by commas so
-    any changes to the prompt will have to make sure that the resulting
-    keywords are comma-separated.
-
     Arguments:
-        chain: A langchain chain that has two input parameters, `input_documents` and `query`.
+        llm: A langchain LLM class. e.g ChatOpenAI, OpenAI, etc.
         prompt: The prompt to be used in the model. If no prompt is given,
-                `self.default_prompt_` is used instead.
+                `self.DEFAULT_PROMPT_TEMPLATE` is used instead.
+                NOTE: The prompt should contain:
+                1. Placeholders
+                - `[DOCUMENT]`: Required. The document to extract keywords from.
+                - `[CANDIDATES]`: Optional. The candidate keywords to fine-tune the extraction.
+                2. Output format instructions
+                - Include this or something similar in your prompt:
+                    "Extracted keywords must be separated by comma."
         verbose: Set this to True if you want to see a progress bar for the
-                 keyword extraction.
+                keyword extraction.
 
     Usage:
 
@@ -32,14 +40,18 @@ class LangChain(BaseLLM):
     like openai:
 
     `pip install langchain`
-    `pip install openai`
+    `pip install langchain-openai`
 
     Then, you can create your chain as follows:
 
     ```python
-    from langchain.chains.question_answering import load_qa_chain
-    from langchain.llms import OpenAI
-    chain = load_qa_chain(OpenAI(temperature=0, openai_api_key=my_openai_api_key), chain_type="stuff")
+    from langchain_openai import ChatOpenAI
+
+    _llm = ChatOpenAI(
+        model="gpt-4o",
+        api_key="my-openai-api-key",
+        temperature=0,
+    )
     ```
 
     Finally, you can pass the chain to KeyBERT as follows:
@@ -49,14 +61,39 @@ class LangChain(BaseLLM):
     from keybert import KeyLLM
 
     # Create your LLM
-    llm = LangChain(chain)
+    llm = LangChain(_llm)
 
     # Load it in KeyLLM
     kw_model = KeyLLM(llm)
 
     # Extract keywords
-    document = "The website mentions that it only takes a couple of days to deliver but I still have not received mine."
-    keywords = kw_model.extract_keywords(document)
+    docs = [
+        "KeyBERT: A minimal method for keyword extraction with BERT. The keyword extraction is done by finding the sub-phrases in a document that are the most similar to the document itself. First, document embeddings are extracted with BERT to get a document-level representation. Then, word embeddings are extracted for N-gram words/phrases. Finally, we use cosine similarity to find the words/phrases that are the most similar to the document. The most similar words could then be identified as the words that best describe the entire document.",
+        "KeyLLM: A minimal method for keyword extraction with Large Language Models (LLM). The keyword extraction is done by simply asking the LLM to extract a number of keywords from a single piece of text.",
+    ]
+    keywords = kw_model.extract_keywords(docs=docs)
+    print(keywords)
+
+    # Output:
+    # [
+    #     ['KeyBERT', 'keyword extraction', 'BERT', 'document embeddings', 'word embeddings', 'N-gram phrases', 'cosine similarity', 'document representation'],
+    #     ['KeyLLM', 'keyword extraction', 'Large Language Models', 'LLM', 'minimal method']
+    # ]
+
+
+    # fine tune with candidate keywords
+    candidates = [
+        ["keyword extraction", "Large Language Models", "LLM", "BERT", "transformer", "embeddings"],
+        ["keyword extraction", "Large Language Models", "LLM", "BERT", "transformer", "embeddings"],
+    ]
+    keywords = kw_model.extract_keywords(docs=docs, candidate_keywords=candidates)
+    print(keywords)
+
+    # Output:
+    # [
+    #     ['keyword extraction', 'BERT', 'document embeddings', 'word embeddings', 'cosine similarity', 'N-gram phrases'],
+    #     ['KeyLLM', 'keyword extraction', 'Large Language Models', 'LLM']
+    # ]
     ```
 
     You can also use a custom prompt:
@@ -67,16 +104,35 @@ class LangChain(BaseLLM):
     ```
     """
 
+    DEFAULT_PROMPT_TEMPLATE = """
+# Task
+You are provided with a document and possiblily a list of candidate keywords.
+
+If no candidate keywords are provided, your task to is extract keywords from the document.
+If candidate keywords are provided, your task is  to improve the candidate keywords to best describe the topic of the document.
+
+# Document
+[DOCUMENT]
+
+# Candidate Keywords
+[CANDIDATES]
+
+
+Now extract the keywords from the document.
+The keywords must be comma separated.
+For example: "keyword1, keyword2, keyword3"
+"""
+
     def __init__(
         self,
-        chain,
+        llm: LangChainBaseChatModel | LangChainBaseLLM,
         prompt: str = None,
         verbose: bool = False,
     ):
-        self.chain = chain
-        self.prompt = prompt if prompt is not None else DEFAULT_PROMPT
-        self.default_prompt_ = DEFAULT_PROMPT
+        self.llm = llm
+        self.prompt = prompt if prompt is not None else self.DEFAULT_PROMPT_TEMPLATE
         self.verbose = verbose
+        self.chain = self._get_chain()
 
     def extract_keywords(self, documents: List[str], candidate_keywords: List[List[str]] = None):
         """Extract topics.
@@ -95,12 +151,19 @@ def extract_keywords(self, documents: List[str], candidate_keywords: List[List[s
         candidate_keywords = process_candidate_keywords(documents, candidate_keywords)
 
         for document, candidates in tqdm(zip(documents, candidate_keywords), disable=not self.verbose):
-            prompt = self.prompt.replace("[DOCUMENT]", document)
-            if candidates is not None:
-                prompt = prompt.replace("[CANDIDATES]", ", ".join(candidates))
-            input_document = Document(page_content=document)
-            keywords = self.chain.run(input_documents=[input_document], question=self.prompt).strip()
+            keywords = self.chain.invoke({"DOCUMENT": document, "CANDIDATES": candidates})
             keywords = [keyword.strip() for keyword in keywords.split(",")]
             all_keywords.append(keywords)
 
         return all_keywords
+
+    def _get_chain(self):
+        """Get the chain using LLM and prompt."""
+        # format prompt for langchain template placeholders
+        prompt = self.prompt.replace("[DOCUMENT]", "{DOCUMENT}").replace("[CANDIDATES]", "{CANDIDATES}")
+        # check if the model is a chat model
+        is_chat_model = isinstance(self.llm, LangChainBaseChatModel)
+        # langchain prompt template
+        prompt_template = ChatPromptTemplate([("human", prompt)]) if is_chat_model else PromptTemplate(template=prompt)
+        # chain
+        return prompt_template | self.llm | StrOutputParser()


Reviews:
user: login: MaartenGr
body: Left the tiniest of comments, but otherwise it looks great!
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-19T14:33:46Z
user: login: shengbo-ma
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T23:44:35Z

Review comments:
diff_hunk: @@ -1,29 +1,37 @@
-from tqdm import tqdm
 from typing import List
-from langchain.docstore.document import Document
+
+from langchain.prompts import ChatPromptTemplate, PromptTemplate
+from langchain_core.language_models.chat_models import BaseChatModel as LangChainBaseChatModel
+from langchain_core.language_models.llms import BaseLLM as LangChainBaseLLM
+from langchain_core.output_parsers import StrOutputParser
+from tqdm import tqdm
+
 from keybert.llm._base import BaseLLM
 from keybert.llm._utils import process_candidate_keywords
 
-
-DEFAULT_PROMPT = "What is this document about? Please provide keywords separated by commas."
+"""NOTE
+langchain >= 0.1 is required. Which supports:
+- chain.invoke()
+- LangChain Expression Language (LCEL) is used and it is not compatible with langchain < 0.1.
+"""
path: keybert/llm/_langchain.py
user: login: MaartenGr
body: Might be best to also update this in the documentation here: https://github.com/MaartenGr/KeyBERT/blob/master/docs/guides/llms.md
created_at: 2025-03-19T14:31:53Z
author_association: OWNER
diff_hunk: @@ -1,29 +1,37 @@
-from tqdm import tqdm
 from typing import List
-from langchain.docstore.document import Document
+
+from langchain.prompts import ChatPromptTemplate, PromptTemplate
+from langchain_core.language_models.chat_models import BaseChatModel as LangChainBaseChatModel
+from langchain_core.language_models.llms import BaseLLM as LangChainBaseLLM
+from langchain_core.output_parsers import StrOutputParser
+from tqdm import tqdm
+
 from keybert.llm._base import BaseLLM
 from keybert.llm._utils import process_candidate_keywords
 
-
-DEFAULT_PROMPT = "What is this document about? Please provide keywords separated by commas."
+"""NOTE
+langchain >= 0.1 is required. Which supports:
+- chain.invoke()
+- LangChain Expression Language (LCEL) is used and it is not compatible with langchain < 0.1.
+"""
path: keybert/llm/_langchain.py
user: login: shengbo-ma
body: Sure. Updated
created_at: 2025-03-19T23:44:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: MaartenGr
created_at: 2025-03-14T07:22:22Z
body: @shengbo-ma Thanks for the PR!

> keybert.llm.LangChain provides
>   a default prompt template that nicely extract keywords from a doc using candidate keywords
>   keywords output parsed as list[str] such that can be integrated with KeyLLM

That's correct, like the other LLMs it is merely used as the main model to parse the documents and/or keywords according the 5 principles [here](https://maartengr.github.io/KeyBERT/guides/keyllm.html).


> I am assuming that users have basic understanding of writing simple langchain chain below since they choose to use langchain with keybert.

The intent is that users do not need to have a basic understanding of any LLM provider/framework but just can throw in any model (GGUF or otherwise). LangChain is a tricky library to be familiar with considering the quick changes in their API over the years. As such, I would prefer the most straightforward implementation so that users do not have to learn a framework. Compared to the LangChain implementation in BERTopic, I believe the keyword extraction use case is much simpler.

> I am assume the motivation of keybert.llm.LangChain is to support a chain, not a LLM model.
However, a chain can be so flexible, containing prompts, llm, output formats / parsers, etc, that it is very tricky trying to change the prompt template of a given chain inside our own keybert.llm.LangChain by taking a prompt arg. So in this PR, I am suggesting users to use LangChain.DEFAULT_PROMPT_TEMPLATE when they create the chain. And prompt arg is removed from keybert.llm.LangChain.__init__.

It is actually to support an LLM model considering many users encounter LangChain as one of the first techniques to load a model (rather than llama-cpp-python, ollama, vllm, etc.).

That said, I would actually prefer to keep the `prompt` variable here as users would only have to replace `{DOCUMENTS}` with `[DOCUMENTS]`. Perhaps you can check out the BERTopic implementation here: https://github.com/MaartenGr/BERTopic/blob/master/bertopic/representation/_langchain.py

> I see keybert.llm.LangChain imports langchain in source code [this line](https://github.com/MaartenGr/KeyBERT/blob/master/keybert/llm/_langchain.py#L3), but does not list it as a dependency in pyproject.toml. Are we assuming before keybert.llm.LangChain is called, user must have installed langchain since they have to write their only chain?

Yes, LangChain needs to be installed beforehand, but it does not need to be included in the pyproject.toml considering there are currently no tests for LangChain. It will give an error if it is not installed: https://github.com/MaartenGr/KeyBERT/blob/master/keybert/llm/__init__.py
author_association: OWNER
user: login: shengbo-ma
created_at: 2025-03-14T19:33:52Z
body: @MaartenGr Thanks for the info. Will change this PR accordingly.

> As such, I would prefer the most straightforward implementation so that users do not have to learn a framework. 

Will match what BERTopic does for langchain. 
Glad to see BERTopic also replies on the user to copy the LangChain Expression Language (LCEL) from BERTopic documentation and feed it into LangChain api, like did in this PR. 

> Compared to the LangChain implementation in BERTopic, I believe the keyword extraction use case is much simpler.

Agree. I don't think load_qa_chain is necessary here. A simple and straight forward chain like in this PR should be good enough. Thus one less dependency from langchain.

>   It is actually to support an LLM model considering many users encounter LangChain as one of the first techniques to load a model (rather than llama-cpp-python, ollama, vllm, etc.).
> That said, I would actually prefer to keep the prompt variable here as users would only have to replace {DOCUMENTS} with [DOCUMENTS]. Perhaps you can check out the BERTopic implementation here:

Got it. Will restore the prompt arg in next commits.

author_association: CONTRIBUTOR
user: login: shengbo-ma
created_at: 2025-03-17T02:42:48Z
body: > It is actually to support an LLM model considering many users encounter LangChain as one of the first techniques to load a model (rather than llama-cpp-python, ollama, vllm, etc.).

@MaartenGr Thanks for this clarification. It inspires me to simplify the user experience as below. Please let me know if it makes sense.

Updates
- `keybert.llm.LangChain.__init__` takes a langchain llm class rather than a chain, so user simply pass their langchain llm like `ChatOpenAI`
- `keybert.llm.LangChain.__init__` takes a prompt again.
- A simple chain is constructed inside `keybert.llm.LangChain.__init__` using the input llm and prompt. User doesn't need any knowledge of langchain.
- Updated the example in doc string accordingly. 
- langchain >=0.1 is required for this PR. since we are now using .invoke instead of .run. Also the simple chain uses LangChain Expression Language which also requires lagnchain >=0.1

The new implementation
```py
from langchain_openai import ChatOpenAI

from keybert import KeyLLM
from keybert.llm import LangChain

_llm = ChatOpenAI(
    model="gpt-4o",
    api_key="my-openai-api-key",
    temperature=0,
)


# Create your LLM
llm = LangChain(_llm)

# Load it in KeyLLM
kw_model = KeyLLM(llm)

# Extract keywords
docs = [
    "KeyBERT: A minimal method for keyword extraction with BERT. The keyword extraction is done by finding the sub-phrases in a document that are the most similar to the document itself. First, document embeddings are extracted with BERT to get a document-level representation. Then, word embeddings are extracted for N-gram words/phrases. Finally, we use cosine similarity to find the words/phrases that are the most similar to the document. The most similar words could then be identified as the words that best describe the entire document.",
    "KeyLLM: A minimal method for keyword extraction with Large Language Models (LLM). The keyword extraction is done by simply asking the LLM to extract a number of keywords from a single piece of text.",
]
keywords = kw_model.extract_keywords(docs=docs)
print("with no candidates")
print(keywords)

# Output:
# [
#     ['KeyBERT', 'keyword extraction', 'BERT', 'document embeddings', 'word embeddings', 'N-gram phrases', 'cosine similarity', 'document representation'],
#     ['KeyLLM', 'keyword extraction', 'Large Language Models', 'LLM', 'minimal method']
# ]


# fine tune with candidate keywords
candidates = [
    ["keyword extraction", "Large Language Models", "LLM", "BERT", "transformer", "embeddings"],
    ["keyword extraction", "Large Language Models", "LLM", "BERT", "transformer", "embeddings"],
]
keywords = kw_model.extract_keywords(docs=docs, candidate_keywords=candidates)
print("with candidates")
print(keywords)

# Output:
# [
#     ['keyword extraction', 'BERT', 'document embeddings', 'word embeddings', 'cosine similarity', 'N-gram phrases'],
#     ['KeyLLM', 'keyword extraction', 'Large Language Models', 'LLM']
# ]

```
author_association: CONTRIBUTOR
user: login: shengbo-ma
created_at: 2025-03-17T04:39:55Z
body: This PR is ready for review @MaartenGr 
author_association: CONTRIBUTOR
user: login: shengbo-ma
created_at: 2025-03-20T00:43:50Z
body: @MaartenGr 
Added a simple version check for `langchain`. 

`packaging` package is used for version check. It is a popular package for python versioning. AND it is an existing (transient) dependency of `keybert`
- keybert -> sentence-transformers -> transformers -> packaging

Despite of that, it still would be better to include it explicitly in `pyproject.toml`, just in case `sentence-transformers` is no longer a dependency, or they drop the dependency, although not very likely.
Please let me know if it makes sense to use `packaging` in this PR and whether to explicitly add `packaging` as a dependency in `pyproject.toml`.
author_association: CONTRIBUTOR
user: login: MaartenGr
created_at: 2025-03-21T13:46:13Z
body: Thanks for the initiative! There's an edge case for this, namely that it is possible to install KeyBERT without `sentence-transformers` by simply running `pip install keybert --no-deps scikit-learn model2vec`. 

As such, it will throw an error when using this functionality. I would prefer not using this additional dependency if it is not absolutely needed for the core functionality of KeyBERT (which includes this light-weight installation option). 
author_association: OWNER
user: login: shengbo-ma
created_at: 2025-03-22T06:30:10Z
body: > I would prefer not using this additional dependency if it is not absolutely needed for the core functionality of KeyBERT (which includes this light-weight installation option).

@MaartenGr Yeah make sense. 
I removed the version check logic that uses `packaging` dependency. Instead, I catch the `ModuleNotFoundError` for `langchian_core` which was introduced since `langchain` 0.1.0.
It is equivalent to check version >= 0.1
author_association: CONTRIBUTOR
user: login: MaartenGr
created_at: 2025-03-25T11:51:07Z
body: Awesome, thank you for the work on this. Everything looks in order!
author_association: OWNER