GPT-like multimodal chatbot¶
 +

https://github.com/dagworks-inc/burr by DAGWorks Inc. (YCW23 & StartX).
Take🏠:
- high level what is Burr 
- what you can do with Burr (observing state & being able to debug a particular point in time) 
- watch a walkthrough of this notebook here. 
Agentic Problems¶
- Why did this LLM call fail? 
- Oh crap my code broke, why? 
- Things went off the rails, but where? 
- etc 
Monitoring for the win, right?¶
Well but … monitoring doesn’t help you debug & complete your dev loop
- How do I debug that quickly? 
- How do I fix the inputs/code, and restart my agent? 
- What if my agent was 20+ steps in … do I have to restart from step 0? or can I go to a specific point in time? 
Solution: Burr¶
(Complements our other framework Hamilton)
1. Agent application is modeled as State + Actions –> Graph¶
Straightforward multi-modal example below:
import copy
from IPython.display import Image, display
from IPython.core.display import HTML 
import openai
from burr.core import ApplicationBuilder, State, default, graph, when
from burr.core.action import action
from burr.tracking import LocalTrackingClient
MODES = {
    "answer_question": "text",
    "generate_image": "image",
    "generate_code": "code",
    "unknown": "text",
}
@action(reads=[], writes=["chat_history", "prompt"])
def process_prompt(state: State, prompt: str) -> State:
    result = {"chat_item": {"role": "user", "content": prompt, "type": "text"}}
    state = state.append(chat_history=result["chat_item"])
    state = state.update(prompt=prompt)
    return state
@action(reads=["prompt"], writes=["mode"])
def choose_mode(state: State) -> State:
    prompt = (
        f"You are a chatbot. You've been prompted this: {state['prompt']}. "
        f"You have the capability of responding in the following modes: {', '.join(MODES)}. "
        "Please respond with *only* a single word representing the mode that most accurately "
        "corresponds to the prompt. Fr instance, if the prompt is 'draw a picture of a cat', "
        "the mode would be 'generate_image'. If the prompt is "
        "'what is the capital of France', the mode would be 'answer_question'."
        "If none of these modes apply, please respond with 'unknown'."
    )
    llm_result = openai.Client().chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "You are a helpful assistant"},
            {"role": "user", "content": prompt},
        ],
    )
    content = llm_result.choices[0].message.content
    mode = content.lower()
    if mode not in MODES:
        mode = "unknown"
    result = {"mode": mode}
    return state.update(**result)
@action(reads=["prompt", "chat_history"], writes=["response"])
def prompt_for_more(state: State) -> State:
    result = {
        "response": {
            "content": "None of the response modes I support apply to your question. "
                       "Please clarify?",
            "type": "text",
            "role": "assistant",
        }
    }
    return state.update(**result)
@action(reads=["prompt", "chat_history", "mode"], writes=["response"])
def chat_response(
        state: State, prepend_prompt: str, model: str = "gpt-3.5-turbo"
) -> State:
    
    chat_history = copy.deepcopy(state["chat_history"])
    chat_history[-1]["content"] = f"{prepend_prompt}: {chat_history[-1]['content']}"
    chat_history_api_format = [
        {
            "role": chat["role"],
            "content": chat["content"],
        }
        for chat in chat_history
    ]
    client = openai.Client()
    result = client.chat.completions.create(
        model=model,
        messages=chat_history_api_format,
    )
    text_response = result.choices[0].message.content
    result = {"response": {"content": text_response, "type": MODES[state["mode"]], "role": "assistant"}}
    return state.update(**result)
@action(reads=["prompt", "chat_history", "mode"], writes=["response"])
def image_response(state: State, model: str = "dall-e-2") -> State:
    """Generates an image response to the prompt. Optional save function to save the image to a URL."""
    # raise ValueError("Demo error")
    client = openai.Client()
    result = client.images.generate(
        model=model, prompt=state["prompt"], size="1024x1024", quality="standard", n=1
    )
    image_url = result.data[0].url
    result = {"response": {"content": image_url, "type": MODES[state["mode"]], "role": "assistant"}}
    return state.update(**result)
@action(reads=["response", "mode"], writes=["chat_history"])
def response(state: State) -> State:
    # you'd do something specific here based on prior state
    result = {"chat_item": state["response"]}
    return state.append(chat_history=result["chat_item"])
