Applications¶
Note
Applications tie everything in Burr together. They specify which actions should be included, how they connect (including conditional edges), and how to run.
Applications form the core representation of the state machine. You build them with the ApplicationBuilder
.
Here is the minimum that is required:
A
**kwargs
of actions passed towith_actions(...)
Any relevant transitions (with conditions)
An entry point – this is the first action to execute
This is shown in the example from getting started
from burr.core import ApplicationBuilder, default, expr
app = (
ApplicationBuilder()
.with_actions(human_input, ai_response)
.with_transitions(
("human_input", "ai_response"),
("ai_response", "human_input")
).with_state(chat_history=[])
.with_entrypoint("human_input")
.build()
)
Running¶
There are four 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.
action, result, state = application.step()
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 inputs for more information.
action, result, state = application.step(inputs={"prompt": input()})
iterate
/aiterate
¶
Iterate just runs step
in a row, functioning as a generator:
for action, result, state in application.iterate(halt_after=["final_action_1", "final_action_2"]):
print(action.name, result)
You can also run aiterate
in an async context:
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:
the action that was specified in
halt_after
orhalt_before
. In the after case the action will have already run. In thebefore
case the action will not have run.The result of that action, in the
halt_after
case, else None in thehalt_before
case.The state of the application at the time of halting.
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 generators.
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).
If you want it to (attempt to) run forever, you can pass empty lists to halt_after
and halt_before
.
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.
Warning
The state machine has the capability of halting even if it does not reach one of the specified conditions – if there are no more transitions to take. This, however, is not a supported feature (and will log a warning). This is considered undefined behavior – use at your own risk, and consider adding a halting condition, or a default transition to a terminal.
run
/arun
¶
Run just calls out to iterate
and returns the final state.
The halt_after
and halt_before
keyword arguments specify when to break out of running the state machine
and return control back. halt_after
will stop after the specified action(s) has run, and halt_before
will stop before the specified action(s) has run.
If multiple are specified, it will stop after the first one encountered, and the return values will be for that action.
action, result, state = application.run(
halt_after=["final_action_1", "final_action_2"]
)
In the async context, you can run arun
:
action, result, state = await application.arun(
halt_after=["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.
run
and arun
largely have the same behavior as iterate
and aiterate
.
stream_result
/astream_result
¶
These allow you to stream responses back. Note they work best with streaming actions. Read about them in the streaming section.
Graph API¶
The application exposes a graph API, allowing you to specify it as a graph separate from application concerns. While you likely won’t use this in your first few applications, it will become useful when you want to run it in a web-server (create a graph once, application many times), import the graph from another context, or run it in multiple contexts.
from burr.core import ApplicationBuilder, default, expr
graph = (
GraphBuilder()
.with_actions(human_input, ai_response)
.with_transitions(
("human_input", "ai_response"),
("ai_response", "human_input")
).build()
)
app = (
ApplicationBuilder()
.with_graph(graph)
with_state(chat_history=[])
.with_entrypoint("human_input")
.build()
)
You can read more about it in the GraphBuilder docs
.
Inspection¶
You can ask various questions of the state machine using publicly-supported APIs:
application.graph
will give you a static representation of the state machine with enough information to visualizeapplication.state
will give you the current state of the state machine. Note that if you modify it the results will not show up – state is immutable! Modify the state through actions.
See the application docs