-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 2af7149
Showing
166 changed files
with
26,247 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Sphinx build info version 1 | ||
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. | ||
config: 2b5d932f18ded5d046f54cb38626cd9e | ||
tags: d77d1c0d9ca2f4c8421862c7c5a0d620 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
burr.dagworks.io |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
======= | ||
Actions | ||
======= | ||
|
||
.. _actions: | ||
|
||
.. note:: | ||
|
||
Actions are the core building block of Burr. They read from state and write to state. | ||
They can be synchronous and asynchonous, and have both a ``sync`` and ``async`` API. | ||
There are both function and class-based APIs. | ||
|
||
|
||
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). | ||
|
||
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 | ||
|
||
---------------------- | ||
Function-based actions | ||
---------------------- | ||
|
||
You can also define actions by decorating a function with the :py:func:`@action <burr.core.action.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 :py:meth:`bind <burr.core.action.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. If an action has parameters that are not | ||
bound, they will be referred to as inputs. For example: | ||
|
||
|
||
.. 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. See below for how to do that. | ||
|
||
Note that these combine the ``update`` and ``run`` methods into a single function, and they're both executed at the same time. | ||
|
||
|
||
------------------- | ||
Class-Based Actions | ||
------------------- | ||
|
||
You can define an action by implementing the :py:class:`Action <burr.core.action.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 :py:class:`ApplicationBuilder <burr.core.application.ApplicationBuilder>`: | ||
|
||
.. code-block:: python | ||
from burr.core import ApplicationBuilder | ||
app = ApplicationBuilder().with_actions( | ||
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"] | ||
See below for how to pass in inputs at runtime. | ||
|
||
----------------------- | ||
``Inputs`` only actions | ||
----------------------- | ||
|
||
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`` only actions | ||
------------------------ | ||
|
||
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. | ||
|
||
|
||
.. _inputref: | ||
|
||
-------------- | ||
Runtime Inputs | ||
-------------- | ||
|
||
Actions can declare parameters that are not part of the state. Use this to: | ||
|
||
1. Provide variables that can be bound to an action. E.g. API clients, DB clients, etc. | ||
2. Provide inputs that are required as part of the application to function, e.g. human input, configuration, etc. | ||
|
||
For example using the function based API, consider the following action: | ||
|
||
.. code-block:: python | ||
@action(reads=["..."], writes=["..."]) | ||
def my_action(state: State, client: Client, prompt: str) -> Tuple[dict, State]: | ||
"""client & `prompt` here are something we need to pass in.""" | ||
context = client.get_data(state["..."]) | ||
result = llm_call(prompt, context) # some LLM call... | ||
return result, state.update(**result) | ||
We need to pass in `client` and `prompt` somehow. Here are the ways to do that: | ||
|
||
.. code-block:: python | ||
# (1) bind values | ||
app = ( | ||
ApplicationBuilder() | ||
# we can "bind" values to an action | ||
.with_actions(my_action=my_action.bind(client=client)) | ||
... | ||
.build() | ||
) | ||
# (2) pass them in at runtime | ||
app.run( # or app.step, app.iterate, app.astep, etc.\n" | ||
halt_..., # your halt logic\n" | ||
inputs={"prompt": "this will be passed into `prompt`"} # <-- we pass in values here | ||
) | ||
For instance, say you have a chatbot. The first step will likely declare the ``input`` parameter ``prompt`` -- | ||
it will take that, process it, and put the result in state. The subsequent steps will read the result of that from state. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
===================== | ||
Additional Visibility | ||
===================== | ||
|
||
.. note:: | ||
|
||
Burr comes with the ability to see inside your actions. This is a very pluggable framework | ||
that comes with the default tracking client, but can also be hooked up to tools such as `OpenTelemetry <https://opentelemetry.io/>`_ | ||
|
||
------- | ||
Tracing | ||
------- | ||
|
||
Burr comes with a tracing capability to see recursive spans inside an action. This is similar to | ||
the `OpenTelemetry <https://opentelemetry.io/>`_ sdk, although it is simplified significantly. | ||
|
||
To add the tracing capability, the action first has to declare a ``__tracer`` input. This is a | ||
an object that instantiates spans and tracks state. | ||
|
||
Then, using the `__tracer` as a callable, you can instantiate spans and track state. | ||
|
||
For the function-based API, this would look as follows: | ||
|
||
.. code-block:: python | ||
from burr.visibility import TracingFactory | ||
from burr.core import action | ||
@action(reads=['input_var'], writes=['output_var']) | ||
def my_action(state: State, __tracer: TracingFactory) -> Tuple[dict, State]: | ||
with __tracer('process_data'): | ||
initial_data = _process_data(state['input_var']) | ||
with __tracer('validate_data'): | ||
_validate(initial_data) | ||
with __tracer('transform_data', dependencies=['process_data']): | ||
transformed_data = _transform(initial_data) | ||
return {'output_var': transformed_data}, state.update({'output_var': transformed_data}) | ||
This would create the following traces: | ||
|
||
#. ``process_data`` | ||
#. ``validate_data`` as a child of ``process_data`` | ||
#. ``transform_data`` as a causal dependent of ``process_data`` | ||
|
||
Dependencies are used to express [dag](-style structures of spans within actions. This is useful for gaining visibility into the internal structure | ||
of an action, but is likely best used with integrations with micro-orchestration systems for implementating actions, such as Hamilton or Lanchain. | ||
This maps to the `span link <https://opentelemetry.io/docs/concepts/signals/traces/#span-links>`_ concept in OpenTelemetry. | ||
|
||
Note that, on the surface, this doesn't actually *do* anything. It has to be paired with the appropriate hooks. | ||
These just function as callbacks (on enter/exit). The :py:class:`LocalTrackingClient <burr.tracking.LocalTrackingClient>`, used by the | ||
:ref:`tracking <tracking>` feature forms one of these hooks, but we will be adding more, including: | ||
|
||
1. An OpenTelemetry client | ||
2. A DataDog client | ||
|
||
.. note:: | ||
|
||
The class-based API can leverage this by declaring ``inputs`` as ``__tracer`` and then using the ``__tracer`` inside the ``run`` method. | ||
|
||
------------ | ||
Observations | ||
------------ | ||
|
||
(This is a work in progress, and is not complete) | ||
|
||
You can make observations on the state by calling out to the `log_artifact` method on the `__tracer` context manager. | ||
For instance: | ||
|
||
.. code-block:: python | ||
from burr.visibility import TracingFactory, ArtifactLogger | ||
from burr.core import action | ||
@action(reads=['input_var'], writes=['output_var']) | ||
def my_action( | ||
state: State, | ||
__tracer: TracingFactory, | ||
__logger: ArtifactLogger | ||
) -> Tuple[dict, State]: | ||
with __tracer('process_data'): | ||
initial_data = _process_data(state['input_var']) | ||
with __tracer('validate_data'): | ||
validation_results = _validate(initial_data) | ||
t.log_artifact(validation_results=validation_results) | ||
with __tracer('transform_data', dependencies=['process_data']) | ||
transformed_data = _transform(initial_data) | ||
__logger.log_artifact(transformed_data_size=len(transformed_data)) | ||
return {'output_var': transformed_data}, state.update({'output_var': transformed_data}) | ||
The output can be any "json-dumpable" object (or pydantic model). This will be stored along with the span and can be used for debugging or analysis. | ||
|
||
You can read more in the :ref:`reference documentation <visibility>`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
===== | ||
Hooks | ||
===== | ||
|
||
.. _hooks: | ||
.. note:: | ||
|
||
Hooks allow you to customize every aspect of Burr's execution, plugging in whatever tooling, | ||
observability framework, or debugging tool you need. | ||
|
||
Burr has a system of lifecycle adapters (adapted from the similar `Hamilton <https://github.com/dagworks-inc/hamilton>`_ concept), which allow you to run tooling before and after | ||
various places in a node's execution. For instance, you could: | ||
|
||
1. Log every step as a trace in datadog | ||
2. Add a time-delay to your steps to allow for rendering | ||
3. Add a print statement to every step to see what happened (E.G. implement the printline in cowsay above) | ||
4. Synchronize state/updates to an external database | ||
5. Put results on a queue to feed to some monitoring system | ||
|
||
Note some of the above are yet to be implemented. | ||
|
||
To implement hooks, you subclass any number of the :ref:`available lifecycle hooks <hooksref>`. | ||
These have synchronous and asynchronous versions, and your hook can subclass as many as you want | ||
(as long as it doesn't do both the synchronous and asynchronous versions of the same hook). | ||
|
||
To use them, you pass them into the :py:class:`ApplicationBuilder <burr.core.application.ApplicationBuilder>` as a ``*args`` list of hooks. For instance, | ||
a hook that prints out the nodes name during execution looks like this. | ||
We implement the pre/post run step hooks. | ||
|
||
.. code-block:: python | ||
class PrintLnHook(PostRunStepHook, PreRunStepHook): | ||
def pre_run_step(self, *, state: "State", action: "Action", **future_kwargs: Any): | ||
print(f"Starting action: {action.node.name}") | ||
def post_run_step( | ||
self, | ||
*, | ||
state: "State", | ||
action: "Action", | ||
result: Optional[dict], | ||
sequence_id: int, | ||
exception: Exception, | ||
**future_kwargs: Any, | ||
): | ||
print(f"Finishing action: {action.node.name}") | ||
To include this in the application, you pass it into the :py:meth:`with_hooks <burr.core.application.ApplicationBuilder.with_hooks>` method. | ||
|
||
.. code-block:: python | ||
app = ( | ||
ApplicationBuilder() | ||
.with_hooks(PrintLnHook()) | ||
... | ||
.build()) | ||
.. note:: | ||
|
||
There are synchronous and asynchronous hooks. Synchronous hooks will be called with both synchronous and asynchronous run methods | ||
(all of ``step``, ``astep``, ``iterate``, ``aiterate``, ``run``, and ``arun``), whereas synchronous hooks will only be called with | ||
the asynchronous methods (``astep``, ``aiterate``, ``arun``). | ||
|
||
.. warning:: | ||
Hook order is currently undefined -- they happen to be called now in the order in which they are defined. We will likely | ||
alter them to be called in the order they are defined (for `pre...`) hooks and in reverse order for `post...` hooks, | ||
but this is not yet implemented. | ||
|
||
Read more about the hook API in the :ref:`hooks section <hooksref>`. |
Oops, something went wrong.