..
   Licensed to the Apache Software Foundation (ASF) under one
   or more contributor license agreements.  See the NOTICE file
   distributed with this work for additional information
   regarding copyright ownership.  The ASF licenses this file
   to you under the Apache License, Version 2.0 (the
   "License"); you may not use this file except in compliance
   with the License.  You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing,
   software distributed under the License is distributed on an
   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   KIND, either express or implied.  See the License for the
   specific language governing permissions and limitations
   under the License.


.. _simpleexample:

==============
Quickstart
==============

Let's build a basic chatbot using Burr.

While Burr has a host of features related to state management and inspection, this basic tutorial is going to demonstrate two that are particularly relevant to LLM apps.

1. Specifying user inputs
2. Persisting state across multiple interactions

This tutorial will set you up to do more complex things, including:

1. Building an application that makes decisions/handles conditional edges
2. Persisting state in a database
3. Tracking/monitoring your application
4. Generating test cases from prior application runs

So hold tight! This gets you started with the basics but there's a lot more you can do with little effort.


.. note::

    This should take about 10 minutes to complete, and give you a good sense of the library basics.
    You'll need an OpenAI key set as the environment variable ``OPENAI_API_KEY``. If you don't have one you can get one at `OpenAI <https://platform.openai.com>`_.
    If you don't want to get one, check out the simple example of a `counter application <https://github.com/apache/burr/tree/main/examples/hello-world-counter>`_.

    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 <examples>`.

🤔 If you prefer to learn by video, check out
`this video walkthrough <https://www.youtube.com/watch?v=rEZ4oDN0GdU>`_
using `this notebook <https://github.com/apache/burr/blob/main/examples/simple-chatbot-intro/notebook.ipynb>`_.

----------------------
Build a Simple Chatbot
----------------------

Let's build! Our chatbot will accept user input and pass it to an AI. The AI will then respond with a message.

Let's start by defining some actions, the building-blocks of Burr. You can think of actions as a function that
computes a result and modifies state. They declare what they read and write.


We define two actions:

1. ``human_input`` This accepts a prompt from the outside and adds it to the state
2. ``ai_response`` This takes the prompt + chat history and queries OpenAI.


.. code-block:: python

    # Note: you can replace this with your favorite LLM Client
    client = openai.Client()

    @action(reads=[], writes=["prompt", "chat_history"])
    def human_input(state: State, prompt: str) -> State:
        chat_item = {
            "content": prompt,
            "role": "user"
        }
        return state.update(prompt=prompt).append(chat_history=chat_item)


    @action(reads=["chat_history"], writes=["response", "chat_history"])
    def ai_response(state: State) -> State:
        content = client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=state["chat_history"],
        ).choices[0].message.content
        chat_item = {
            "content": content,
            "role": "assistant"
        }
        return state.update(response=content).append(chat_history=chat_item)


Before we proceed, let's note the following about how we define these actions:

1. State is a dictionary -- actions declare input fields (as strings) and write values to those fields
2. Actions use a specific *immutable* state object and call operations on it (``.append(...)``, ``.update(...)``)
3. Functions can do whatever you want -- they can use plain python, or delegate to `langchain <https://langchain.com>`_, `hamilton <https://github.com/apache/hamilton>`_, etc... All they have to do is return the new state.
4. We declare the parameter ``prompt``, meaning that we will expect the user to pass ``prompt`` every time they run the graph.

.. note::

   The action API has a few different forms. We've shown the simplest above, but it can also return a tuple of ``result, state`` (where ``result``
   the intermediate result of the computation), to help with debugging. There are also multiple class-based APIs. See the :ref:`actions <actions>` section for more details.

Next, let's piece together our application. To do this, we'll use an ``ApplicationBuilder``

.. code-block:: python

    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()
    )

We can visualize the application (note you need ``burr[graphviz]`` installed):

.. code-block:: python

    app.visualize("./graph", format="png")

.. image:: ../_static/chatbot.png
    :align: center

Let's note the following about how we define the application:

1. It is an infinite loop! It is meant to pause for new prompt input.
2. We're just using the function names as the action names. You can also name them if you want ``with_actions(human_input=human_input, ai_response=ai_response)``.
3. We start it with an empty ``chat_history`` state field
4. It utilizes a `builder pattern <https://en.wikipedia.org/wiki/Builder_pattern#:~:text=The%20builder%20pattern%20is%20a,Gang%20of%20Four%20design%20patterns>`_ -- this is a bit old-school (comes from the java days), but is an easy/modular way to express your application

Finally, we can run the application -- it gives back multiple pieces of information but all we'll use is the state.

.. code-block:: python

    *_, state = app.run(halt_after=["ai_response"], inputs={"prompt": "Who was Aaron Burr?"})
    print("answer:", app.state["response"])
    print(len(state["chat_history"]), "items in chat")

The result looks exactly as you'd expect!

.. code-block:: text

    answer: Aaron Burr was an American politician and lawyer who served as the third
    Vice President of the United States from 1801 to 1805 under President Thomas Jefferson.
    He is also known for killing Alexander Hamilton in a famous duel in 1804.
    Burr was involved in various political intrigues and controversies throughout his career,
    and his reputation was tarnished by suspicions of treason and conspiracy.
    2 items in chat

If you want to copy/paste, you can open up the following code block and add to a file called ``run.py``:

.. collapse:: <code>run.py</code>

    .. code-block:: python

        import uuid

        import openai  # replace with your LLM library

        from burr.core import action, State, ApplicationBuilder

        client = openai.Client()  # replace this if not using OpenAI

        @action(reads=[], writes=["prompt", "chat_history"])
        def human_input(state: State, prompt: str) -> State:
            chat_item = {
                "content": prompt,
                "role": "user"
            }
            return state.update(prompt=prompt).append(chat_history=chat_item)

        @action(reads=["chat_history"], writes=["response", "chat_history"])
        def ai_response(state: State) -> State:
            content = client.chat.completions.create(
                model="gpt-3.5-turbo",
                messages=state["chat_history"],
            ).choices[0].message.content
            chat_item = {
                "content": content,
                "role": "assistant"
            }
            return state.update(response=content).append(chat_history=chat_item)
        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()
        )

        app.visualize("./graph", format="png")
        *_, state = app.run(halt_after=["ai_response"], inputs={"prompt": "Who was Aaron Burr?"})
        print("answer:", app.state["response"])
        print(len(state["chat_history"]), "items in chat")


Finally, let's open up the UI and see what it looks like (note, that if you have not installed ``burr[learn]`` now is a good time...).

.. code-block:: bash

    burr

You'll see the UI pop up with projects. Navigate to `the UI <http://localhost:7241/project/my_first_app>`_ and explore!


Now that we've built a basic application, we can do the following with only a few lines of code:

1. :ref:`Add conditional edges <transitions>` --  add a condition as a third item in the tuple to the ``with_transitions`` method.
2. :ref:`Persist state to a database + reload <state-persistence>` -- add a ``initialize_from`` line to the builder and select a pre-existing/implement a custom persistence method.
3. :ref:`Add monitoring to track application data <tracking>` -- leverage ``with_tracker`` to track to the Burr UI and visualize your application live.
4. :ref:`Stream results back <streaming>` -- minimize time to first token by streaming results back to the user.
5. `Generate test cases from prior runs <https://github.com/apache/burr/tree/main/examples/test-case-creation>`_ -- use the ``burr-testburr-test-case create`` command to automatically generate test cases for your LLM app.
