Choose your own adventureยถ

A simple text-based game in the style of 80s adventure games in which you play a hungry corgi.

Setupยถ

import json
from typing import Optional, Tuple

from burr.core import action, Application, ApplicationBuilder, State, default, when
from openai import OpenAI
# set your OpenAI API key to run the rest of the notebook
import os
os.environ["OPENAI_API_KEY"] = ...
RESTRICTIONS = """You're a small corgi with short legs. You can't jump high,
you can't run fast, you can't perform feats of athleticism in general
to achieve any of your goals. You can't open doors, you can't use tools,
you can't communicate with humans, you can't use your paws to manipulate
objects, you can't use your mouth to communicate with humans"""

Define actionsยถ

@action(reads=["challenges"], writes=["current_challenge"])
def start(state: State) -> State:
    """Load the first challenge as the current challenge"""
    current_challenge = state["challenges"][0]
    return state.update(current_challenge=current_challenge)


@action(reads=["current_challenge"], writes=["attempts"])
def prompt_for_challenge(state: State) -> State:
    """Request user input to solve the current challenge"""
    current_challenge = state["current_challenge"]
    user_response = input(f'{current_challenge}. What do you do?\n $ ')
    return state.append(attempts=user_response)


@action(
    reads=["attempts", "current_challenge"],
    writes=["challenge_solved", "what_happened"],
)
def evaluate_attempt(state: State) -> State:
    response = OpenAI().chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": f"""You are evaluating responses for
             whether they constitute solutions to the provided challenge in a text
             based game, whose protagonist is a dog subject to the following limitations:
             {RESTRICTIONS}. You respond ONLY with a json object containing two fields: "solved", which is a
             boolean indicating whether the challenge was solved by the attempt, and "what_happened",
             which is a string containing a brief narrative, written in the second person and addressed
             to the player, of what happened during the protagonist's attempt""",
            },
            {
                "role": "user",
                "content": f"The current challenge is: {state['current_challenge']} "
                f"and the player's attempt is: {state['attempts'][-1]}",
            },
        ],
    )
    content = response.choices[0].message.content
    try:
        json_result = json.loads(content)
    except json.JSONDecodeError:
        print("bad json: ", content)
        json_result = {
            "solved": False,
            "what_happened": "Not sure, really. I'm a dog. I can't read json. I can't read at all.",
        }

    return state.update(
        challenge_solved=json_result["solved"],
        what_happened=json_result["what_happened"],
    )


@action(
    reads=["challenge_solved", "current_challenge", "what_happened"],
    writes=["current_challenge", "did_win"],
)
def maybe_progress(state: State) -> State:
    print("What happened:", state["what_happened"])
    if state["challenge_solved"]:
        if state["current_challenge"] == challenges[-1]:
            result = {"did_win": True}
        else:
            result = {
                "current_challenge": challenges[challenges.index(state["current_challenge"]) + 1]
            }
    else:
        result = {"current_challenge": state["current_challenge"]}
    return state.update(**result)


@action(reads=["challenges"], writes=[])
def win(state: State) -> State:
    # get summary of actions taken from openai
    print("you won")
    return state
challenges = [
    "There is a dish of dog food on the floor. You want to eat it",
    "There is a dish of dog food on a table. You want to eat it",
    "There is a dish of dog food in a locked car. You want to eat it",
]
def build_application(
    app_id: Optional[str] = None,
    storage_dir: Optional[str] = "~/.burr",
) -> Application:
    return (
        ApplicationBuilder()
        .with_state(did_win=False, challenges=challenges)
        .with_actions(
            start=start,
            prompt_for_challenge=prompt_for_challenge,
            evaluate_attempt=evaluate_attempt,
            maybe_progress=maybe_progress,
            win=win,
        )
        .with_transitions(
            ("start", "prompt_for_challenge", default),
            ("prompt_for_challenge", "evaluate_attempt", default),
            ("evaluate_attempt", "maybe_progress", default),
            ("maybe_progress", "win", when(did_win=True)),
            ("maybe_progress", "prompt_for_challenge", default),
        )
        .with_entrypoint("start")
        .with_tracker(project="demo_corgi_adventure", params={"storage_dir": storage_dir})
        .with_identifiers(app_id=app_id)
        .build()
    )

app = build_application()
app.visualize(output_file_path="digraph", include_conditions=True, view=False, format="png")
../../../_images/f52a61a867091a818e1acc164177df0b8e6967c73e2babf6f81bade4ac0b6dc8.svg
action, state, result = app.run(halt_after=["win"])
What happened: You opened your mouth and attempted to eat the food, but without the ability to manipulate the dish, you couldnโ€™t reach the food. It remained untouched on the floor, taunting you with its delicious aroma.
What happened: You tugged at the dish with your mouth, but your short legs can't provide enough strength to pull it any closer. The dish remains out of reach, and your belly rumbles in disappointment as you realize you can't manipulate it like you hoped.
What happened: You approach the dish of dog food with eager anticipation, but as you try to lean over and take a bite, your short legs make it difficult to reach the food comfortably. You can't quite get your mouth to the dish, and after a few attempts, you realize you'll need a different plan to enjoy your meal.
What happened: You try to bite the side of the dish to tip it over, but your short legs and limited strength make it impossible. The dish remains firmly on the floor, and the tantalizing smell of the dog food wafts up to you, teasing you further.
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[15], line 1
----> 1 action, state, result = app.run(halt_after=["win"])