# Built the graph.
base_graph = (
    graph.GraphBuilder()
    .with_actions(
        # these are the "nodes" 
        prompt=process_prompt,
        decide_mode=choose_mode,
        generate_image=image_response,
        generate_code=chat_response.bind(
            prepend_prompt="Please respond with *only* code and no other text (at all) to the following:",
        ),
        answer_question=chat_response.bind(
            prepend_prompt="Please answer the following question:",
        ),
        prompt_for_more=prompt_for_more,
        response=response,
    )
    .with_transitions(
        # these are the edges between nodes, based on state.
        ("prompt", "decide_mode", default),
        ("decide_mode", "generate_image", when(mode="generate_image")),
        ("decide_mode", "generate_code", when(mode="generate_code")),
        ("decide_mode", "answer_question", when(mode="answer_question")),
        ("decide_mode", "prompt_for_more", default),
        (
            ["generate_image", "answer_question", "generate_code", "prompt_for_more"],
            "response",
        ),
        ("response", "prompt", default),
    )
    .build()
)
base_graph.visualize()
2. Build application –> built in checkpointing & tracking¶
tracker = LocalTrackingClient(project="agent-demo")
app = (
    ApplicationBuilder()
    .with_graph(base_graph)
    .initialize_from(
        tracker, 
        resume_at_next_action=True, 
        default_state={"chat_history": []},
        default_entrypoint="prompt",
    )
    .with_tracker(tracker)  # tracking + checkpointing; one line 🪄.
    .build()
)
app
3. Comes with a UI¶
View runs in the UI; Let’s run the app first.
while True:
    user_input = input("Hi, how can I help?")
    if "quit" == user_input.lower():
        break
    last_action, action_result, app_state = app.run(
        halt_after=["response"], 
        inputs={"prompt": user_input}
    )
    last_message = app_state["chat_history"][-1]
    if last_message['type'] == 'image':
        display(Image(url=last_message["content"]))
    else:
        print(f"🤖: {last_message['content']}")
Hi, how can I help? what is the capital of France?
🤖: The capital of France is Paris.
Hi, how can I help? write hello world in java
🤖: ```
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
```
Hi, how can I help? draw a pen
********************************************************************************
-------------------------------------------------------------------
Oh no an error! Need help with Burr?
Join our discord and ask for help! https://discord.gg/4FxBMyzW5n
-------------------------------------------------------------------
> Action: `generate_image` encountered an error!<
> State (at time of action):
{'__PRIOR_STEP': 'decide_mode',
 '__SEQUENCE_ID': 10,
 'chat_history': "[{'role': 'user', 'content': 'what is the capital ...",
 'mode': 'generate_image',
 'prompt': 'draw a pen',
 'response': "{'content': '```\\npublic class HelloWorld {\\n    p..."}
