From b5934317b1071961840d3c40446d32f2b9c0978b Mon Sep 17 00:00:00 2001 From: elijahbenizzy Date: Thu, 8 Feb 2024 14:37:59 -0800 Subject: [PATCH] Adds full documentation on sphinx See docs/README-internal.md --- .github/workflows/docs.yml | 28 ++++ .gitignore | 2 + README.md | 19 +-- burr/core/action.py | 98 +++++++++++--- burr/core/application.py | 14 +- burr/core/state.py | 2 +- burr/integrations/hamilton.py | 1 + burr/lifecycle/base.py | 36 +++++ burr/lifecycle/default.py | 18 ++- docs/Makefile | 20 +++ docs/README-internal.md | 11 ++ docs/_static/counter.png | Bin 0 -> 7727 bytes demo_graph.png => docs/_static/demo_graph.png | Bin docs/concepts/actions.rst | 108 +++++++++++++++ docs/concepts/hooks.rst | 51 +++++++ docs/concepts/index.rst | 17 +++ docs/concepts/more.rst | 127 ++++++++++++++++++ docs/concepts/state-machine.rst | 91 +++++++++++++ docs/concepts/state.rst | 55 ++++++++ docs/concepts/transitions.rst | 35 +++++ docs/conf.py | 45 +++++++ docs/examples/chatbot.rst | 5 + docs/examples/index.rst | 16 +++ docs/examples/ml_training.rst | 5 + docs/examples/simple.rst | 7 + docs/examples/simulation.rst | 5 + docs/getting_started/index.rst | 15 +++ docs/getting_started/install.rst | 24 ++++ docs/getting_started/simple-example.rst | 100 ++++++++++++++ docs/getting_started/up-next.rst | 12 ++ docs/getting_started/why-burr.rst | 48 +++++++ docs/index.rst | 23 ++++ docs/main.rst | 16 +++ docs/make.bat | 35 +++++ docs/reference/actions.rst | 27 ++++ docs/reference/application.rst | 15 +++ docs/reference/conditions.rst | 13 ++ docs/reference/index.rst | 19 +++ docs/reference/integrations/hamilton.rst | 15 +++ docs/reference/integrations/index.rst | 11 ++ docs/reference/integrations/streamlit.rst | 31 +++++ docs/reference/lifecycle.rst | 30 +++++ docs/reference/state.rst | 10 ++ pyproject.toml | 8 ++ 44 files changed, 1236 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/Makefile create mode 100644 docs/README-internal.md create mode 100644 docs/_static/counter.png rename demo_graph.png => docs/_static/demo_graph.png (100%) create mode 100644 docs/concepts/actions.rst create mode 100644 docs/concepts/hooks.rst create mode 100644 docs/concepts/index.rst create mode 100644 docs/concepts/more.rst create mode 100644 docs/concepts/state-machine.rst create mode 100644 docs/concepts/state.rst create mode 100644 docs/concepts/transitions.rst create mode 100644 docs/conf.py create mode 100644 docs/examples/chatbot.rst create mode 100644 docs/examples/index.rst create mode 100644 docs/examples/ml_training.rst create mode 100644 docs/examples/simple.rst create mode 100644 docs/examples/simulation.rst create mode 100644 docs/getting_started/index.rst create mode 100644 docs/getting_started/install.rst create mode 100644 docs/getting_started/simple-example.rst create mode 100644 docs/getting_started/up-next.rst create mode 100644 docs/getting_started/why-burr.rst create mode 100644 docs/index.rst create mode 100644 docs/main.rst create mode 100644 docs/make.bat create mode 100644 docs/reference/actions.rst create mode 100644 docs/reference/application.rst create mode 100644 docs/reference/conditions.rst create mode 100644 docs/reference/index.rst create mode 100644 docs/reference/integrations/hamilton.rst create mode 100644 docs/reference/integrations/index.rst create mode 100644 docs/reference/integrations/streamlit.rst create mode 100644 docs/reference/lifecycle.rst create mode 100644 docs/reference/state.rst diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..62dab408 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,28 @@ +name: documentation + +on: [push] + +permissions: + contents: write + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - name: Install dependencies + run: | + pip install -e ".[documentation]" + - name: Sphinx build + run: | + sphinx-build docs _build + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 +# if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/initial-prototypes' }} + with: + publish_branch: gh-pages + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: _build/ + force_orphan: true diff --git a/.gitignore b/.gitignore index 68bc17f9..6c016ebb 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,5 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +# +_build diff --git a/README.md b/README.md index ac6ef190..cbbaed50 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,6 @@ Let us take a look at of how one might design a gpt-like chatbot. It: 6. If this succeeds, we present the response to the user 7. Await a new prompt, GOTO (1) -Simple, right? Until you start tracking how state flows through: -- The prompt is referenced by multiple future steps -- The chat history is referred to at multiple points, and appended to (at (1) and (5)) -- The decision on mode is opaque, and referred to both by (4), to know what model to query and by (6) to know how to render the response -- You will likely want to add more capabilities, more retries, etc... - -Chatbots, while simple at first glance, turn into something of a beast when you want to bring them to production and understand exactly *why* -they make the decisions they do. We can model this as a _State Machine_, using the following two concepts: 1. `Actions` -- a function that has two jobs. These form Nodes. @@ -36,6 +28,17 @@ We can model this as a _State Machine_, using the following two concepts: The set of these together form what we will call a `Application` (effectively) a graph. +Why do we need all this abstraction? + +This is all simple until you start tracking how state flows through: +- The prompt is referenced by multiple future steps +- The chat history is referred to at multiple points, and appended to (at (1) and (5)) +- The decision on mode is opaque, and referred to both by (4), to know what model to query and by (6) to know how to render the response +- You will likely want to add more capabilities, more retries, etc... + +Chatbots, while simple at first glance, turn into something of a beast when you want to bring them to production and understand exactly *why* +they make the decisions they do. + For those into the CS details, this is reverse to how a state machine is usually represented (edges are normally actions and nodes are normally state). We've found this the easiest way to express computation as simple python functions. diff --git a/burr/core/action.py b/burr/core/action.py index 36b31eb0..51f35ef6 100644 --- a/burr/core/action.py +++ b/burr/core/action.py @@ -9,23 +9,45 @@ class Function(abc.ABC): + """Interface to represent the 'computing' part of an action""" + @property @abc.abstractmethod def reads(self) -> list[str]: + """Returns the keys from the state that this function reads + + :return: A list of keys + """ pass @abc.abstractmethod def run(self, state: State) -> dict: - pass + """Runs the function on the given state and returns the result. + The result is jsut a key/value dictionary. - def is_async(self): + :param state: State to run the function on + :return: Result of the function + """ + + def is_async(self) -> bool: + """Convenience method to check if the function is async or not. + This can be used by the application to run it. + + :return: True if the function is async, False otherwise + """ return inspect.iscoroutinefunction(self.run) class Reducer(abc.ABC): + """Interface to represent the 'updating' part of an action""" + @property @abc.abstractmethod def writes(self) -> list[str]: + """Returns the keys from the state that this reducer writes + + :return: A list of keys + """ pass @abc.abstractmethod @@ -35,15 +57,8 @@ def update(self, result: dict, state: State) -> State: class Action(Function, Reducer, abc.ABC): def __init__(self): - """Represents an action in a state machine. This is the base class from which: - - 1. Custom actions - 2. Conditions - 3. Results - - All extend this class. Note that name is optional so that APIs can set the - name on these actions as part of instantiation. - When they're used, they must have a name set. + """Represents an action in a state machine. This is the base class from which + actions extend. Note that this class needs to have a name set after the fact. """ self._name = None @@ -72,7 +87,7 @@ def with_name(self, name: str) -> "Action": @property def name(self) -> str: """Gives the name of this action. This should be unique - across your agent.""" + across your application.""" return self._name def __repr__(self): @@ -85,13 +100,28 @@ class Condition(Function): KEY = "PROCEED" def __init__(self, keys: List[str], resolver: Callable[[State], bool], name: str = None): + """Base condition class. Chooses keys to read from the state and a resolver function. + + :param keys: Keys to read from the state + :param resolver: Function to resolve the condition to True or False + :param name: Name of the condition + """ self._resolver = resolver self._keys = keys self._name = name @staticmethod def expr(expr: str) -> "Condition": - """Returns a condition that evaluates the given expression""" + """Returns a condition that evaluates the given expression. Expression must use + only state variables and Python operators. Do not trust that anything else will work. + + Do not accept expressions generated from user-inputted text, this has the potential to be unsafe. + + You can also refer to this as ``from burr.core import expr`` in the API. + + :param expr: Expression to evaluate + :return: A condition that evaluates the given expression + """ tree = ast.parse(expr, mode="eval") # Visitor class to collect variable names @@ -124,7 +154,13 @@ def reads(self) -> list[str]: @classmethod def when(cls, **kwargs): """Returns a condition that checks if the given keys are in the - state and equal to the given values.""" + state and equal to the given values. + + You can also refer to this as ``from burr.core import when`` in the API. + + :param kwargs: Keyword arguments of keys and values to check -- will be an AND condition + :return: A condition that checks if the given keys are in the state and equal to the given values + """ keys = list(kwargs.keys()) def condition_func(state: State) -> bool: @@ -136,18 +172,34 @@ def condition_func(state: State) -> bool: name = f"{', '.join(f'{key}={value}' for key, value in sorted(kwargs.items()))}" return Condition(keys, condition_func, name=name) + @classmethod + @property + def default(self) -> "Condition": + """Returns a default condition that always resolves to True. + You can also refer to this as ``from burr.core import default`` in the API. + + :return: A default condition that always resolves to True + """ + return Condition([], lambda _: True, name="default") + @property def name(self) -> str: return self._name -default = Condition([], lambda _: True, name="default") +default = Condition.default when = Condition.when expr = Condition.expr class Result(Action): def __init__(self, fields: list[str]): + """Represents a result action. This is purely a convenience class to + pull data from state and give it out to the result. It does nothing to + the state itself. + + :param fields: Fields to pull from the state + """ super(Result, self).__init__() self._fields = fields @@ -272,6 +324,22 @@ def bind(self, **kwargs: Any): def bind(self: FunctionRepresentingAction, **kwargs: Any) -> FunctionRepresentingAction: + """Binds an action to the given parameters. This is functionally equivalent to + functools.partial, but is more explicit and is meant to be used in the API. This only works with + the functional API for ``@action`` and not with the class-based API. + + .. code-block:: python + + @action(["x"], ["y"]) + def my_action(state: State, z: int) -> Tuple[dict, State]: + return {"y": state.get("x") + z}, state + + my_action.bind(z=2) + + :param self: The decorated function + :param kwargs: The keyword arguments to bind + :return: The decorated function with the given parameters bound + """ self.action_function = self.action_function.with_params(**kwargs) return self diff --git a/burr/core/application.py b/burr/core/application.py index db0eb857..ae3539b9 100644 --- a/burr/core/application.py +++ b/burr/core/application.py @@ -112,7 +112,7 @@ def step(self) -> Optional[Tuple[Action, dict, State]]: Use this if you just want to do something with the state and not rely on generators. E.G. press forward/backwards, hnuman in the loop, etc... Odds are this is not - the method you want -- you'll want iterate() (if you want to see the state/ + the method you want -- you'll want iterate() (if you want to see the state/ results along the way), or run() (if you just want the final state/results). :return: Tuple[Function, dict, State] -- the function that was just ran, the result of running it, and the new state @@ -439,13 +439,13 @@ def _validate_actions(actions: Optional[List[Action]]): raise ValueError("Must have at least one action in the application!") -@dataclasses.dataclass class ApplicationBuilder: - state: State = dataclasses.field(default_factory=State) - transitions: List[Tuple[str, str, Condition]] = None - actions: List[Action] = None - start: str = None - lifecycle_adapters: List[LifecycleAdapter] = dataclasses.field(default_factory=list) + def __init__(self): + self.state: State = State() + self.transitions: Optional[List[Tuple[str, str, Condition]]] = None + self.actions: Optional[List[Action]] = None + self.start: Optional[str] = None + self.lifecycle_adapters: List[LifecycleAdapter] = list() def with_state(self, **kwargs) -> "ApplicationBuilder": if self.state is not None: diff --git a/burr/core/state.py b/burr/core/state.py index a311887b..ec9ecdc2 100644 --- a/burr/core/state.py +++ b/burr/core/state.py @@ -127,7 +127,7 @@ class State(Mapping): 2. Pulling/pushing to external places 3. Simultaneous writes/reads in the case of parallelism 4. Schema enforcement -- how to specify/manage? Should this be a - dataclass when implemented? + dataclass when implemented? """ def __init__(self, initial_values: Dict[str, Any] = None): diff --git a/burr/integrations/hamilton.py b/burr/integrations/hamilton.py index 78c9527b..51c10080 100644 --- a/burr/integrations/hamilton.py +++ b/burr/integrations/hamilton.py @@ -208,6 +208,7 @@ def writes(self) -> list[str]: return [source.key for source in self._outputs.values()] def visualize_step(self, **kwargs): + """Visualizes execution for a Hamilton step""" dr = self._driver inputs = {key: ... for key in self._inputs} overrides = inputs diff --git a/burr/lifecycle/base.py b/burr/lifecycle/base.py index a392b5c6..390d86ab 100644 --- a/burr/lifecycle/base.py +++ b/burr/lifecycle/base.py @@ -10,20 +10,38 @@ @lifecycle.base_hook("pre_run_step") class PreRunStepHook(abc.ABC): + """Hook that runs before a step is executed""" + @abc.abstractmethod def pre_run_step(self, *, state: "State", action: "Action", **future_kwargs: Any): + """Run before a step is executed. + + :param state: State prior to step execution + :param action: Action to be executed + :param future_kwargs: Future keyword arguments + """ pass @lifecycle.base_hook("pre_run_step") class PreRunStepHookAsync(abc.ABC): + """Async hook that runs before a step is executed""" + @abc.abstractmethod async def pre_run_step(self, *, state: "State", action: "Action", **future_kwargs: Any): + """Async run before a step is executed. + + :param state: State prior to step execution + :param action: Action to be executed + :param future_kwargs: Future keyword arguments + """ pass @lifecycle.base_hook("post_run_step") class PostRunStepHook(abc.ABC): + """Hook that runs after a step is executed""" + @abc.abstractmethod def post_run_step( self, @@ -34,11 +52,21 @@ def post_run_step( exception: Exception, **future_kwargs: Any, ): + """Run after a step is executed. + + :param state: State after step execution + :param action: Action that was executed + :param result: Result of the action + :param exception: Exception that was raised + :param future_kwargs: Future keyword arguments + """ pass @lifecycle.base_hook("post_run_step") class PostRunStepHookAsync(abc.ABC): + """Async hook that runs after a step is executed""" + @abc.abstractmethod async def post_run_step( self, @@ -49,6 +77,14 @@ async def post_run_step( exception: Exception, **future_kwargs: Any, ): + """Async run after a step is executed + + :param state: State after step execution + :param action: Action that was executed + :param result: Result of the action + :param exception: Exception that was raised + :param future_kwargs: Future keyword arguments + """ pass diff --git a/burr/lifecycle/default.py b/burr/lifecycle/default.py index e748bd72..0ed38161 100644 --- a/burr/lifecycle/default.py +++ b/burr/lifecycle/default.py @@ -14,6 +14,8 @@ def safe_json(obj: Any) -> str: class StateAndResultsFullLogger(PostRunStepHook, PreRunStepHook): + """Logs the state and results of the action in a jsonl file.""" + DONT_INCLUDE = object() # sentinel value def __init__( @@ -22,12 +24,19 @@ def __init__( mode: Literal["append", "w"] = "append", json_dump: Callable[[dict], str] = safe_json, ): + """Initializes the logger. + + :param jsonl_path: Path to the jsonl file + :param mode: Mode to open the file in. Either "append" or "w" + :param json_dump: Function to use to dump the json. Default is safe_json + """ if not jsonl_path.endswith(".jsonl"): raise ValueError(f"jsonl_path must end with .jsonl. Got: {jsonl_path}") self.jsonl_path = jsonl_path open_mode = "a" if mode == "append" else "w" self.f = open(jsonl_path, mode=open_mode) # open in append mode self.tracker = [] # tracker to keep track of timing/whatnot + self.json_dump = json_dump def pre_run_step(self, **future_kwargs: Any): self.tracker.append({"time": datetime.datetime.now()}) @@ -49,7 +58,7 @@ def post_run_step( "start_time": self.tracker[-1]["time"].isoformat(), "end_time": datetime.datetime.now().isoformat(), } - self.f.writelines([safe_json(state_and_result) + "\n"]) + self.f.writelines([self.json_dump(state_and_result) + "\n"]) def __del__(self): if hasattr(self, "f"): @@ -58,7 +67,14 @@ def __del__(self): class SlowDownHook(PostRunStepHook, PreRunStepHook): + """Slows down execution. You'll only want to use this for debugging/visualizing.""" + def __init__(self, pre_sleep_time: float = 0.5, post_sleep_time: float = 0.5): + """Initializes the hook. + + :param pre_sleep_time: Time to sleep before the step + :param post_sleep_time: Time to sleep after the step + """ self.post_sleep_time = post_sleep_time self.pre_sleep_time = pre_sleep_time diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README-internal.md b/docs/README-internal.md new file mode 100644 index 00000000..8f4334fc --- /dev/null +++ b/docs/README-internal.md @@ -0,0 +1,11 @@ +# Internal README for docs + +This documentation aims to follow the [diataxis](diataxis.fr) approach to documentation. This outlines: +1. Tutorials [getting_started](./getting_started) +2. How-to guides (examples in the repo) +3. References [reference](./reference) +4. Explanation [concepts](./concepts) + +TODO: +- [ ] fill in all docs todos +- [ ] Add examples for Hamilton integration, streamlit integration diff --git a/docs/_static/counter.png b/docs/_static/counter.png new file mode 100644 index 0000000000000000000000000000000000000000..db6bc637fb201655cd47fe5b74c68871f5fec4c7 GIT binary patch literal 7727 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 literal 0 HcmV?d00001 diff --git a/demo_graph.png b/docs/_static/demo_graph.png similarity index 100% rename from demo_graph.png rename to docs/_static/demo_graph.png diff --git a/docs/concepts/actions.rst b/docs/concepts/actions.rst new file mode 100644 index 00000000..170a655c --- /dev/null +++ b/docs/concepts/actions.rst @@ -0,0 +1,108 @@ +======= +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. Compute a result from the state +2. Update the state with the result + +We call (1) a ``Function`` and (2) a ``Reducer`` (similar to Redux). The ``run`` method is the function and the ``update`` +method is the reducer. 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. + +In the case of a function-based action, the function returns both at the same time. + +------------------- +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. + +---------------------- +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 ` for documentation. diff --git a/docs/concepts/hooks.rst b/docs/concepts/hooks.rst new file mode 100644 index 00000000..5f47ed96 --- /dev/null +++ b/docs/concepts/hooks.rst @@ -0,0 +1,51 @@ +===== +Hooks +===== + +.. _hooks: + +Burr has a system of lifecycle adapters (adapted from [Hamilton's](https://github.com/dagworks-inc/hamilton) similar concept, which allow you to run tooling before and after +various places in a node's execution. For instance, you could (many of these are yet to be implemented): + +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 + +To implement hooks, you subclass any number of the :ref:`available lifecycle hooks `. +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 `ApplicationBuilder` as a 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], + exception: Exception, + **future_kwargs: Any, + ): + print(f"Finishing action: {action.node.name}") + +To include this in the application, you would pass it in as a list of hooks: + +.. code-block:: python + + app = ( + ApplicationBuilder() + .with_hooks(PrintLnHook()) + ... + .build()) + +Read more about the hook API in the :ref:`hooks section `. diff --git a/docs/concepts/index.rst b/docs/concepts/index.rst new file mode 100644 index 00000000..2b5f0c42 --- /dev/null +++ b/docs/concepts/index.rst @@ -0,0 +1,17 @@ +==================== +Concepts +==================== + +Overview of the concepts -- read these to get a mental model for how Burr works. + +.. _concepts: + +.. toctree:: + :maxdepth: 2 + + state-machine + state + actions + transitions + hooks + more diff --git a/docs/concepts/more.rst b/docs/concepts/more.rst new file mode 100644 index 00000000..0a82fc69 --- /dev/null +++ b/docs/concepts/more.rst @@ -0,0 +1,127 @@ +==================== +Planned capabilities +==================== + +These are on the roadmap (and will be part of Burr in the imminent future), but have not been built yet. + +We build fast though, so let us know which ones you need and they'll be in there before you know it! + +----------- +Typed State +----------- + +We plan to add the ability to type-check state with some (or all) of the following: + +- Pydantic +- dataclasses +- TypedDict +- Custom state schemas (through the ``reads``/``writes`` parameters) + +The idea is you would define state at the function level, parameterized by the state type, and Burr would be able to validate +against that state. + +.. code-block:: python + + class InputState(TypedDict): + foo: int + bar: str + + class OutputState(TypedDict): + baz: int + qux: str + + @action(reads=["foo", "bar"], writes=["baz"]) + def my_action(state: State[InputState]) -> Tuple[dict, State[OutputState]]: + result = {"baz": state["foo"] + 1, "qux": state["bar"] + "!"} + return result, state.update(**result) + +The above could also be dataclasses/pydantic models. We could also add something as simple as: + +.. code-block:: python + + @action(reads={"foo": int, "bar": str}, writes={"baz": int, "qux": str}) + ... + +----------------------------- +State Management/Immutability +----------------------------- + +We plan the ability to manage state in a few ways: +1. ``commit`` -- an internal tool to commit/compile a series of changes so that we have the latest state evaluated +2. ``persist`` -- a user-facing API to persist state to a database. This will be pluggable by the user, and we will have a few built-in options (e.g. a simple in-memory store, a file store, a database store, etc...) +3. ``hydrate`` -- a static method to hydrate state from a database. This will be pluggable by the user, and we will have a few built-in options that mirror those in ``persist`` options. + +Currently state is immutable, but it utilizes an inefficient copy mechanism. This is out of expedience -- we don't anticipate this will +be painful for the time being, but plan to build a more efficient functional paradigm. We will likely have: + +1. Each state object be a node in a linked list, with a pointer to the previous state. It carries a diff of the changes from the previous state. +2. An ability to ``checkpoint`` (allowing for state garbage collection), and store state in memory/kill out the pointers. + +We will also consider having the ability to have a state solely backed by redis (and not memory), but we are still thinking through the API. + +---------------------- +Compilation/Validation +---------------------- + +We currently do not validate that the chain of actions provide a valid state, although we plan to walk the graph to ensure that no "impossible" +situation is reached. E.G. if an action reads from a state that is not written to (or not initialized), we will raise an error, likely upon calling `validate`. +We may be changing the behavior with defaults over time. + +-------------------- +Exception Management +-------------------- + +Currently, exceptions will break the control flow of an action, stopping the program early. Thus, +if an exception is expected, the program will stop early. We will be adding the ability to conditionally transition based +on exceptions, which will allow you to transition to an error-handling (or retry) action that does not +need the full outputs of the prior action. + +Here is what it would look liek in the current API: + +.. code-block:: python + + @action(reads=["attempts"], writes=["output", "attempts"]) + def some_flaky_action(state: State, max_retries: int=3) -> Tuple[dict, State]: + result = {"output": None, "attempts": state["attempts"] + 1} + try: + result["output"] = call_some_api(...) + excecpt APIException as e: + if state["attempts"] >= max_retries: + raise e + return result, state.update(**result) + +One could imagine adding it as a condition (a few possibilities) + +.. code-block:: python + + @action(reads=[], writes=["output"]) + def some_flaky_action(state: State) -> Tuple[dict, State]: + result = {"output": call_some_api(...)} + return result, state.update(**result) + + builder.with_actions( + some_flaky_action=some_flaky_action + ).with_transitions( + ( + "some_flaky_action", + "some_flaky_action", + error(APIException) # infinite retries + error(APIException, max=3) # 3 visits to this edge then it gets reset if this is not chosen + # That's stored in state + ) + +Will have to come up with ergonomic APIs -- the above are just some ideas. + +----------------- +Streaming results +----------------- + +Results should be able to stream in, but we'll want to store the final output in state. + +Still thinking through the UX. + +------------ +Integrations +------------ + +Langchain is next up (using LCEL). Please request any other integrations you'd like to see. diff --git a/docs/concepts/state-machine.rst b/docs/concepts/state-machine.rst new file mode 100644 index 00000000..87b4c636 --- /dev/null +++ b/docs/concepts/state-machine.rst @@ -0,0 +1,91 @@ +==================== +Applications +==================== + +.. _applications: + +Applications form the core representation of the state machine. You build them with the ``ApplicationBuilder``. + +The ``ApplicationBuilder`` is a class that helps you build an application. Here is the minimum that is required: +1. A ``**kwargs`` of actions passed to ``with_actions(...)`` +2. Any relevant transitions (with conditions) +3. An entry point + +This is shown in the example from :ref:`getting started ` + +.. code-block:: python + + from burr.core import ApplicationBuilder, default, expr + app = ( + ApplicationBuilder() + .with_state(counter=0) # initialize the count to zero + .with_actions( + count=count, # add the counter action with the name "counter" + done=done # add the printer action with the name "printer" + ).with_transitions( + ("count", "count", expr("counter < 10")), # Keep counting if the counter is less than 10 + ("count", "done", default) # Otherwise, we're done + ).with_entrypoint("counter") # we have to start somewhere + .build() + ) + + +------- +Running +------- + +There are three APIs for executing an application. + +``step``/``astep`` +------------------ + +Returns the tuple of the action, the result of that action, and the new state. Call this if you want to run the application step-by-step. +.. code-block:: python + + action, result, state = application.step() + +If you're in an async context, you can run `astep` instead: + +.. code-block:: python + + action, result, state = await application.astep() + +``iterate``/``aiterate`` +------------------------ + +Iterate just runs ``step`` in a row, functioning as a generator: + +.. code-block:: python + + for action, result, state in application.iterate(until=["final_action_1", "final_action_2"]): + print(action.name, result) + +You can also run ``aiterate`` in an async context: + +.. code-block:: python + + async for action, result, state in application.aiterate(): + print(action.name, result) + +In the synchronous context this also has a return value of a tuple of: +1. the final state +2. A list of the actions that were run, one for each result + +You can access this by looking at the ``value`` variable of the ``StopIteration`` exception that is thrown +at the end of the loop, as is standard for python. +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). + +``run``/``arun`` +---------------- + +Run just calls out to ``iterate`` and returns the final state. + +.. 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``). diff --git a/docs/concepts/state.rst b/docs/concepts/state.rst new file mode 100644 index 00000000..e712522d --- /dev/null +++ b/docs/concepts/state.rst @@ -0,0 +1,55 @@ +===== +State +===== + +.. _state: + +The ``State`` class provides the ability to manipulate state for a given action. It is entirely immutable, +meaning that you can only create new states from old ones, not modify them in place. + +State manipulation is done through the ``State`` class. The most common write are: + +.. code-block:: python + + state.update(foo=bar) # update the state with the key "foo" set to "bar" + state.append(foo=bar) # append "bar" to the list at "foo" + +The read operations extend from those in the [Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) +interface, but there are a few extra: + +.. code-block:: python + + state.subset(["foo", "bar"]) # return a new state with only the keys "foo" and "bar" + state.get_all() # return a dictionary of all the state + +When an update action is run, the state is first subsetted to get just the keys that are being read from, +then the action is run, and a new state is written to. This state is merged back into the original state +after the action is complete. Pseudocode: + +.. code-block:: python + + current_state = ... + read_state = current_state.subset(action.reads) + result = action.run(new_state) + write_state = current_state.subset(action.writes) + new_state = action.update(result, new_state) + current_state = current_state.merge(new_state) + +If you're used to thinking about version control, this is a bit like a commit/checkout/merge mechanism. + +---------------------- +Immutability (planned) +---------------------- + +We plan the ability to manage state in a few ways: + +1. ``commit`` -- an internal tool to commit/compile a series of changes so that we have the latest state evaluated +2. ``persist`` -- a user-facing API to persist state to a database. This will be pluggable by the user, and we will have a few built-in options (e.g. a simple in-memory store, a file store, a database store, etc...) +3. ``hydrate`` -- a static method to hydrate state from a database. This will also be pluggable and will mirror those in ``persist`` options. + +Currently state is immutable, but it utilizes an inefficient copy mechanism. This is out of expedience -- we don't anticipate this will +be painful for the time being, but plan to build a more efficient functional paradigm. We will likely have: +1. Each state object be a node in a linked list, with a pointer to the previous state. It carries a diff of the changes from the previous state. +2. An ability to ``checkpoint`` (allowing for state garbage collection), and store state in memory/kill out the pointers. + +We will also consider having the ability to have a state solely backed by a tool such as redis (and not memory), but we are still thinking through the API. diff --git a/docs/concepts/transitions.rst b/docs/concepts/transitions.rst new file mode 100644 index 00000000..90980df3 --- /dev/null +++ b/docs/concepts/transitions.rst @@ -0,0 +1,35 @@ +==================== +Transitions +==================== + +.. _transitions: + +Transitions move between actions. You can think of them as edges in a graph. + +They have three main components: +- The ``from`` state +- The ``to`` state +- The ``condition`` that must be met to move from the ``from`` state to the ``to`` + +---------- +Conditions +---------- + +Conditions have a few APIs, but the most common are the three convenience functions: + +.. code-block:: python + + from burr.core import when, expr, default + with_transitions( + ("from", "to", when(foo="bar"), # will evaluate when the state has the variable "foo" set to the value "bar" + ("from", "to", expr('epochs>100')) # will evaluate to True when the state has the variable "foo" set to the value "bar" + ("from", "to", default) # will always evaluate to True + ) + + +Conditions are evaluated in the order they are specified, and the first one that evaluates to True will be the transition that is selected +when determining which action to run next. + +Note that if no condition evaluates to ``True``, the application execution will stop early. + +See the :ref: `transitions docs ` for more information on the transition API. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..0ee027eb --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,45 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "Burr" +copyright = "2024, Elijah ben Izzy, Stefan Krawczyk" +author = "Elijah ben Izzy, Stefan Krawczyk" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ["sphinx.ext.autodoc", "sphinx.ext.autosummary", "myst_parser", "sphinx_sitemap"] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +html_theme = "furo" +html_static_path = ["_static"] + +html_title = "Burr" +html_theme_options = { + "source_repository": "https://github.com/dagworks-inc/burr", + "source_branch": "main", + "source_directory": "docs/", + "light_css_variables": { + "color-announcement-background": "#ffba00", + "color-announcement-text": "#091E42", + }, + "dark_css_variables": { + "color-announcement-background": "#ffba00", + "color-announcement-text": "#091E42", + }, +} + +html_baseurl = "https://burr.dagworks.io/en/latest/" # TODO -- update this + +exclude_patterns = ["README-internal.md"] + +autodoc_typehints_format = "short" +python_maximum_signature_line_length = 100 +python_use_unqualified_type_names = True diff --git a/docs/examples/chatbot.rst b/docs/examples/chatbot.rst new file mode 100644 index 00000000..97b6c0ad --- /dev/null +++ b/docs/examples/chatbot.rst @@ -0,0 +1,5 @@ +==================== +GPT-like chatbot +==================== + +TODO -- link to github diff --git a/docs/examples/index.rst b/docs/examples/index.rst new file mode 100644 index 00000000..0b6f32a3 --- /dev/null +++ b/docs/examples/index.rst @@ -0,0 +1,16 @@ +======== +Examples +======== + +.. _examples: + +These are still under progress -- see the github `examples directory `_ +for the latest. + +Examples of more complex/powerful use-cases of Burr. Download/copy these to adapt to your use-cases. + +.. toctree:: + simple + chatbot + ml_training + simulation diff --git a/docs/examples/ml_training.rst b/docs/examples/ml_training.rst new file mode 100644 index 00000000..f2d7395d --- /dev/null +++ b/docs/examples/ml_training.rst @@ -0,0 +1,5 @@ +==================== +ML Model Training +==================== + +TODO -- link to github diff --git a/docs/examples/simple.rst b/docs/examples/simple.rst new file mode 100644 index 00000000..853fb5f0 --- /dev/null +++ b/docs/examples/simple.rst @@ -0,0 +1,7 @@ +==================== +Simple examples +==================== + +TODO -- link to github +- cowsay +- counter diff --git a/docs/examples/simulation.rst b/docs/examples/simulation.rst new file mode 100644 index 00000000..aac33c75 --- /dev/null +++ b/docs/examples/simulation.rst @@ -0,0 +1,5 @@ +==================== +Simulation +==================== + +TODO -- link to github diff --git a/docs/getting_started/index.rst b/docs/getting_started/index.rst new file mode 100644 index 00000000..3a78e084 --- /dev/null +++ b/docs/getting_started/index.rst @@ -0,0 +1,15 @@ +.. _gettingstarted: + +==================== +Getting Started +==================== + +The following section of the docs will walk you through Burr and how to integrate into your project: + +.. toctree:: + :maxdepth: 1 + + why-burr + install + simple-example + up-next diff --git a/docs/getting_started/install.rst b/docs/getting_started/install.rst new file mode 100644 index 00000000..831634f2 --- /dev/null +++ b/docs/getting_started/install.rst @@ -0,0 +1,24 @@ +==================== +Getting Started +==================== + +Burr requires almost no dependencies. Every "extra"/"plugin" is an additional install target. Note, if you're using ``zsh``, +you'll need to add quotes around the install target, like `pip install "burr[visualization]"`. + +.. code-block:: bash + + pip install burr + +To get visualization capabilities, you can install the `burr[visualization]` extra: + +.. code-block:: bash + + pip install burr[visualization] + +And to visualize your state machines on streamlit, you can install the `burr[streamlit]` extra: + +.. code-block:: bash + + pip install burr[streamlit] + +Don't worry, you can always install these extras later if you need them. diff --git a/docs/getting_started/simple-example.rst b/docs/getting_started/simple-example.rst new file mode 100644 index 00000000..f61c8217 --- /dev/null +++ b/docs/getting_started/simple-example.rst @@ -0,0 +1,100 @@ +.. _simpleexample: + +================= +Simple Example +================= +This simple example is just to learn the basics of the library. The application we're building is not particularly interesting, +but it will get you powerful. If you want to skip ahead to the cool stuff (chatbots, +ML training, simulations, etc...) feel free to jump into the deep end and start with the :ref:`examples `. + +We will go over enough of the concepts to help you understand the code, but there is a much more in-depth set of +explanations in the :ref:`concepts ` section. + +------------------ +Build a Counter +------------------ +We're going to build a counter application. This counter will count to 10 and then stop. + +Let's start by defining some actions, the building-block of Burr. You can think of actions as a function that +computes a result and modifies state. They declare what they read and write. + +Let's define two actions: +1. A counter action that increments the counter +2. A printer action that prints the counter + + +.. code-block:: python + + @action(reads=["counter"], writes=["counter"]) + def count(state: State) -> Tuple[dict, State]: + current = state["counter"] + 1 + result = {"counter": current} + print("counted to ", current) + return result, state.update(**result) # return both the intermediate + + @action(reads=["counter"], writes=[]) + def done(state: State) -> Tuple[dict, State]: + print("Bob's your uncle") + return {}, state + +This is an action that reads the "counter" from the state, increments it by 1, and then writes the new value back to the state. +It returns both the intermediate result (``result``) and the updated state. + +Next, let's put together our application. To do this, we'll use an ``ApplicationBuilder`` + +.. code-block:: python + + from burr.core import ApplicationBuilder, default, expr + app = ( + ApplicationBuilder() + .with_state(counter=0) # initialize the count to zero + .with_actions( + count=count, # add the counter action with the name "counter" + done=done # add the printer action with the name "printer" + ).with_transitions( + ("count", "count", expr("counter < 10")), # Keep counting if the counter is less than 10 + ("count", "done", default) # Otherwise, we're done + ).with_entrypoint("counter") # we have to start somewhere + .build() + ) + + +We can visualize the application (note you need ``burr[visualization]`` installed): + +.. code-block:: python + + app.visualize() + +.. image:: ../_static/counter.png + :align: center + +As you can see, we have: + +1. The action ``count`` that reads and writes the ``counter`` state field +2. The action ``done`` that reads the ``counter`` state field +3. A transition from ``count`` to ``count`` if ``counter < 10`` +4. A transition from ``count`` to ``done`` otherwise + +Finally, we can run the application: + +.. code-block::python + + app.run(until=["printer"]) + +And the output looks exactly as we expect! + +.. code-block::bash + + counted to 1 + counted to 2 + counted to 3 + counted to 4 + counted to 5 + counted to 6 + counted to 7 + counted to 8 + counted to 9 + counted to 10 + +All this to increment? Well, if all you want to do is count to 10, this might not be for you. But we imagine most of you want to do more exciting things +than count to 10... diff --git a/docs/getting_started/up-next.rst b/docs/getting_started/up-next.rst new file mode 100644 index 00000000..7d3fca71 --- /dev/null +++ b/docs/getting_started/up-next.rst @@ -0,0 +1,12 @@ +================= +Next Steps +================= + +You've written your own burr application in a few lines of code. Nice work! Let's outline a few of the capabilities +of the Burr library and where you can go to learn more about them. + +- :ref:`Creating custom actions ` and calling out to integrated frameworks +- :ref:`Running applications `, managing their lifeycyle, and inspecting the results +- :ref:`Managing state ` -- persisting, inspecting, and updating +- :ref:`Handling transition between nodes ` and managing the flow of your application +- :ref:`Adding hooks to customize execution ` and integrate with other systems diff --git a/docs/getting_started/why-burr.rst b/docs/getting_started/why-burr.rst new file mode 100644 index 00000000..3a16ddec --- /dev/null +++ b/docs/getting_started/why-burr.rst @@ -0,0 +1,48 @@ +================= +Why Burr +================= + +--------------------------------------- +An abstraction to make your life easier +--------------------------------------- + +Why do you need a state machine for your applications? Won't the normal programming constructs suffice? + +Yes, until a point. Let's look at a chatbot as an example. Here's a simple design of something gpt-like: + +#. Accept a prompt from the user +#. Does some simple checks/validations on that prompt (is it safe/within the terms of service) +#. If (2) it then decides the mode to which to respond to that prompt from a set of capabilities. Else respond accordingly: + * Generate an image + * Answer a question + * Write some code + * ... +#. It then queries the appropriate model with the prompt, formatted as expected +#. If this fails, we present an error message +#. If this succeeds, we present the response to the user +#. Await a new prompt, GOTO (1) + +Visually, we might have an implementation/spec that looks like this: + +.. image:: ../_static/demo_graph.png + :align: center + + +While this involves multiple API calls, error-handling, etc... it is definitely possible to get a prototype +that looks slick out without too much abstraction. Let's get this to production, however. We need to: + +#. Add monitoring to figure out if/why any of our API calls return strange results +#. Understand the decisions made by the application -- E.G. why it chose certain modes, why it formatted a response correctly. This involves: + * Tracking all the prompts/responses + * Going back in time to examine the state of the application at a given point +#. Debug it in a local mode, step-by-step, using the state we observed in production +#. Add new capabilities to the application +#. Monitor the performance of the application -- which steps/decisions are taking the longest? +#. Monitor the cost of running the application -- how many tokens are we accepting from/delivering to the user. +#. Save the state out to some external store so you can restart the conversation from where you left off + +And this is the tip of the iceberg -- chatbots (and all stateful applications) get really complicated, really quickly. + +Burr is designed to unlock the capabilities above and more -- decomposing your application into functions that manipulate state +and transition, with hooks that allow you to customize any part of the application. It is a platform on top of which you can build any of the +production requirements above, and comes with many of them out of the box! diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..e6feb08d --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,23 @@ +.. include:: main.rst + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: User guide: + + getting_started/index + concepts/index + +.. toctree:: + :hidden: + :maxdepth: 2 + :caption: Examples: + + examples/index + +.. toctree:: + :hidden: + :maxdepth: 2 + :caption: Reference: + + reference/index diff --git a/docs/main.rst b/docs/main.rst new file mode 100644 index 00000000..2ee12ff9 --- /dev/null +++ b/docs/main.rst @@ -0,0 +1,16 @@ +============== +Burr +============== + +Welcome to Burr's documentation, the state machine for managing complex data/LLM projects. + +You'll find this documentation separated into three sections. + +- If you don't know where to start, go to :ref:`getting started `. +- If you're looking to build a mental model/read more, go to :ref:`concepts `. +- If you're looking for a specific piece of information, go to :ref:`reference `. + +We also ask that you: + +- Join the `Hamilton slack `_ (currently we're combining the help-channel for the two products) +- Give us a star on `GitHub `_ if you like the project! diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..32bb2452 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/reference/actions.rst b/docs/reference/actions.rst new file mode 100644 index 00000000..0e0f9bb6 --- /dev/null +++ b/docs/reference/actions.rst @@ -0,0 +1,27 @@ +================= +Actions +================= + +.. _actionref: + +.. autoclass:: burr.core.action.Action + :members: + :inherited-members: + :show-inheritance: + + .. automethod:: __init__ + +.. autoclass:: burr.core.action.Result + :members: + + .. automethod:: __init__ + +.. autodecorator:: burr.core.action.action +.. autofunction:: burr.core.action.bind + + +.. autoclass:: burr.core.action.Function + :members: + +.. autoclass:: burr.core.action.Reducer + :members: diff --git a/docs/reference/application.rst b/docs/reference/application.rst new file mode 100644 index 00000000..87455732 --- /dev/null +++ b/docs/reference/application.rst @@ -0,0 +1,15 @@ +================= +Applications +================= + +Use this to build and manage a state Machine. You should only ever instantiate the ``ApplicationBuilder`` class, +and not the ``Application`` class directly. + + +.. autoclass:: burr.core.application.ApplicationBuilder + :members: + +.. autoclass:: burr.core.application.Application + :members: + + .. automethod:: __init__ diff --git a/docs/reference/conditions.rst b/docs/reference/conditions.rst new file mode 100644 index 00000000..56bed0f3 --- /dev/null +++ b/docs/reference/conditions.rst @@ -0,0 +1,13 @@ +======================= +Conditions/Transitions +======================= + +.. transitionref: + +Conditions represent choices to move between actions -- these are read by the application builder when executing the graph. +Note that these will always be specified in order -- the first condition that evaluates to ``True`` will be the selected action. + +.. autoclass:: burr.core.action.Condition + :members: + + .. automethod:: __init__ diff --git a/docs/reference/index.rst b/docs/reference/index.rst new file mode 100644 index 00000000..6e22292c --- /dev/null +++ b/docs/reference/index.rst @@ -0,0 +1,19 @@ +.. _reference: + +======================== +Reference Documentation +======================== + +Reference documentation. Anything in here is part of the public (semantically versioned) API, unless marked otherwise. +That means that if you find something not in here, it's not part of the public API and may change without notice. If you +need functionality that is not publicly exposed, please open an issue and we can discuss adding it! + +.. toctree:: + :maxdepth: 2 + + application + actions + state + conditions + lifecycle + integrations/index diff --git a/docs/reference/integrations/hamilton.rst b/docs/reference/integrations/hamilton.rst new file mode 100644 index 00000000..5bf84a08 --- /dev/null +++ b/docs/reference/integrations/hamilton.rst @@ -0,0 +1,15 @@ +======================= +Hamilton +======================= + +Full Hamilton integration. Touch-points are custom Hamilton actions. This is installed by default. + +.. autofunction:: burr.integrations.hamilton.Hamilton + +.. autofunction:: burr.integrations.hamilton.from_state + +.. autofunction:: burr.integrations.hamilton.from_value + +.. autofunction:: burr.integrations.hamilton.update_state + +.. autofunction:: burr.integrations.hamilton.append_state diff --git a/docs/reference/integrations/index.rst b/docs/reference/integrations/index.rst new file mode 100644 index 00000000..d84e9b8f --- /dev/null +++ b/docs/reference/integrations/index.rst @@ -0,0 +1,11 @@ +================ +Integrations +================ + +Integrations -- we will be adding more + +.. toctree:: + :maxdepth: 2 + + hamilton + streamlit diff --git a/docs/reference/integrations/streamlit.rst b/docs/reference/integrations/streamlit.rst new file mode 100644 index 00000000..d24e6417 --- /dev/null +++ b/docs/reference/integrations/streamlit.rst @@ -0,0 +1,31 @@ +======================= +Streamlit +======================= + +Full Streamlit integration. Tough-points are utility functions. +It is likely this will adapt/change over time, so it is only recommended to use this for debugging/developing. + +Install with pypi: + +.. code-block:: bash + + pip install burr[streamlit] + +.. autoclass:: burr.integrations.streamlit.AppState + :members: + +.. autofunction:: burr.integrations.streamlit.load_state_from_log_file + +.. autofunction:: burr.integrations.streamlit.get_state + +.. autofunction:: burr.integrations.streamlit.update_state + +.. autofunction:: burr.integrations.streamlit.render_state_machine + +.. autofunction:: burr.integrations.streamlit.render_action + +.. autofunction:: burr.integrations.streamlit.render_state_results + +.. autofunction:: burr.integrations.streamlit.set_slider_to_current + +.. autofunction:: burr.integrations.streamlit.render_explorer diff --git a/docs/reference/lifecycle.rst b/docs/reference/lifecycle.rst new file mode 100644 index 00000000..bf2778d7 --- /dev/null +++ b/docs/reference/lifecycle.rst @@ -0,0 +1,30 @@ +================= +Lifecycle API +================= + + +These are the available lifecycle hooks for Burr. Implement these classes +and add instances to the application builder to customize your state machines's execution. + +.. _hooksref: +.. autoclass:: burr.lifecycle.base.PreRunStepHook + :members: + +.. autoclass:: burr.lifecycle.base.PreRunStepHookAsync + :members: + +.. autoclass:: burr.lifecycle.base.PostRunStepHook + :members: + +.. autoclass:: burr.lifecycle.base.PostRunStepHookAsync + :members: + +These hooks are available for you to use: + +.. autoclass:: burr.lifecycle.default.StateAndResultsFullLogger + + .. automethod:: __init__ + +.. autoclass:: burr.lifecycle.default.SlowDownHook + + .. automethod:: __init__ diff --git a/docs/reference/state.rst b/docs/reference/state.rst new file mode 100644 index 00000000..a262a781 --- /dev/null +++ b/docs/reference/state.rst @@ -0,0 +1,10 @@ +================= +State +================= + +Use the state API to manipulate the state of the application. + +.. autoclass:: burr.core.state.State + :members: + + .. automethod:: __init__ diff --git a/pyproject.toml b/pyproject.toml index e80f3ff1..6a6924ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,14 @@ tests = [ "pytest-asyncio", ] +documentation = [ + "sphinx", + "sphinx-autobuild", + "myst-parser", + "furo", + "sphinx-sitemap" +] + [tool.poetry.packages] include = [ { include = "burr", from = "." },