File ~/dagworks/burr/burr/telemetry.py:276, in capture_function_usage.<locals>.wrapped_fn(*args, **kwargs)
    273 @functools.wraps(call_fn)
    274 def wrapped_fn(*args, **kwargs):
    275     try:
--> 276         return call_fn(*args, **kwargs)
    277     finally:
    278         if is_telemetry_enabled():

File ~/dagworks/burr/burr/core/application.py:672, in _call_execute_method_pre_post.__call__.<locals>.wrapper_sync(app_self, *args, **kwargs)
    670 exc = None
    671 try:
--> 672     return fn(app_self, *args, **kwargs)
    673 finally:
    674     self.call_post(app_self, exc)

File ~/dagworks/burr/burr/core/application.py:1243, in Application.run(self, halt_before, halt_after, inputs)
   1241 while True:
   1242     try:
-> 1243         next(gen)
   1244     except StopIteration as e:
   1245         result = e.value

File ~/dagworks/burr/burr/core/application.py:1186, in Application.iterate(self, halt_before, halt_after, inputs)
   1183 prior_action: Optional[Action] = None
   1184 while self.has_next_action():
   1185     # self.step will only return None if there is no next action, so we can rely on tuple unpacking
-> 1186     prior_action, result, state = self.step(inputs=inputs)
   1187     yield prior_action, result, state
   1188     if self._should_halt_iterate(halt_before, halt_after, prior_action):

File ~/dagworks/burr/burr/core/application.py:672, in _call_execute_method_pre_post.__call__.<locals>.wrapper_sync(app_self, *args, **kwargs)
    670 exc = None
    671 try:
--> 672     return fn(app_self, *args, **kwargs)
    673 finally:
    674     self.call_post(app_self, exc)

File ~/dagworks/burr/burr/core/application.py:843, in Application.step(self, inputs)
    840 # we need to increment the sequence before we start computing
    841 # that way if we're replaying from state, we don't get stuck
    842 self._increment_sequence_id()
--> 843 out = self._step(inputs=inputs, _run_hooks=True)
    844 return out

File ~/dagworks/burr/burr/core/application.py:885, in Application._step(self, inputs, _run_hooks)
    883 try:
    884     if next_action.single_step:
--> 885         result, new_state = _run_single_step_action(
    886             next_action, self._state, action_inputs
    887         )
    888     else:
    889         result = _run_function(
    890             next_action, self._state, action_inputs, name=next_action.name
    891         )

File ~/dagworks/burr/burr/core/application.py:287, in _run_single_step_action(action, state, inputs)
    284 # TODO -- guard all reads/writes with a subset of the state
    285 action.validate_inputs(inputs)
    286 result, new_state = _adjust_single_step_output(
--> 287     action.run_and_update(state, **inputs), action.name, action.schema
    288 )
    289 _validate_result(result, action.name, action.schema)
    290 out = result, _state_update(state, new_state)

File ~/dagworks/burr/burr/core/action.py:655, in FunctionBasedAction.run_and_update(self, state, **run_kwargs)
    654 def run_and_update(self, state: State, **run_kwargs) -> tuple[dict, State]:
--> 655     return self._fn(state, **self._bound_params, **run_kwargs)

Cell In[12], line 12, in prompt_for_challenge(state)
     10 """Request user input to solve the current challenge"""
     11 current_challenge = state["current_challenge"]
---> 12 user_response = input(f'{current_challenge}. What do you do?\n $ ')
     13 return state.append(attempts=user_response)

File ~/.pyenv/versions/burr-py310/lib/python3.10/site-packages/ipykernel/kernelbase.py:1270, in Kernel.raw_input(self, prompt)
   1268     msg = "raw_input was called, but this frontend does not support input requests."
   1269     raise StdinNotImplementedError(msg)
-> 1270 return self._input_request(
   1271     str(prompt),
   1272     self._parent_ident["shell"],
   1273     self.get_parent("shell"),
   1274     password=False,
   1275 )

File ~/.pyenv/versions/burr-py310/lib/python3.10/site-packages/ipykernel/kernelbase.py:1313, in Kernel._input_request(self, prompt, ident, parent, password)
   1310 except KeyboardInterrupt:
   1311     # re-raise KeyboardInterrupt, to truncate traceback
   1312     msg = "Interrupted by user"
-> 1313     raise KeyboardInterrupt(msg) from None
   1314 except Exception:
   1315     self.log.warning("Invalid Message:", exc_info=True)

KeyboardInterrupt: Interrupted by user