> Inputs (at time of action):
{'prompt': 'draw a pen'}
********************************************************************************
Traceback (most recent call last):
  File "/Users/stefankrawczyk/dagworks/burr/burr/core/application.py", line 534, in _step
    result, new_state = _run_single_step_action(
  File "/Users/stefankrawczyk/dagworks/burr/burr/core/application.py", line 233, in _run_single_step_action
    action.run_and_update(state, **inputs), action.name
  File "/Users/stefankrawczyk/dagworks/burr/burr/core/action.py", line 533, in run_and_update
    return self._fn(state, **self._bound_params, **run_kwargs)
  File "/var/folders/gv/q39lb_1s26x7gbyyypqc3dkm0000gn/T/ipykernel_43564/1354917547.py", line 94, in image_response
    raise ValueError("Demo error")
ValueError: Demo error
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[12], line 5
      3 if "quit" == user_input.lower():
      4     break
----> 5 last_action, action_result, app_state = app.run(
      6     halt_after=["response"], 
      7     inputs={"prompt": user_input}
      8 )
      9 last_message = app_state["chat_history"][-1]
     10 if last_message['type'] == 'image':
File ~/dagworks/burr/burr/telemetry.py:273, in capture_function_usage.<locals>.wrapped_fn(*args, **kwargs)
    270 @functools.wraps(call_fn)
    271 def wrapped_fn(*args, **kwargs):
    272     try:
--> 273         return call_fn(*args, **kwargs)
    274     finally:
    275         if is_telemetry_enabled():
File ~/dagworks/burr/burr/core/application.py:878, in Application.run(self, halt_before, halt_after, inputs)
    876 while True:
    877     try:
--> 878         next(gen)
    879     except StopIteration as e:
    880         return e.value
File ~/dagworks/burr/burr/core/application.py:823, in Application.iterate(self, halt_before, halt_after, inputs)
    820 prior_action: Optional[Action] = None
    821 while self.has_next_action():
    822     # self.step will only return None if there is no next action, so we can rely on tuple unpacking
--> 823     prior_action, result, state = self.step(inputs=inputs)
    824     yield prior_action, result, state
    825     if self._should_halt_iterate(halt_before, halt_after, prior_action):
File ~/dagworks/burr/burr/core/application.py:495, in Application.step(self, inputs)
    492 # we need to increment the sequence before we start computing
    493 # that way if we're replaying from state, we don't get stuck
    494 self._increment_sequence_id()
--> 495 out = self._step(inputs=inputs, _run_hooks=True)
    496 return out
File ~/dagworks/burr/burr/core/application.py:548, in Application._step(self, inputs, _run_hooks)
    546     exc = e
    547     logger.exception(_format_BASE_ERROR_MESSAGE(next_action, self._state, inputs))
--> 548     raise e
    549 finally:
    550     if _run_hooks:
File ~/dagworks/burr/burr/core/application.py:534, in Application._step(self, inputs, _run_hooks)
    532 try:
    533     if next_action.single_step:
--> 534         result, new_state = _run_single_step_action(
    535             next_action, self._state, action_inputs
    536         )
    537     else:
    538         result = _run_function(
    539             next_action, self._state, action_inputs, name=next_action.name
    540         )
File ~/dagworks/burr/burr/core/application.py:233, in _run_single_step_action(action, state, inputs)
    230 # TODO -- guard all reads/writes with a subset of the state
    231 action.validate_inputs(inputs)
    232 result, new_state = _adjust_single_step_output(
--> 233     action.run_and_update(state, **inputs), action.name
    234 )
    235 _validate_result(result, action.name)
    236 out = result, _state_update(state, new_state)
File ~/dagworks/burr/burr/core/action.py:533, in FunctionBasedAction.run_and_update(self, state, **run_kwargs)
    532 def run_and_update(self, state: State, **run_kwargs) -> tuple[dict, State]:
--> 533     return self._fn(state, **self._bound_params, **run_kwargs)
Cell In[8], line 94, in image_response(state, model)
     91 @action(reads=["prompt", "chat_history", "mode"], writes=["response"])
     92 def image_response(state: State, model: str = "dall-e-2") -> State:
     93     """Generates an image response to the prompt. Optional save function to save the image to a URL."""
---> 94     raise ValueError("Demo error")
     95     client = openai.Client()
     96     result = client.images.generate(
     97         model=model, prompt=state["prompt"], size="1024x1024", quality="standard", n=1
     98     )
ValueError: Demo error
But something broke / I want to debug¶
Use:
- Application ID 
app_id = "a6d74912-9ad6-42f0-9a18-bc17c5e77eaf"
resumed_app = (
    ApplicationBuilder()
    .with_graph(base_graph)
    .initialize_from(
        tracker,
        resume_at_next_action=True, 
        default_state={"chat_history": []},
        default_entrypoint="prompt",
    )
    .with_tracker(tracker)
    .with_identifiers(app_id=app_id)
    .build()
)
resumed_app.state["chat_history"]
[{'role': 'user', 'content': 'what is the capital of France?', 'type': 'text'},
 {'content': 'The capital of France is Paris.',
  'type': 'text',
  'role': 'assistant'},
 {'role': 'user', 'content': 'write hello world in java', 'type': 'text'},
 {'content': '```\npublic class HelloWorld {\n    public static void main(String[] args) {\n        System.out.println("Hello, World!");\n    }\n}\n```',
  'type': 'code',
  'role': 'assistant'},
 {'role': 'user', 'content': 'draw a pen', 'type': 'text'}]
while True:
    user_input = input("Hi, how can I help?")
    if "quit" == user_input.lower():
        break
    last_action, action_result, app_state = resumed_app.run(
        halt_after=["response"], 
        inputs={"prompt": user_input}
    )
    last_message = app_state["chat_history"][-1]
    if last_message['type'] == 'image':
        display(Image(url=last_message["content"]))
    else:
        print(f"🤖: {last_message['content']}")
Hi, how can I help? 

Hi, how can I help? what is the capital of England?
🤖: The capital of England is London.
Hi, how can I help? quit
Actually what if I want to go back to a certain point in time?¶
- Fork: Start with state from any checkpoint 
app_id = "a6d74912-9ad6-42f0-9a18-bc17c5e77eaf"
sequence_id = 4
# partition_key = ""
forked_app = (
    ApplicationBuilder()
    .with_graph(base_graph) # this could be different...
    .initialize_from(
        tracker,
        resume_at_next_action=True, 
        default_state={"chat_history": []},
        default_entrypoint="prompt",
        fork_from_app_id=app_id,
        fork_from_sequence_id=sequence_id,
        # fork_from_partition_key=partition_key
    )
    .with_tracker(tracker)
    .build()
)
# show prior forked state
forked_app.state["chat_history"]
[{'role': 'user', 'content': 'what is the capital of France?', 'type': 'text'},
 {'content': 'The capital of France is Paris.',
  'type': 'text',
  'role': 'assistant'},
 {'role': 'user', 'content': 'write hello world in java', 'type': 'text'}]
while True:
    user_input = input("Hi, how can I help?")
    if "quit" == user_input.lower():
        break
    last_action, action_result, app_state = forked_app.run(
        halt_after=["response"], 
        inputs={"prompt": user_input}
    )
    last_message = app_state["chat_history"][-1]
    if last_message['type'] == 'image':
        display(Image(url=last_message["content"]))
    else:
        print(f"🤖: {last_message['content']}")
Hi, how can I help? 
🤖: ```java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
```
Want to know more?¶
Link to video walking through this notebook.
https://github.com/dagworks-inc/burr

Time Travel blog post & video:
More blogs @ blog.dagworks.io e.g. async & streaming
More examples:
- e.g. test case creation 
Follow on Twitter & LinkedIn:
- https://x.com/burr_framework 
- https://x.com/dagworks 
- https://x.com/stefkrawczyk 
- https://www.linkedin.com/in/skrawczyk/ 
