Skip to content

Commit

Permalink
Adds documentation for user-specified inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
elijahbenizzy committed Feb 17, 2024
1 parent aac35fa commit 58643a6
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 9 deletions.
83 changes: 76 additions & 7 deletions docs/concepts/actions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Actions
.. _actions:


Actions do any heavy-lifting in a workflow. They should contain all complex compute. You can define actions
Actions do the 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).

Expand All @@ -18,6 +18,17 @@ The ``run`` method should return a dictionary of the result and the ``update`` m
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).

.. _inputref:

--------------
Runtime inputs
--------------

Actions can declare inputs that are not part of the state. This is for the case that you want to pause workflow execution for human input.

For instance, say you have a chatbot. The first step will declare the ``input`` parameter ``prompt`` -- it will take that, process it, and put
it in the state. The subsequent steps will read the result of that from state.

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
Expand Down Expand Up @@ -58,6 +69,33 @@ You then pass the action to the ``ApplicationBuilder``:
custom_action=CustomAction()
)...
Note that if the action has inputs, you have to define the optional `inputs` property:

.. 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, increment_by: int) -> dict:
return {"var_to_update": state["var_from_state"] + increment_by}
@property
def writes(self) -> list[str]:
return ["var_to_update"]
def update(self, result: dict, state: State) -> State:
return state.update(**result)
@property
def inputs(self) -> list[str]:
return ["increment_by"]
----------------------
Function-based actions
----------------------
Expand Down Expand Up @@ -90,20 +128,51 @@ Function-based actions can take in parameters which are akin to passing in const
custom_action=custom_action.bind(increment_by=2)
)...
This is the same as ``functools.partial``, but it is more explicit and easier to read.
This is the same as ``functools.partial``, but it is more explicit and easier to read. If an action has parameters that are not
bound, they will be referred to as inputs. For example:

Note that these combine the `reduce` and `run` methods into a single function, and they're both returned at the same time.

----------------------
Results
----------------------
.. 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
)...
Will require the inputs to be passed in at runtime.

Note that these combine the ``reduce`` and ``run`` methods into a single function, and they're both returned at the same time.

-----------
``Inputs``
-----------

If you simply want a node to take in inputs and pass them to the state, you can use the `Input` action:

.. code-block:: python
app = ApplicationBuilder().with_actions(
get_input=Input("var_from_state")
)...
This will look for the `var_from_state` in the inputs and pass it to the state. Note this is just syntactic sugar
for declaring inputs through one of the other APIs and adding it to state -- if you want to do anything more complex
with the input, you should use other APIs.

-----------
``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"])
get_result=Result("var_from_state")
)...
Expand Down
28 changes: 26 additions & 2 deletions docs/concepts/state-machine.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ If you're in an async context, you can run `astep` instead:
action, result, state = await application.astep()
Step can also take in ``inputs`` as a dictionary, which will be passed to the action's run function as keyword arguments.
This is specifically meant for a "human in the loop" scenario, where the action needs to ask for input from a user. In this case,
the control flow is meant to be interrupted to allow for the user to provide input. See :ref:`inputs <inputref>` for more information.

.. code-block:: python
action, result, state = application.step(inputs={"prompt": input()})
``iterate``/``aiterate``
------------------------

Expand Down Expand Up @@ -80,17 +89,32 @@ See the function implementation of ``run`` to show how this is done.
In the async context, this does not return anything
(asynchronous generators are not allowed a return value).

.. note::
You can add inputs to ``iterate``/``aiterate`` by passing in a dictionary of inputs through the ``inputs`` parameter.
This will only apply to the first action. Actions that are not the first but require inputs are considered undefined behavior.


``run``/``arun``
----------------

Run just calls out to ``iterate`` and returns the final state.

The ``until`` variable is a ``or`` gate (E.G. ``any_complete``), although we will be adding an ``and`` gate (E.G. ``all_complete``),
and the ability to run until the state machine naturally executes (``until=None``).

.. code-block:: python
final_state, results = application.run(until=["final_action_1", "final_action_2"])
Currently the ``until`` variable is a ``or`` gate (E.G. ``any_complete``), although we will be adding an ``and`` gate (E.G. ``all_complete``),
and the ability to run until the state machine naturally executes (``until=None``).
In the async context, you can run ``arun``:

.. code-block:: python
final_state = await application.arun(until=["final_action_1", "final_action_2"])
.. note::
You can add inputs to ``run``/``arun`` in the same way as you can with ``iterate`` -- it will only apply to the first action.

----------
Inspection
Expand Down
6 changes: 6 additions & 0 deletions docs/reference/actions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ Actions

.. automethod:: __init__


.. autoclass:: burr.core.action.Input
:members:

.. automethod:: __init__

.. autodecorator:: burr.core.action.action
.. autofunction:: burr.core.action.bind

Expand Down

0 comments on commit 58643a6

Please sign in to comment.