-
Notifications
You must be signed in to change notification settings - Fork 3
Alternative proposal for an async interface for effectful #390
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
base: staging-llm
Are you sure you want to change the base?
Conversation
async def main():
with handler({foo: foo_handler}):
t = foo(bar(8))
# t is a term
print(t)
with handler({foo: foo_handler2, bar: bar_handler}):
res = evaluate(t)
print(res)
with handler({foo: foo_handler}):
t = foo(await bar_async(8))
# t is a term
print(t)
with handler({foo: foo_handler2, bar_async: bar_async_handler}):
res = await evaluate(t)
print(res)Behaves the same. |
|
Add tests interleaving coroutine forcing and handler installation |
async def main():
with handler({foo: foo_handler}):
t = foo(await bar_async(8))
# t is a term
print(t)
with handler({foo: foo_handler2, bar_async: bar_async_handler}):
res_async = evaluate(t)
with handler({foo: foo_handler3 }):
res = await res_async
print(res) |
|
Points in the design space to flesh out:
await as an explicit term. |
| kwargs_evaluated = {k: evaluate(v) for k, v in expr.kwargs.items()} | ||
|
|
||
| # Check if any evaluated args/kwargs are coroutines | ||
| has_coro = any(inspect.iscoroutine(a) for a in args_evaluated) or any( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to push this second traversal into a branch of the first traversal in evaluate? I think the data structure traversal logic in the rest of evaluate is necessary for this to be correct in general (what if you have a list of coroutines in an argument?), so we either have to duplicate it or reuse it.
|
|
||
|
|
||
| @defop | ||
| def await_[**P, T](op: Operation[P, T], *args: P.args, **kwargs: P.kwargs) -> T: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something I still think is confusing here is the type of async Operations. I'd expect await_ to take Operations that return coroutines and unbox them to return a value:
def await_[**P, T](
op: Operation[P, Coroutine[T]], *args, **kwargs
) -> T:
...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the correct signature or maybe we should have AsyncOperation.
If we go for the first option, then morally defop will have to modify the signature and wrap the return type in a coroutine (not sure if there's anything that specifically depends on defop maintaining the type but design wise it should be doing this)
if we go for the second option, then this might require more changes throughout the codebase if code relies on receiving an operation, and now we'd need to make it support either (probably no runtime changes, but design wise the signatures might need to be modified). AsyncOperation might be a subtype of Operation which might cut down on this.
| try: | ||
| from effectful.ops.semantics import await_ | ||
|
|
||
| if self._is_async: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to encapsulate this logic in a separate Operation or _BaseOperation subtype _AsyncOperation instead of carrying this flag around.
| # Return the term representation | ||
| return typing.cast( | ||
| Callable[Concatenate[Operation[Q, V], Q], Expr[V]], defdata | ||
| )(await_, self, *args, **kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this is the only place where await_ is introduced, but I'm not sure if that's right? That may need to happen in Operation.__call__ instead. We should clarify the invariants we're trying to maintain around await_ and async Operations.
is there some way of evaluating an await in the context of the parent caller. |
On the ongoing discussion regarding async support for effectful (#388), in particular for LLM-based interfaces, this PR implements a minor modification that allows async coroutines to flow through the usual effectful boilerplate. To an end client, the effectful API would end up looking as follows:
Pros:
Cons: