blob: 7a52723e4166d7ff966e65956d18bf5cb5d30b48 [file] [log] [blame]
=======
Actions
=======
.. _actions:
Actions do any heavy-lifting in a workflow. They should contain all complex compute. You can define actions
either through a class-based or function-based API. If actions implement `async def run` then will be run in an
asynchronous context (and thus require one of the async application functions).
Actions have two primary responsibilities:
1. ``run`` -- compute a result
2. ``update`` -- modify the state
The ``run`` method should return a dictionary of the result and the ``update`` method should return
the updated state. They declare their dependencies so the framework knows *which* state variables they read and write. This allows the
framework to optimize the execution of the workflow. We call (1) a ``Function`` and (2) a ``Reducer`` (similar to `Redux <https://redux.js.org/>`_, if you're familiar with frontend UI technology).
There are two APIs for defining actions: class-based and function-based. They are largely equivalent, but differ in use.
- use the function-based API when you want to write something quick and terse that reads from a fixed set of state variables
- use the class-based API when you want to leverage inheritance or parameterize the action in more powerful ways
-------------------
Class-based actions
-------------------
You can define an action by implementing the ``Action`` class:
.. code-block:: python
from burr.core import Action, State
class CustomAction(Action):
@property
def reads(self) -> list[str]:
return ["var_from_state"]
def run(self, state: State) -> dict:
return {"var_to_update": state["var_from_state"] + 1}
@property
def writes(self) -> list[str]:
return ["var_to_update"]
def update(self, result: dict, state: State) -> State:
return state.update(**result)
You then pass the action to the ``ApplicationBuilder``:
.. code-block:: python
from burr.core import ApplicationBuilder
app = ApplicationBuilder().with_actions(
custom_action=CustomAction()
)...
----------------------
Function-based actions
----------------------
You can also define actions by decorating a function with the `@action` decorator:
.. code-block:: python
from burr.core import action, State
@action(reads=["var_from_state"], writes=["var_to_update"])
def custom_action(state: State) -> Tuple[dict, State]:
result = {"var_to_update": state["var_from_state"] + 1}
return result, state.update(**result)
app = ApplicationBuilder().with_actions(
custom_action=custom_action
)...
Function-based actions can take in parameters which are akin to passing in constructor parameters. This is done through the `bind` method:
.. code-block:: python
@action(reads=["var_from_state"], writes=["var_to_update"])
def custom_action(state: State, increment_by: int) -> Tuple[dict, State]:
result = {"var_to_update": state["var_from_state"] + increment_by}
return result, state.update(**result)
app = ApplicationBuilder().with_actions(
custom_action=custom_action.bind(increment_by=2)
)...
This is the same as ``functools.partial``, but it is more explicit and easier to read.
Note that these combine the `reduce` and `run` methods into a single function, and they're both returned at the same time.
----------------------
Results
----------------------
If you just want to fill a result from the state, you can use the `Result` action:
.. code-block:: python
app = ApplicationBuilder().with_actions(
get_result=Result(["var_from_state"])
)...
This simply grabs the value from the state and returns it as the result. It is purely a placeholder
for an action that should just use the result, although you do not need it.
Refer to :ref:`actions <actions>` for documentation.