Repository: langchain-ai/langchain (Python)

Issue:
RunnableLambda: returned runnable called synchronously when using ainvoke
### System Info

langchain on master branch

### Who can help?

_No response_

### Information

- [ ] The official example notebooks/scripts
- [X] My own modified scripts

### Related Components

- [ ] LLMs/Chat Models
- [ ] Embedding Models
- [ ] Prompts / Prompt Templates / Prompt Selectors
- [ ] Output Parsers
- [ ] Document Loaders
- [ ] Vector Stores / Retrievers
- [ ] Memory
- [ ] Agents / Agent Executors
- [ ] Tools / Toolkits
- [X] Chains
- [ ] Callbacks/Tracing
- [X] Async

### Reproduction

```
from langchain.schema.runnable import RunnableLambda

import asyncio


def idchain_sync(__input):
    print(f'sync chain call: {__input}')
    return __input

async def idchain_async(__input):
    print(f'async chain call: {__input}')
    return __input

idchain = RunnableLambda(func=idchain_sync,afunc=idchain_async)




def func(__input):
    return idchain


asyncio.run(RunnableLambda(func).ainvoke('toto'))
#printss 'sync chain call: toto' instead of 'async chain call: toto'                                                                                                                                                  

```


### Expected behavior

LCEL's route can cause chains to be silently run synchronously, while the user uses ainvoke...

When calling a RunnableLambda A returning a chain B with ainvoke, we would expect the new chain B to be called with ainvoke;
However, if the function provided to RunnableLambda A is not async, then the chain B will be called with invoke, silently causing all the rest of the chain to be called synchronously.



Discussion:
_🤖_
<!-- Greeting -->
Hello @nbondoux! 

I'm here to provide assistance with your issue while we wait for a human maintainer to join us. I'm a bot that's designed to help solve bugs, answer questions, and guide users like you on how to contribute. 

I'm currently reviewing your issue regarding the RunnableLambda function in LangChain. I'll provide a more detailed response shortly. Hang tight!
<!-- Will answer -->



_🤖_
<!-- Answer -->
Thank you for bringing this to our attention. The behavior you're experiencing is due to how the `RunnableLambda` class in LangChain handles synchronous and asynchronous functions. 

For synchronous functions, the `invoke` method is used. This method checks if the `func` attribute is present in the instance of the class. If it is, it calls the `_call_with_config` method with the `_invoke` method, the input, and the configuration. 

For asynchronous functions, the `ainvoke` method is used. This method checks if the `afunc` attribute is present in the instance of the class. If it is, it calls the `_acall_with_config` method with the `_ainvoke` method, the input, and the configuration. 

In your case, when calling a `RunnableLambda` A returning a chain B with `ainvoke`, if the function provided to `RunnableLambda` A is not async, then the chain B will be called with `invoke`, silently causing all the rest of the chain to be called synchronously.

This behavior is also related to how the `SequentialChain` class in LangChain handles the `ainvoke` method. Even though `ainvoke` is marked with the `async` keyword, it runs synchronously because it does not perform any IO-bound or CPU-bound tasks concurrently. 

You can find more details about this in the source code of LangChain:
- [libs/langchain/langchain/schema/runnable/base.py](https://github.com/langchain-ai/langchain/blob/master/libs/langchain/langchain/schema/runnable/base.py)
- [libs/langchain/langchain/chains/sequential.py](https://github.com/langchain-ai/langchain/blob/master/libs/langchain/langchain/chains/sequential.py)

A similar issue was also discussed in [Streaming not working when using RunnableLambda](https://github.com/langchain-ai/langchain/issues/13126).

To work around this issue, you could ensure that the function provided to `RunnableLambda` A is async. This would ensure that the chain B is called with `ainvoke`, allowing the rest of the chain to be called asynchronously. 

I hope this helps! If you have any further questions, please let me know.

## Sources
- [Streaming not working when using RunnableLambda](https://github.com/langchain-ai/langchain/issues/13126)
- [libs/langchain/tests/unit_tests/chains/test_sequential.py](https://github.com/langchain-ai/langchain/blob/master/libs/langchain/tests/unit_tests/chains/test_sequential.py)
- [libs/langchain/langchain/chains/sequential.py](https://github.com/langchain-ai/langchain/blob/master/libs/langchain/langchain/chains/sequential.py)
- [libs/langchain/langchain/schema/runnable/base.py](https://github.com/langchain-ai/langchain/blob/master/libs/langchain/langchain/schema/runnable/base.py)

---

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.




Patch:
diff --git a/libs/core/langchain_core/runnables/base.py b/libs/core/langchain_core/runnables/base.py
index 68778af3c1b93..3796d4cc7b93b 100644
--- a/libs/core/langchain_core/runnables/base.py
+++ b/libs/core/langchain_core/runnables/base.py
@@ -2477,8 +2477,19 @@ async def _ainvoke(
         config: RunnableConfig,
         **kwargs: Any,
     ) -> Output:
+        if hasattr(self, "afunc"):
+            afunc = self.afunc
+        else:
+
+            async def f(*args, **kwargs):  # type: ignore[no-untyped-def]
+                return await asyncio.get_running_loop().run_in_executor(
+                    None, partial(self.func, **kwargs), *args
+                )
+
+            afunc = f
+
         output = await acall_func_with_variable_args(
-            self.afunc, input, config, run_manager, **kwargs
+            afunc, input, config, run_manager, **kwargs
         )
         # If the output is a runnable, invoke it
         if isinstance(output, Runnable):
@@ -2539,17 +2550,13 @@ async def ainvoke(
         **kwargs: Optional[Any],
     ) -> Output:
         """Invoke this runnable asynchronously."""
-        if hasattr(self, "afunc"):
-            return await self._acall_with_config(
-                self._ainvoke,
-                input,
-                self._config(config, self.afunc),
-                **kwargs,
-            )
-        else:
-            # Delegating to super implementation of ainvoke.
-            # Uses asyncio executor to run the sync version (invoke)
-            return await super().ainvoke(input, config)
+        the_func = self.afunc if hasattr(self, "afunc") else self.func
+        return await self._acall_with_config(
+            self._ainvoke,
+            input,
+            self._config(config, the_func),
+            **kwargs,
+        )
 
 
 class RunnableEachBase(RunnableSerializable[List[Input], List[Output]]):


Test patch:
diff --git a/libs/core/tests/unit_tests/runnables/test_runnable.py b/libs/core/tests/unit_tests/runnables/test_runnable.py
index 89522ef3f4b85..0ee3915966e94 100644
--- a/libs/core/tests/unit_tests/runnables/test_runnable.py
+++ b/libs/core/tests/unit_tests/runnables/test_runnable.py
@@ -4061,3 +4061,22 @@ def test_with_config_callbacks() -> None:
     # ConfigError: field "callbacks" not yet prepared so type is still a ForwardRef,
     # you might need to call RunnableConfig.update_forward_refs().
     assert isinstance(result, RunnableBinding)
+
+
+@pytest.mark.asyncio
+async def test_ainvoke_on_returned_runnable() -> None:
+    """Verify that a runnable returned by a sync runnable in the async path will
+    be runthroughaasync path (issue #13407)"""
+
+    def idchain_sync(__input: dict) -> bool:
+        return False
+
+    async def idchain_async(__input: dict) -> bool:
+        return True
+
+    idchain = RunnableLambda(func=idchain_sync, afunc=idchain_async)
+
+    def func(__input: dict) -> Runnable:
+        return idchain
+
+    assert await RunnableLambda(func).ainvoke({})