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
**kwargsof 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_afterorhalt_before. In the after case the action will have already run. In thebeforecase the action will not have run.The result of that action, in the
halt_aftercase, else None in thehalt_beforecase.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.graphwill give you a static representation of the state machine with enough information to visualizeapplication.statewill 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