Conversation
The decorator code in `langfuse/_client/observe.py` seems to have been built to support the case of observing an async generator however it misses the mark by using `asyncio.iscoroutinefunction` to decide whether to wrap it in the async or the sync implementation. That method doesn't detect async generators. The solution is to use the inspect module and check for both cases.
There was a problem hiding this comment.
LGTM
1 file reviewed, no comments
Edit PR Review Bot Settings | Greptile
There was a problem hiding this comment.
LGTM
1 file reviewed, no comments
Edit PR Review Bot Settings | Greptile
|
related to langfuse/langfuse#7226 |
|
Thanks a lot for your contribution, will be added in #1259 |
|
Released in Python SDK v3.2.1 |
|
@hassiebp thanks! I tested the new version and now the issue I reported returns the output correctly! There is still some unexpected behavior that I mentioned in #1233 where each "yield" from the async generator produces it's own trace, but not sure if that should be considered a bug. In any case one can easily workaround the problem by starting a trace before creating the generator. However while re-testing this I found something else that was unexpected for me, and maybe worth opening a new issue about it (let me know what you think). Here's a test case: What I see in langfuse is this (link to trace in langfuse): I think it's probably ok that the two spans from the async for loop's inner code are nested under the top level span. But shouldn't the three spans that were created inside the async generator be nested under the async generator's span? |

The decorator code in
langfuse/_client/observe.pyseems to have been built to support the case of observing both async and async generators. Based on the code it seems the intent is that the resulting generator should be wrapped in an nested async generator function so to capture the output. However this only works for the case of the sync generator, since in the case os an async generator function:asyncio.iscoroutinefunctionto decide whether to use the async or the sync wrapper which doesn't detect async generator functions (theinspect.isasyncgenfunctiondoes however). So the sync wrapper is used, which would work ok (since async generator functions should not be awaited) exceptinspect.isgenerator), but even if that check was updated to detected the async generator it would still use the wrong implementation (the correct would be_wrap_async_generator_result).This means that:
<async generator>, andWe could use
inspect.asyncgenfunctionto detect this case at decoration time and use the async wrapper instead of the sync wrapper, but this would have the weird side effect that the decorated method would be an awaitable coroutine (in contrast to an async generator function which is not awaitable). When awaited this coroutine would returns an async generator function, which is undesirable.So it seems that for async generator functions the decorator could in fact continue to use the sync wrapper, but that the wrapper should be able to handle the case where the result of calling the function is an async generator. This is the approach of this PR.
demo
with this code and current version of langfuse
I see this

However with the code from this PR I see instead:

Important
Fixes async generator handling in
@observedecorator by usinginspect.isasyncgenfunctionand updating sync wrapper for async generator results.@observedecorator inlangfuse/_client/observe.py.inspect.isasyncgenfunctionto detect async generator functions.inspect.isasyncgen.asyncioimport fromlangfuse/_client/observe.py.test_async_generator_as_return_value().This description was created by
for 2e03b03. You can customize this summary. It will automatically update as commits are pushed.
Greptile Summary
Disclaimer: Experimental PR review
Fixes critical bug in
@observedecorator's async generator handling by improving detection and maintaining proper execution context.asyncio.iscoroutinefunctionwithinspectmodule functions for accurate async generator detection<async generator>test_async_generator_as_return_valuewhich confirms proper async generator behavior