Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Class that extends RunnableSerializable makes the Chain not serializable #19338

Closed
5 tasks done
felipebcs opened this issue Mar 20, 2024 · 2 comments
Closed
5 tasks done
Labels
🤖:bug Related to a bug, vulnerability, unexpected error with an existing feature 🔌: openai Primarily related to OpenAI integrations Ɑ: Runnables Related to Runnables

Comments

@felipebcs
Copy link

felipebcs commented Mar 20, 2024

Checked other resources

  • I added a very descriptive title to this issue.
  • I searched the LangChain documentation with the integrated search.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangChain rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).

Example Code

class CustomRunnable(RunnableSerializable):
    def transform_meaning(self, input):
        if input["input"].find("meaning"):
            input["input"] = input["input"].replace("meaning", "purpose")
        return input

    def invoke(
        self,
        input: Any,
        config: RunnableConfig = None,
        **kwargs: Any,
    ) -> Any:
        # Implement the custom logic here
        return self.transform_meaning(input)
    
llm = ChatOpenAI()

question = 'What is the meaning of life?'
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant."),
    ("user", "{input}")
])
output_parser = StrOutputParser()

original_chain = CustomRunnable() | prompt | llm | output_parser

serialized_chain = langchain_core.load.dumps(original_chain.to_json())
deserialized_chain = langchain_core.load.loads(serialized_chain)

deserialized_chain.invoke({
    "input": question
})

Error Message and Stack Trace (if applicable)

---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[93], line 46
     41 
     42 
     43 
     45 serialized_chain = langchain_core.load.dumps(chain.to_json())
---> 46 deserialized_chain = langchain_core.load.loads(serialized_chain, valid_namespaces=["langchain", "__main__"])
     48 deserialized_chain.invoke({
     49     "input": input
     50 })

File ./site-packages/langchain_core/_api/beta_decorator.py:109, in beta.<locals>.beta.<locals>.warning_emitting_wrapper(*args, **kwargs)
    107     warned = True
    108     emit_warning()
--> 109 return wrapped(*args, **kwargs)

File ./site-packages/langchain_core/load/load.py:132, in loads(text, secrets_map, valid_namespaces)
    113 @beta()
    114 def loads(
    115     text: str,
   (...)
    118     valid_namespaces: Optional[List[str]] = None,
    119 ) -> Any:
    120     """Revive a LangChain class from a JSON string.
    121     Equivalent to `load(json.loads(text))`.
    122 
   (...)
    130         Revived LangChain objects.
    131     """
--> 132     return json.loads(text, object_hook=Reviver(secrets_map, valid_namespaces))

File /usr/lib/python3.10/json/__init__.py:359, in loads(s, cls, object_hook, parse_float, parse_int, parse_constant, object_pairs_hook, **kw)
    357 if parse_constant is not None:
    358     kw['parse_constant'] = parse_constant
--> 359 return cls(**kw).decode(s)

File /usr/lib/python3.10/json/decoder.py:337, in JSONDecoder.decode(self, s, _w)
    332 def decode(self, s, _w=WHITESPACE.match):
    333     """Return the Python representation of ``s`` (a ``str`` instance
    334     containing a JSON document).
    335 
    336     """
--> 337     obj, end = self.raw_decode(s, idx=_w(s, 0).end())
    338     end = _w(s, end).end()
    339     if end != len(s):

File /usr/lib/python3.10/json/decoder.py:353, in JSONDecoder.raw_decode(self, s, idx)
    344 """Decode a JSON document from ``s`` (a ``str`` beginning with
    345 a JSON document) and return a 2-tuple of the Python
    346 representation and the index in ``s`` where the document ended.
   (...)
    350 
    351 """
    352 try:
--> 353     obj, end = self.scan_once(s, idx)
    354 except StopIteration as err:
    355     raise JSONDecodeError("Expecting value", s, err.value) from None

File ./site-packages/langchain_core/load/load.py:60, in Reviver.__call__(self, value)
     53         raise KeyError(f'Missing key "{key}" in load(secrets_map)')
     55 if (
     56     value.get("lc", None) == 1
     57     and value.get("type", None) == "not_implemented"
     58     and value.get("id", None) is not None
     59 ):
