Skip to main content
Definition: An agent is a generative program in which an LLM determines the control flow of the program.
In the generative programs we have seen so far, the developer orchestrates a sequence of LLM calls. In contrast, agentic generative programs delegate control flow to the model itself. In this chapter we will see a couple of different ways of developing agents in Mellea:
  1. Classical Agents: How to implement agentic loops in Mellea using the ReACT pattern.
  2. Guarded Nondeterminism: We will return to the idea of generative slots, and see how this abstraction can help build more robust agents.

Case Study: Implementing ReACT in Mellea

Let’s build up to a full agent example using the ReACT pattern. We’ll start with pseudocode and then incrementally build our Mellea ReACT program. The core idea of ReACT is to alternate between reasoning (“Thought”) and acting (“Action”):
# Pseudocode
while not done:
    get the model's next thought
    take an action based upon the though
    choose arguments for the selection action
    observe the toll output
    check if a final answer can be obtained
return the final answer
Let’s look at how this agent is implemented in Mellea:
# file: https://github.com/generative-computing/mellea/blob/main/docs/examples/agents/react.py#L99
def react(
    m: mellea.MelleaSession,
    goal: str,
    state_description: str | None,
    react_toolbox: ReactToolbox,
):
    test_ctx_lin = m.ctx.view_for_generation()
    assert test_ctx_lin is not None and len(test_ctx_lin) == 0, (
        "ReACT expects a fresh context."
    )

    # Construct the system prompt for ReACT.
    _sys_prompt = react_system_template.render(
        {"today": datetime.date.today(), "tools": react_toolbox.tools}
    )

    # Add the system prompt and the goal to the chat history.
    m.ctx = m.ctx.add(
        mellea.stdlib.chat.Message(role="system", content=_sys_prompt)
    ).add(mellea.stdlib.chat.Message(role="user", content=f"{goal}"))

    # The main ReACT loop as a dynamic program:
    # (  ?(not done) ;
    #    (thought request ; thought response) ;
    #    (action request ; action response) ;
    #    (action args request ; action args response) ;
    #    observation from the tool call ;
    #    (is done request ; is done response) ;
    #    { ?(model indicated done) ; emit_final_answer ; done := true }
    # )*
    done = False
    turn_num = 0
    while not done:
        turn_num += 1
        print(f"## ReACT TURN NUMBER {turn_num}")

        print("### Thought")
        thought = m.chat(
            "What should you do next? Respond with a description of the next piece of information you need or the next action you need to take."
        )
        print(thought.content)

        print("### Action")
        act = m.chat(
            "Choose your next action. Respond with a nothing other than a tool name.",
            format=react_toolbox.tool_name_schema(),
        )
        selected_tool: ReactTool = react_toolbox.get_tool_from_schema(act.content)
        print(selected_tool.get_name())

        print("### Arguments for action")
        act_args = m.chat(
            "Choose arguments for the tool. Respond using JSON and include only the tool arguments in your response.",
            format=selected_tool.args_schema(),
        )
        print(f"```json\n{json.dumps(json.loads(act_args.content), indent=2)}\n```")

        print("### Observation")
        tool_output = react_toolbox.call_tool(selected_tool, act_args.content)
        m.ctx = m.ctx.add(mellea.stdlib.chat.Message(role="tool", content=tool_output))
        print(tool_output)

        print("### Done Check")
        is_done = IsDoneModel.model_validate_json(
            m.chat(
                f"Do you know the answer to the user's original query ({goal})? If so, respond with Yes. If you need to take more actions, then respond No.",
                format=IsDoneModel,
            ).content
        ).is_done
        if is_done:
            print("Done. Will summarize and return output now.")
            done = True
            return m.chat(
                f"Please provide your final answer to the original query ({goal})."
            ).content
        else:
            print("Not done.")
            done = False
The example implementation needs some helper classes which even allows tool calling:
if __name__ == "__main__":
    m = mellea.start_session(ctx=ChatContext())

    # ZIP lookup tool function
    def zip_lookup_tool_fn(city: str):
        """Returns the ZIP code for the `city`."""
        return "03285"

    # convert to tool
    zip_lookup_tool = ReactTool(
        name="Zip Code Lookup",
        fn=zip_lookup_tool_fn,
        description="Returns the ZIP code given a town name.",
    )

    # Weather lookup tool function
    def weather_lookup_fn(zip_code: str):
        """Looks up the weather for a town given a five-digit `zip_code`."""
        return "The weather in Thornton, NH is sunny with a high of 78 and a low of 52. Scattered showers are possible in the afternoon."

    # convert to tool
    weather_lookup_tool = ReactTool(
        name="Get the weather",
        fn=weather_lookup_fn,
        description="Returns the weather given a ZIP code.",
    )

    # use tools for request
    result = react(
        m,
        goal="What is today's high temperature in Thornton, NH?",
        state_description=None,
        react_toolbox=ReactToolbox(tools=[zip_lookup_tool, weather_lookup_tool]),
    )

    print("## Final Answer")
    print(result)
For the query What is today’s high temperature in Thornton, NH? the agent outputs the following:
## ReACT TURN NUMBER 1
### Thought
To provide the current weather details for Thornton,
New Hampshire, I first need to obtain its ZIP code
using the "Zip Code Lookup" tool. Once I have the
ZIP code, I will then use the "Get the weather" tool
to find today's high temperature.
### Action
Zip Code Lookup
### Arguments for action
"json
{
  "city": "Thornton, NH"
}"
### Observation
03285
### Done Check
Not done.


## ReACT TURN NUMBER 2
### Thought
I now have Thornton, NH's ZIP code (03285) and need
to use the "Get the weather" tool to find today's
high temperature.

Next step: Utilize the "Get the weather" tool with
the ZIP code for Thornton, New Hampshire.
### Action
Get the weather
### Arguments for action
"json
{
  "zip_code": "03285"
}"
### Observation
The weather in Thornton, NH is sunny with a high of 78
and a low of 52. Scattered showers are possible in
the afternoon.
### Done Check
Done. Will summarize and return output now.

## Final Answer
Today's high temperature in Thornton, New Hampshire is 78 degrees.
The agent looked up the zip code (round 1) and used the zip code to call the weather tool (round 2). The full code can be found in docs/example/agents/react.py on GitHub.

Advanced: Guarded Nondeterminism

Recall Chapter Generative Slots, where we saw how libraries of GenerativeSlot components can be composed by introducing compositionality contracts. We will now build an “agentic” mechanism for automating the task of chaining together possibly-composable generative functions. Let’s get started on our guarded nondeterminism agent (“guarded nondeterminism” is a bit of a mouthful, so we’ll call this a a Kripke agent going forward). This example will be ready soon…