Apache Burr

repository·main·Indexed 25 days ago

https://github.com/apache/burr

Apache Burr (incubating) is a framework for developing stateful applications, such as chatbots, agents, and simulations, using a state-machine approach with simple Python building blocks. It provides a built-in UI for real-time telemetry and debugging, and supports complex decision-making logic for LLM-driven applications. Version 0.42.0.

Tokens
100K
Snippets
263
Records
441
Agent score
79%

What's inside apache-burr

  1. Overview of Burr UI

    main
    Burr UI is an open-source telemetry interface designed for monitoring, debugging, and replaying application runs in real time. It is capable of running locally for development or being deployed alongside a production stack to observe live application behavior.
  2. Overview of Burr State Persistence

    main

    Burr provides tools for loading and saving state using lifecycle hooks. It supports various database integrations through both synchronous and asynchronous implementations.

    To avoid naming conflicts with underlying database libraries, Burr uses a naming convention of b_dependency-library (e.g., b_psycopg2).

  3. Use parallelism tools in Apache Burr

    main
    Apache Burr provides a set of tools within burr.core.parallelism to manage sub-actions and sub-graphs, making it easier to execute multiple tasks or processes in parallel within your workflows. These tools allow you to decompose complex graphs into manageable, concurrent units of work.
  4. Index of Burr usage examples

    main

    The following examples demonstrate various features and use cases of the Burr library:

    • simple-chatbot-intro: A basic chatbot implementation; recommended starting point for understanding Burr.
    • conversational-rag: Demonstrates conversational Retrieval-Augmented Generation (RAG) using state and prior knowledge to augment LLM calls.
    • hello-world-counter: A minimal state machine example used in documentation.
    • llm-adventure-game: A text-based adventure game demonstrating progression through hidden states and component reuse.
    • ml-training: A simple ML training pipeline demonstrating how to track model training.
    • multi-agent-collaboration: Demonstrates multi-agent collaboration patterns.
    • multi-modal-chatbot: Shows how to use a model to conditionally delegate tasks to other models.
    • streaming-overview: Demonstrates using the streaming API for faster user responses.
    • integrations/bedrock: Minimal graphs using Amazon Bedrock via BedrockAction and BedrockStreamingAction.
    • tracing-and-spans: Demonstrates how to use Burr's tracing functionality for increased visibility.
    • web-server: Shows how to integrate Burr into a web server for user interaction.
  5. Build trustworthy LLM applications with Instructor and Burr

    main

    This example demonstrates how to combine instructor for structured LLM outputs with burr for application observability, debugging, and testing.

    By using Burr in your LLM application, you gain:

    • Observability: Real-time monitoring and logging of your Application execution, viewable via Burr's web user interface.
    • Persistence: The ability to save the application State at any point. This enables creating user sessions (like conversation histories), investigating bugs, iterating on code paths, and generating test cases for guardrails.
    • Portability: The Application can be deployed in various environments, including notebooks, scripts, or web services (e.g., using FastAPI).
  6. Explore Streaming Async and Notebook implementations

    main

    The streaming overview includes additional implementations for different environments:

    • Async Streaming: The async_application.py file demonstrates how to implement streaming using asynchronous patterns. This is useful for building high-concurrency applications where you want to stream responses without blocking.
    • Interactive Notebooks: A Google Colab notebook is available to walk through the streaming logic step-by-step in a managed notebook environment.
  7. Use Streamlit integration for debugging and development

    main
    The Streamlit integration provides utility functions to visualize and interact with Burr state machines. Note that these are considered 'tough-points' (utility functions) and are subject to change; they are recommended primarily for debugging and development purposes rather than production use.
  8. Scaling Burr applications in a distributed environment

    main

    When scaling a Burr-powered web service horizontally, consider these two layers:

    Database Layer

    Scalability depends on your chosen database and schema. Burr facilitates efficient querying by partitioning data using application_id and a partition_key (such as a user_id). Indexing your state table on these keys allows for efficient retrieval of specific application states.

    Compute Layer

    To scale the server instances, you must handle state synchronization and locking to prevent race conditions if multiple servers attempt to run the same application simultaneously. Strategies include:

    • Database Locking: Use a locking mechanism in the database to ensure only one server processes a specific application at a time.
    • Sticky Sessions/Sharding: Route a specific user to the same server instance to minimize synchronization issues.
    • Custom Persistence Logic: Implement custom logic at the persistence layer to handle forking or resolution of state.
  9. Implement streaming actions in Burr

    main

    Streaming actions allow you to yield intermediate results (like LLM tokens) to the user as they are produced, reducing time-to-first-token.

    Function-based Streaming Actions

    Use the @streaming_action decorator. The function must be a generator that yields a tuple: (result_dict, state_update, None, None).

    • Intermediate results: Yield (result_dict, None) to provide data without updating the state.
    • Final result: The last yield must include the final result and the state update (e.g., yield final_result_dict, state.append(...)).

    Class-based Streaming Actions

    Inherit from StreamingAction. This approach separates the execution logic from the state update logic:

    • stream_run(...): A generator that yields only the result_dict. The final yield in this method is passed to the update method.
    • update(result, state): Receives the final result from stream_run and returns the updated State.
    • reads and writes: Properties defining the state keys used.

    Async Streaming Actions

    Both function-based and class-based patterns support async implementations using AsyncGenerator and async for loops.

  10. Synchronous vs Asynchronous hooks

    main

    Burr supports both synchronous and asynchronous hooks. The behavior depends on which version you implement:

    • Synchronous hooks: These are called during both synchronous and asynchronous run methods (including step, astep, iterate, aiterate, run, and arun).
    • Asynchronous hooks: These are only called when using the asynchronous methods (astep, aiterate, and arun).

    Note on Hook Order: Currently, hook execution order is undefined (though they currently follow the order of definition). Future implementations may call pre... hooks in definition order and post... hooks in reverse order.

  11. Planned Exception Management and Error Transitions

    main

    Currently, exceptions in an action break the control flow and stop the program. Burr plans to introduce the ability to conditionally transition based on exceptions, allowing you to route to error-handling or retry actions.

    While the final API is being designed, the conceptual approach involves defining error-based transitions in the builder. For example, you might transition from an action back to itself on a specific error type, or limit the number of error-based transitions before resetting.

    # Conceptual idea for error-based transitions in the builder
    builder.with_actions(
       some_flaky_action=some_flaky_action
    ).with_transitions(
       (
          "some_flaky_action",
          "some_flaky_action",
          error(APIException) # infinite retries
          error(APIException, max=3) # 3 visits to this edge then it gets reset if this is not chosen
          # That's stored in state
       )
    )
  12. Run all combinations of actions and states using MapActionsAndStates

    main

    To execute a full Cartesian product of all provided actions and all provided states (e.g., running every model in a list against every prompt in a list), implement the MapActionsAndStates class. This is the base class for both MapStates and MapActions.

    Implementation requirements:

    • .actions(): Generator yielding the actions to run.
    • .states(): Generator yielding the state variations.
    • .reduce(): Merges the results of all combinations into the final state.
    from burr.core import action, state
    from burr.core.parallelism import MapActionsAndStates, RunnableGraph
    from typing import Callable, Generator, List
    
    @action(reads=["prompt", "model"], writes=["llm_output"])
    def query_llm(state: State, model: str) -> State:
        return state.update(llm_output=_query_my_llm(prompt=state["prompt"], model=model))
    
    class TestModelsOverPrompts(MapActionsAndStates):
    
        def actions(self, state: State, context: ApplicationContext, inputs: Dict[str, Any]) -> Generator[Action | Callable | RunnableGraph, None, None]:
            for action in [
                query_llm.bind(model="gpt-4").with_name("gpt_4_answer"),
                query_llm.bind(model="o1").with_name("o1_answer"),
                query_llm.bind(model="claude").with_name("claude_answer"),
            ]:
                yield action
    
        def states(self, state: State, context: ApplicationContext, inputs: Dict[str, Any]) -> Generator[State, None, None]:
            for prompt in [
                "What is the meaning of life?",
                "What is the airspeed velocity of an unladen swallow?",
                "What is the best way to cook a steak?",
            ]:
                yield state.update(prompt=prompt)
    
        def reduce(self, state: State, states: Generator[State, None, None]) -> State:
            all_llm_outputs = []
            for sub_state in states:
                all_llm_outputs.append(
                    {
                        "output" : sub_state["llm_output"],
                        "model" : sub_state["model"],
                        "prompt" : sub_state["prompt"],
                    }
                )
            return state.update(all_llm_outputs=all_llm_outputs)
    
        @property
        def reads(self) -> List[str]:
            return ["prompts"]
    
        @property
        def writes(self) -> List[str]:
            return ["all_llm_outputs"]