---> 60     raise NotImplementedError(
     61         "Trying to load an object that doesn't implement "
     62         f"serialization: {value}"
     63     )
     65 if (
     66     value.get("lc", None) == 1
     67     and value.get("type", None) == "constructor"
     68     and value.get("id", None) is not None
     69 ):
     70     [*namespace, name] = value["id"]

NotImplementedError: Trying to load an object that doesn't implement serialization: {'lc': 1, 'type': 'not_implemented', 'id': ['__main__', 'CustomRunnable'], 'repr': 'CustomRunnable()', 'name': 'CustomRunnable', 'graph': {'nodes': [{'id': 0, 'type': 'schema', 'data': {'title': 'CustomRunnableInput'}}, {'id': 1, 'type': 'runnable', 'data': {'id': ['__main__', 'CustomRunnable'], 'name': 'CustomRunnable'}}, {'id': 2, 'type': 'schema', 'data': {'title': 'CustomRunnableOutput'}}], 'edges': [{'source': 0, 'target': 1}, {'source': 1, 'target': 2}]}}

Description

After testing many suggestions from the kaga-ai and dosu bots, it seems that classes that extends RunnableSerializable are still not considered to be serializable.

What was expected:

  • To be able to create a custom logic to transform the input using RunnableSerializable..
  • Pipe this to a chain..
  • Serialized it so it can be stored somewhere..
  • And then be able to deserialize it before running invoke

What happened:

  • When I add the class based on RunnableSerializable to the chain, it breaks the deserialization because the graph node has the 'type': 'not_implemented' property

Original discussion: #19307

System Info

langchain==0.1.9
langchain-community==0.0.24
langchain-core==0.1.27
langchain-openai==0.0.8
Python 3.10.12
Copy link

dosubot bot commented Mar 20, 2024

Based on the error message you provided, it seems that the CustomRunnable class is not being recognized as serializable by the LangChain framework. This is likely because the is_lc_serializable method in the Serializable class is hardcoded to return False. This method is used to determine if a class is serializable within the LangChain context. If a class extending RunnableSerializable does not override this method to return True, it will be considered not serializable by default.

To resolve this issue, you should override the is_lc_serializable method in your CustomRunnable class to return True. Here is how you can do it:

class CustomRunnable(RunnableSerializable):
    @classmethod
    def is_lc_serializable(cls) -> bool:
        """Override to indicate this class is serializable."""
        return True

    def transform_meaning(self, input):
        if input["input"].find("meaning"):
            input["input"] = input["input"].replace("meaning", "purpose")
        return input

    def invoke(
        self,
        input: Any,
        config: RunnableConfig = None,
        **kwargs: Any,
    ) -> Any:
        # Implement the custom logic here
        return self.transform_meaning(input)

By overriding the is_lc_serializable method to return True, you indicate that instances of CustomRunnable are serializable within the LangChain framework, thus resolving the issue of the Chain not being serializable when such a class is added.

You can find more information about this in the Serializable class source code.

Regarding the RunnableSerializable class, it serves as a base class for creating runnables that can be serialized to JSON. It inherits from both the Serializable and Runnable classes, combining the functionality of being able to run a task with the ability to serialize the runnable's state. This dual inheritance allows instances of RunnableSerializable to be executed and also easily converted to and from JSON format, which is particularly useful for debugging, tracing, and potentially storing the state of runnables in a persistent storage or transferring them across network boundaries. You can find more details about this in the RunnableSerializable class source code.

I hope this helps! If you have any other questions or need further clarification, feel free to ask.

Sources

About Dosu

This response is meant to be useful and save you time. It is not meant to be a precise solution, but rather a starting point for your own research.

Help me be more useful! Please leave a 👍 if this is helpful and 👎 if it is irrelevant.

If you want to continue the conversation, start your reply with @dosu-bot.

@dosubot dosubot bot added Ɑ: Runnables Related to Runnables 🔌: openai Primarily related to OpenAI integrations 🤖:bug Related to a bug, vulnerability, unexpected error with an existing feature labels Mar 20, 2024
@felipebcs
Copy link
Author

Thanks, @dosu-bot. That actually seems to work. I've tried setting is_lc_serializable before, but when extending from a different class. I thought that RunnableSerializable wouldn't need it, but it seems to be necessary.

That fixed my issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🤖:bug Related to a bug, vulnerability, unexpected error with an existing feature 🔌: openai Primarily related to OpenAI integrations Ɑ: Runnables Related to Runnables
Projects
None yet
Development

No branches or pull requests

1 participant