Files
langgraph/examples/llm-compiler/LLMCompiler.ipynb
T
2024-02-09 16:05:51 -08:00

46 KiB
Raw Blame History

LLMCompiler

This notebook shows how to implement LLMCompiler, by Kim, et. al in LangGraph.

LLMCompiler is an agent architecture intented on speeding up the latency of agentic tasks via fast, parallel tool execution. It has 3 main components:

  1. Planner: generate a DAG of tasks.
  2. Task Fetching Unit: schedules and executes the tasks
  3. Joiner: Responds to the user or triggers a second plan

diagram

This notebook walks through each component and shows how to wire them together using LangGraph.

First, install the dependencies, and set up LangSmith for tracing to more easily debug and observe the agent.

In [1]:
# %pip install -U --quiet langchain_openai langsmith langgraph langchain numexpr
In [41]:
import os
import getpass

def _get_pass(var: str):
    if var not in os.environ:
        os.environ[var] = getpass.getpass(f"{var}: ")
# Optional: Debug + trace calls using LangSmith
os.environ["LANGCHAIN_TRACING_V2"] = "True"
os.environ["LANGCHAIN_PROJECT"] = "LLMCompiler"
_get_pass("LANGCHAIN_API_KEY")
_get_pass("OPENAI_API_KEY")

Part 1: Tools

We'll first define the tools for the agent to use in our demo. We'll give it the class search engine + calculator combo.

If you don't want to sign up for tavily, you can replace it with the free DuckDuckGo.

In [42]:
from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
# Imported from the https://github.com/langchain-ai/langgraph/tree/main/examples/plan-and-execute repo
from math_tools import get_math_tool

_get_pass("TAVILY_API_KEY")

calculate = get_math_tool(ChatOpenAI(model="gpt-4-turbo-preview"))
search = TavilySearchResults(max_results=1, description='tavily_search_results_json(query="the search query") - a search engine.')

tools = [search, calculate]
In [43]:
calculate.invoke({"problem": "What's the temp of sf + 5?", "context": ["Thet empreature of sf is 32 degrees"]})
Out [43]:
'37'

Part 2: Planner

Largely adapted from the original source code, the planner accepts the input question and generates a task list to execute.

If it is provided with a previous plan, it is instructed to re-plan, which is useful if, upon completion of the first batch of tasks, the agent must take more actions.

The code below composes constructs the prompt template for the planner and composes it with LLM and output parser, defined in output_parser.py. The output parser processes a task list in the following form:

1. tool_1(arg1="arg1", arg2=3.5, ...)
Thought: I then want to find out Y by using tool_2
2. tool_2(arg1="", arg2="${1}")'
3. join()<END_OF_PLAN>"

The "Thought" lines are optional. The ${#} placeholders are variables. These are used to route tool (task) outputs to other tools.

In [44]:
from typing import Sequence

from langchain_core.language_models import BaseChatModel
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableBranch
from langchain_core.tools import BaseTool
from langchain_core.messages import BaseMessage, FunctionMessage, HumanMessage, SystemMessage

from output_parser import LLMCompilerPlanParser, Task
from langchain import hub
from langchain_openai import ChatOpenAI


prompt = hub.pull("wfh/llm-compiler")
print(prompt.pretty_print())
================================ System Message ================================

Given a user query, create a plan to solve it with the utmost parallelizability. Each plan should comprise an action from the following {num_tools} types:
{tool_descriptions}
{num_tools}. join(): Collects and combines results from prior actions.

 - An LLM agent is called upon invoking join() to either finalize the user query or wait until the plans are executed.
 - join should always be the last action in the plan, and will be called in two scenarios:
   (a) if the answer can be determined by gathering the outputs from tasks to generate the final response.
   (b) if the answer cannot be determined in the planning phase before you execute the plans. Guidelines:
 - Each action described above contains input/output types and description.
    - You must strictly adhere to the input and output types for each action.
    - The action descriptions contain the guidelines. You MUST strictly follow those guidelines when you use the actions.
 - Each action in the plan should strictly be one of the above types. Follow the Python conventions for each action.
 - Each action MUST have a unique ID, which is strictly increasing.
 - Inputs for actions can either be constants or outputs from preceding actions. In the latter case, use the format $id to denote the ID of the previous action whose output will be the input.
 - Always call join as the last action in the plan. Say '<END_OF_PLAN>' after you call join
 - Ensure the plan maximizes parallelizability.
 - Only use the provided action types. If a query cannot be addressed using these, invoke the join action for the next steps.
 - Never introduce new actions other than the ones provided.

============================= Messages Placeholder =============================

{messages}

================================ System Message ================================

Remember, ONLY respond with the task list in the correct format! E.g.:
idx. tool(arg_name=args)
None
In [45]:
def create_planner(llm: BaseChatModel, tools: Sequence[BaseTool], base_prompt: ChatPromptTemplate):
    tool_descriptions = "\n".join(
        f"{i}. {tool.description}\n" for i, tool in enumerate(tools)
    )
    planner_prompt = base_prompt.partial(
        replan="",
        num_tools=len(tools),
        tool_descriptions=tool_descriptions,
    )
    replanner_prompt = base_prompt.partial(
        replan=' - You are given "Previous Plan" which is the plan that the previous agent created along with the execution results '
        "(given as Observation) of each plan and a general thought (given as Thought) about the executed results."
        'You MUST use these information to create the next plan under "Current Plan".\n'
        ' - When starting the Current Plan, you should start with "Thought" that outlines the strategy for the next plan.\n'
        " - In the Current Plan, you should NEVER repeat the actions that are already executed in the Previous Plan.\n"
        " - You must continue the task index from the end of the previous one. Do not repeat task indices.",
        num_tools=len(tools),
        tool_descriptions=tool_descriptions,
    )
    
    def should_replan(state: list):
        # Context is passed as a system message
        return isinstance(state[-1], SystemMessage)

    def wrap_messages(state: list):
        return {"messages": state}

    def wrap_and_get_last_index(state: list):
        next_task = 0
        for message in state[::-1]:
            if isinstance(message, FunctionMessage):
                next_task = message.additional_kwargs["idx"] + 1
                break
        state[-1].content = state[-1].content + f" - Begin counting at : {next_task}"
        return {"messages": state}
        
    return (
        RunnableBranch(
            (should_replan, wrap_and_get_last_index | replanner_prompt),
            wrap_messages | planner_prompt,
        )
        | llm
        | LLMCompilerPlanParser(tools=tools)
    )
In [46]:
llm = ChatOpenAI(model="gpt-4-turbo-preview")
# This is the primary "agent" in our application
planner = create_planner(llm, tools, prompt)
In [47]:
example_question = "What's the temperature in SF raised to the 3rd power?"

for task in planner.stream([HumanMessage(content=example_question)]):
    print(task['tool'], task['args'])
    print('---')
description='tavily_search_results_json(query="the search query") - a search engine.' max_results=1 {'query': 'current temperature in San Francisco'}
---
name='math' description='math(problem: str, context: Optional[List[str]] = None, config: Optional[langchain_core.runnables.config.RunnableConfig] = None) - math(problem: str, context: Optional[list[str]]) -> float:\n - Solves the provided math problem.\n - `problem` can be either a simple math problem (e.g. "1 + 3") or a word problem (e.g. "how many apples are there if there are 3 apples and 2 apples").\n - You cannot calculate multiple expressions in one call. For instance, `math(\'1 + 3, 2 + 4\')` does not work. If you need to calculate multiple expressions, you need to call them separately like `math(\'1 + 3\')` and then `math(\'2 + 4\')`\n - Minimize the number of `math` actions as much as possible. For instance, instead of calling 2. math("what is the 10% of $1") and then call 3. math("$1 + $2"), you MUST call 2. math("what is the 110% of $1") instead, which will reduce the number of math actions.\n - You can optionally provide a list of strings as `context` to help the agent solve the problem. If there are multiple contexts you need to answer the question, you can provide them as a list of strings.\n - `math` action will not see the output of the previous actions unless you provide it as `context`. You MUST provide the output of the previous actions as `context` if you need to do math on it.\n - You MUST NEVER provide `search` type action\'s outputs as a variable in the `problem` argument. This is because `search` returns a text blob that contains the information about the entity, not a number or value. Therefore, when you need to provide an output of `search` action, you MUST provide it as a `context` argument to `math` action. For example, 1. search("Barack Obama") and then 2. math("age of $1") is NEVER allowed. Use 2. math("age of Barack Obama", context=["$1"]) instead.\n - When you ask a question about `context`, specify the units. For instance, "what is xx in height?" or "what is xx in millions?" instead of "what is xx?"' args_schema=<class 'pydantic.v1.main.mathSchema'> func=<function get_math_tool.<locals>.calculate_expression at 0x119a318a0> {'problem': 'pow($0, 3)', 'context': ['$0']}
---
join ()
---

3. Task Fetching Unit

This component schedules the tasks. It receives a stream of tools of the following format:

{
    tool: BaseTool,
    dependencies: number[],
}

The basic idea is to begin executing tools as soon as their dependencies are met. This is done through multi-threading.

In [48]:
from typing import Any, Union, Iterable, List, Tuple, Dict
from typing_extensions import TypedDict

from langchain_core.runnables import (
    chain as as_runnable,
)

from concurrent.futures import ThreadPoolExecutor, wait
import time


def _get_observations(messages: List[BaseMessage]) -> Dict[int, Any]:
    # Get all previous tool responses
    results = {}
    for message in messages[::-1]:
        if isinstance(message, FunctionMessage):
            results[int(message.additional_kwargs["idx"])] = message.content
    return results

class SchedulerInput(TypedDict):
    messages: List[BaseMessage]
    tasks: Iterable[Task]


def _execute_task(task, observations, config):
    tool_to_use = task["tool"]
    if isinstance(tool_to_use, str):
        return tool_to_use
    args = task["args"]
    try:
        if isinstance(args, str):
            resolved_args = _resolve_arg(args, observations)
        elif isinstance(args, dict):
            resolved_args = {key: _resolve_arg(val, observations) for key, val in args.items()}
        else:
            # This will likely fail
            resolved_args = args
    except Exception as e:
        return (
            f"ERROR(Failed to call {tool_to_use.name} with args {args}.)"
            f" Args could not be resolved. Error: {repr(e)}"
        )
    try:
        return tool_to_use.invoke(resolved_args, config)
    except Exception as e:
        return (
            f"ERROR(Failed to call {tool_to_use.name} with args {args}."
            + f" Args resolved to {resolved_args}. Error: {repr(e)})"
        )


def _resolve_arg(arg: Union[str, Any], observations: Dict[int, Any]):
    if isinstance(arg, str) and arg.startswith("$"):
        try:
            stripped = arg[1:].replace(".output", "").strip("{}")
            idx = int(stripped)
        except Exception:
            return str(arg)
        return str(observations[idx])
    elif isinstance(arg, list):
        return [_resolve_arg(a, observations) for a in arg]
    else:
        return str(arg)


@as_runnable
def schedule_task(task_inputs, config):
    task: Task = task_inputs['task']
    observations: Dict[int, Any] = task_inputs['observations']
    try:
        observation = _execute_task(task, observations, config)
    except Exception:
        import traceback
        observation = traceback.format_exception() #repr(e) + 
    observations[task['idx']] = observation

def schedule_pending_task(task: Task, observations: Dict[int, Any], retry_after: float = 0.2):
    while True:
        deps = task["dependencies"]
        if (
            deps
            and (
                any([dep not in observations for dep in deps])
            )
        ):
            # Dependencies not yet satisfied
            time.sleep(retry_after)
            continue
        schedule_task.invoke({"task": task, "observations": observations})
        break

@as_runnable
def schedule_tasks(scheduler_input: SchedulerInput) -> List[FunctionMessage]:
    """Group the tasks into a DAG schedule."""
    # For streaming, we are making a few simplifying assumption:
    # 1. The LLM does not create cyclic dependencies
    # 2. That the LLM will not generate tasks with future deps
    # If this ceases to be a good assumption, you can either
    # adjust to do a proper topological sort (not-stream)
    # or use a more complicated data structure
    tasks = scheduler_input["tasks"]
    messages = scheduler_input["messages"]
    # If we are re-planning, we may have calls that depend on previous
    # plans. Start with those.
    observations = _get_observations(messages)
    task_names = {}
    originals = set(observations)
    # ^^ We assume each task inserts a different key above to
    # avoid race conditions...
    futures = []
    retry_after = 0.25 # Retry every quarter second
    with ThreadPoolExecutor() as executor:
        for task in tasks:
            deps = task["dependencies"]
            task_names[task["idx"]] = task["tool"] if isinstance(task["tool"], str) else task["tool"].name
            if (
                # Depends on other tasks
                deps
                and (
                    any([dep not in observations for dep in deps])
                )
            ):
                futures.append(executor.submit(schedule_pending_task, task, observations, retry_after))
            else:
                # No deps or all deps satisfied
                # can schedule now
                schedule_task.invoke(dict(task=task, observations=observations))
                # futures.append(executor.submit(schedule_task.invoke dict(task=task, observations=observations)))

        # All tasks have been submitted or enqueued
        # Wait for them to complete
        wait(futures)
    # Convert observations to new tool messages to add to the state
    new_observations = {k: (task_names[k], observations[k]) for k in sorted(observations.keys() - originals)}
    tool_messages = [
            FunctionMessage(
                name=name,
                content=str(obs),
                additional_kwargs={"idx": k}
        ) for k, (name, obs) in new_observations.items()]
    return tool_messages
In [49]:
import itertools

@as_runnable
def plan_and_schedule(messages: List[BaseMessage], config):
    tasks = planner.stream(messages, config)
    # Begin executing the planner immediately
    tasks = itertools.chain([next(tasks)], tasks)
    scheduled_tasks = schedule_tasks.invoke({
        "messages": messages,
        "tasks": tasks,
    }, config)
    return scheduled_tasks

Example Plan

We still haven't introduced any cycles in our computation graph, so this is all easily expressed in LCEL.

In [50]:
tool_messages = plan_and_schedule.invoke([HumanMessage(content=example_question)])
In [51]:
tool_messages
Out [51]:
[FunctionMessage(content="[{'url': 'https://en.climate-data.org/north-america/united-states-of-america/california/san-francisco-385/t/september-9/', 'content': 'San Francisco Weather in September  San Francisco weather in September San Francisco weather by month // weather averages 9.6 (49.2) 6.2 (43.2) 14 (57.3)  Data: 1999 - 2019: avg. Sun hours San Francisco weather and climate for further months  San Francisco weather in September // weather averages Airport close to San FranciscoJanuary February March April May June July August September October November December; Avg. Temperature °C (°F) 9.6 °C (49.2) °F. 10.5 °C (50.8) °F. 11.6 °C'}]", additional_kwargs={'idx': 1}, name='tavily_search_results_json'),
 FunctionMessage(content='1', additional_kwargs={'idx': 2}, name='math'),
 FunctionMessage(content='join', additional_kwargs={'idx': 3}, name='join')]

4. "Joiner"

So now we have the planning and initial execution done. We need a component to process these outputs and either:

  1. Respond with the correct answer.
  2. Loop with a new plan.

The paper refers to this as the "joiner". It's another LLM call. We are using function calling to improve parsing reliability.

In [52]:
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain.chains.openai_functions import create_structured_output_runnable
from langchain_core.messages import AIMessage

class FinalResponse(BaseModel):
    """The final response/answer."""
    response: str

class Replan(BaseModel):
    feedback: str = Field(description="Analysis of the previous attempts and recommendations on what needs to be fixed.")

class JoinOutputs(BaseModel):
    """Decide whether to replan or whether you can return the final response."""
    thought: str = Field(description="The chain of thought reasoning for the selected action")
    action: Union[FinalResponse, Replan]


joiner_prompt = hub.pull("wfh/llm-compiler-joiner").partial(examples="") # You can optionally add examples
llm = ChatOpenAI(model="gpt-4-turbo-preview")

runnable = create_structured_output_runnable(JoinOutputs, llm, joiner_prompt)

We will select only the most recent messages in the state, and format the output to be more useful for the planner, should the agent need to loop.

In [ ]:
def _parse_joiner_output(decision: JoinOutputs) -> List[BaseMessage]:
    response = [AIMessage(content=f"Thought: {decision.thought}")]
    if isinstance(decision.action, Replan):
        return response + [SystemMessage(content=f"Context from last attempt: {decision.action.feedback}")]
    else:
        return response + [AIMessage(content=decision.action.response)]


def select_recent_messages(messages: list) -> dict:
    selected = []
    for msg in messages[::-1]:
        selected.append(msg)
        if isinstance(msg, HumanMessage):
            break
    return {"messages": selected[::-1]}

joiner = (
    select_recent_messages
    | runnable
    | _parse_joiner_output
)
In [53]:
input_messages = [HumanMessage(content=example_question)] + tool_messages
In [54]:
joiner.invoke(input_messages)
Out [54]:
[AIMessage(content="Thought: The information provided gives an average temperature for San Francisco in different months, but it doesn't specify the current temperature or any specific temperature to be raised to the 3rd power. Without the current temperature or a specific temperature value, it's impossible to calculate its value raised to the 3rd power."),
 SystemMessage(content='Context from last attempt: The information provided is not sufficient to answer the question as it lacks the current temperature of San Francisco or any specific temperature value to be raised to the 3rd power. Need to find the current or a specific temperature to perform the calculation.')]

5. Compose using LangGraph

We'll define the agent as a stateful graph, with the main nodes being:

  1. Plan and execute (the DAG from the first step above)
  2. Join: determine if we should finish or replan
  3. Recontextualize: update the graph state based on the output from the joiner
In [55]:
from langgraph.graph import MessageGraph, END
from typing import Dict

graph_builder = MessageGraph()

# 1.  Define vertices
# We defined plan_and_schedule above already
# Assign each node to a state variable to update
graph_builder.add_node("plan_and_schedule", plan_and_schedule)
graph_builder.add_node("join", joiner)


## Define edges
graph_builder.add_edge("plan_and_schedule", "join")

### This condition determines looping logic


def should_continue(state: List[BaseMessage]):
    if isinstance(state[-1], AIMessage):
        return END
    return "plan_and_schedule"

graph_builder.add_conditional_edges(
    start_key="join",
    # Next, we pass in the function that will determine which node is called next.
    condition=should_continue,
)
graph_builder.set_entry_point("plan_and_schedule")
chain = graph_builder.compile()

Simple question

Let's ask a simple question of the agent.

In [56]:
for step in chain.stream([HumanMessage(content="What's the GDP of New York?")]):
    print(step)
    print('---')
{'plan_and_schedule': [FunctionMessage(content='[{\'url\': \'https://www.statista.com/statistics/188087/gdp-of-the-us-federal-state-of-new-york-since-1997/\', \'content\': "Strategy and business building for the data-driven economy: U.S. real GDP of New York 2000-2022  Real gross domestic product of New York in the United States from 2000 to 2022 (in billion U.S. dollars)  Economy U.S. New York metro area GDP 2001-2022 You only have access to basic statistics.  U.S. state and local government outstanding debt 2021, by state Demographics Resident population in New York 1960-2022In 2022, the real gross domestic product (GDP) of New York was about 1.56 trillion U.S. dollars. This is an increase from the previous year, when the state\'s GDP stood at 1.51 trillion..."}]', additional_kwargs={'idx': 0}, name='tavily_search_results_json')]}
---
{'join': [AIMessage(content='Thought: The search result provides the information that in 2022, the real gross domestic product (GDP) of New York was about 1.56 trillion U.S. dollars.'), AIMessage(content='The GDP of New York in 2022 was about 1.56 trillion U.S. dollars.')]}
---
{'__end__': [HumanMessage(content="What's the GDP of New York?"), FunctionMessage(content='[{\'url\': \'https://www.statista.com/statistics/188087/gdp-of-the-us-federal-state-of-new-york-since-1997/\', \'content\': "Strategy and business building for the data-driven economy: U.S. real GDP of New York 2000-2022  Real gross domestic product of New York in the United States from 2000 to 2022 (in billion U.S. dollars)  Economy U.S. New York metro area GDP 2001-2022 You only have access to basic statistics.  U.S. state and local government outstanding debt 2021, by state Demographics Resident population in New York 1960-2022In 2022, the real gross domestic product (GDP) of New York was about 1.56 trillion U.S. dollars. This is an increase from the previous year, when the state\'s GDP stood at 1.51 trillion..."}]', additional_kwargs={'idx': 0}, name='tavily_search_results_json'), AIMessage(content='Thought: The search result provides the information that in 2022, the real gross domestic product (GDP) of New York was about 1.56 trillion U.S. dollars.'), AIMessage(content='The GDP of New York in 2022 was about 1.56 trillion U.S. dollars.')]}
---
In [57]:
# Final answer
print(step[END][-1].content)
The GDP of New York in 2022 was about 1.56 trillion U.S. dollars.

Multi-hop question

This question requires that the agent perform multiple searches.

In [58]:
steps = chain.stream(
    [HumanMessage(content="What's the oldest parrot alive, and how much longer is that than the average?")],
    {
        "recursion_limit": 100,
    },
)
for step in steps:
    print(step)
    print('---')
{'plan_and_schedule': [FunctionMessage(content='[{\'url\': \'https://savetheeaglesinternational.org/old-parrot/\', \'content\': "What Is The World\'s Oldest Parrot?  Living Long and Healthy Lives: A Look at the Worlds Oldest Parrots  certain parrot species are considered older:  your parrot enters its senior years?One remarkable parrot that defied the odds and lived a long life is Cookie, a cockatoo who reached the impressive age of 83. Cookie spent his entire life at the Brookfield Zoo, serving as a testament to the exceptional care and environment provided by the zookeepers."}]', additional_kwargs={'idx': 1}, name='tavily_search_results_json'), FunctionMessage(content="[{'url': 'https://www.animalwised.com/how-long-does-a-parrot-live-3974.html', 'content': 'How Long Does a Parrot Live?  How long does a parrot live in captivity?  How long does a parrot live in the wild?  Why do parrots live so long?Below is the average life expectancy of parrots in captivity, based on their species. Lovebirds. Lovebirds are members of the genus Agapornis, a small group of parrots in the parrot family Psittaculidae. The average life expectancy of a lovebird is between 12 and 15 years. Depending on care and circumstances, the bird can live up to 20 years ...'}]", additional_kwargs={'idx': 2}, name='tavily_search_results_json'), FunctionMessage(content='join', additional_kwargs={'idx': 3}, name='join')]}
---
{'join': [AIMessage(content="Thought: The oldest parrot ever recorded is Cookie, a cockatoo, who lived to be 83 years old. However, the average lifespan provided is specifically for lovebirds, which is between 12 and 15 years. This information doesn't accurately reflect the average lifespan of all parrot species, which would be necessary to compare with Cookie's age accurately. Since parrots encompass a wide variety of species with different lifespans, the information on lovebirds' lifespan alone is insufficient for a comprehensive comparison."), SystemMessage(content="Context from last attempt: We need information on the average lifespan of parrots in general, not just lovebirds, to accurately compare with Cookie's age.")]}
---
{'plan_and_schedule': [FunctionMessage(content='[{\'url\': \'https://www.petmd.com/bird/how-long-do-parrots-live\', \'content\': "Average Parrot Lifespan and Aging  How Long Do Parrots Live?  How to Improve Your Parrot\'s Lifespan  Mcleod DVM, Lianne. The Spruce Pets. How Long do Pet Parrots and Other Birds Live?. 2023.Some pets, such as tortoises and parrots, may live for over 50 years. Because they are a lifelong commitment, lawyers often urge pet parents to provide documented plans for their pet parrots in their wills. Average Parrot Lifespan and Aging. Parrots are an incredibly diverse group of birds known by their scientific name: psittacines."}]', additional_kwargs={'idx': 4}, name='tavily_search_results_json')]}
---
{'join': [AIMessage(content="Thought: The information provided does not give a specific average lifespan for parrots in general, which is necessary for accurately comparing Cookie's age to the average lifespan of parrots. The search result mentions that parrots can live over 50 years but does not provide a detailed average lifespan applicable to all or most parrot species."), SystemMessage(content="Context from last attempt: We need information on the average lifespan of parrots in general to accurately compare with Cookie's age of 83 years. The provided information doesn't specify an average lifespan for parrots as a whole.")]}
---
{'plan_and_schedule': [FunctionMessage(content='join', additional_kwargs={'idx': 5}, name='join')]}
---
{'join': [AIMessage(content="Thought: Despite multiple attempts, the specific average lifespan of parrots as a whole has not been provided. The information obtained mentions that parrots can live over 50 years, but a more precise average is necessary for a detailed comparison with Cookie's age of 83 years. However, it's clear that Cookie lived significantly longer than the average lifespan of many parrot species, including lovebirds which have an average lifespan of 12 to 15 years."), AIMessage(content="The oldest parrot on record is Cookie, a cockatoo, who lived to be 83 years old. While specific average lifespan information for all parrot species has not been provided, it's mentioned that some parrots can live over 50 years. This suggests that Cookie lived significantly longer than the average lifespan for many parrot species. For instance, lovebirds, a type of parrot, have an average lifespan of 12 to 15 years, indicating that Cookie's lifespan was exceptional among parrots.")]}
---
{'__end__': [HumanMessage(content="What's the oldest parrot alive, and how much longer is that than the average?"), FunctionMessage(content='[{\'url\': \'https://savetheeaglesinternational.org/old-parrot/\', \'content\': "What Is The World\'s Oldest Parrot?  Living Long and Healthy Lives: A Look at the Worlds Oldest Parrots  certain parrot species are considered older:  your parrot enters its senior years?One remarkable parrot that defied the odds and lived a long life is Cookie, a cockatoo who reached the impressive age of 83. Cookie spent his entire life at the Brookfield Zoo, serving as a testament to the exceptional care and environment provided by the zookeepers."}]', additional_kwargs={'idx': 1}, name='tavily_search_results_json'), FunctionMessage(content="[{'url': 'https://www.animalwised.com/how-long-does-a-parrot-live-3974.html', 'content': 'How Long Does a Parrot Live?  How long does a parrot live in captivity?  How long does a parrot live in the wild?  Why do parrots live so long?Below is the average life expectancy of parrots in captivity, based on their species. Lovebirds. Lovebirds are members of the genus Agapornis, a small group of parrots in the parrot family Psittaculidae. The average life expectancy of a lovebird is between 12 and 15 years. Depending on care and circumstances, the bird can live up to 20 years ...'}]", additional_kwargs={'idx': 2}, name='tavily_search_results_json'), FunctionMessage(content='join', additional_kwargs={'idx': 3}, name='join'), AIMessage(content="Thought: The oldest parrot ever recorded is Cookie, a cockatoo, who lived to be 83 years old. However, the average lifespan provided is specifically for lovebirds, which is between 12 and 15 years. This information doesn't accurately reflect the average lifespan of all parrot species, which would be necessary to compare with Cookie's age accurately. Since parrots encompass a wide variety of species with different lifespans, the information on lovebirds' lifespan alone is insufficient for a comprehensive comparison."), SystemMessage(content="Context from last attempt: We need information on the average lifespan of parrots in general, not just lovebirds, to accurately compare with Cookie's age. - Begin counting at : 4"), FunctionMessage(content='[{\'url\': \'https://www.petmd.com/bird/how-long-do-parrots-live\', \'content\': "Average Parrot Lifespan and Aging  How Long Do Parrots Live?  How to Improve Your Parrot\'s Lifespan  Mcleod DVM, Lianne. The Spruce Pets. How Long do Pet Parrots and Other Birds Live?. 2023.Some pets, such as tortoises and parrots, may live for over 50 years. Because they are a lifelong commitment, lawyers often urge pet parents to provide documented plans for their pet parrots in their wills. Average Parrot Lifespan and Aging. Parrots are an incredibly diverse group of birds known by their scientific name: psittacines."}]', additional_kwargs={'idx': 4}, name='tavily_search_results_json'), AIMessage(content="Thought: The information provided does not give a specific average lifespan for parrots in general, which is necessary for accurately comparing Cookie's age to the average lifespan of parrots. The search result mentions that parrots can live over 50 years but does not provide a detailed average lifespan applicable to all or most parrot species."), SystemMessage(content="Context from last attempt: We need information on the average lifespan of parrots in general to accurately compare with Cookie's age of 83 years. The provided information doesn't specify an average lifespan for parrots as a whole. - Begin counting at : 5"), FunctionMessage(content='join', additional_kwargs={'idx': 5}, name='join'), AIMessage(content="Thought: Despite multiple attempts, the specific average lifespan of parrots as a whole has not been provided. The information obtained mentions that parrots can live over 50 years, but a more precise average is necessary for a detailed comparison with Cookie's age of 83 years. However, it's clear that Cookie lived significantly longer than the average lifespan of many parrot species, including lovebirds which have an average lifespan of 12 to 15 years."), AIMessage(content="The oldest parrot on record is Cookie, a cockatoo, who lived to be 83 years old. While specific average lifespan information for all parrot species has not been provided, it's mentioned that some parrots can live over 50 years. This suggests that Cookie lived significantly longer than the average lifespan for many parrot species. For instance, lovebirds, a type of parrot, have an average lifespan of 12 to 15 years, indicating that Cookie's lifespan was exceptional among parrots.")]}
---
In [59]:
# Final answer
print(step[END][-1].content)
The oldest parrot on record is Cookie, a cockatoo, who lived to be 83 years old. While specific average lifespan information for all parrot species has not been provided, it's mentioned that some parrots can live over 50 years. This suggests that Cookie lived significantly longer than the average lifespan for many parrot species. For instance, lovebirds, a type of parrot, have an average lifespan of 12 to 15 years, indicating that Cookie's lifespan was exceptional among parrots.

Multi-step math

In [60]:
for step in chain.stream([HumanMessage(content="What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?")]):
    print(step)
{'plan_and_schedule': [FunctionMessage(content='3307.0', additional_kwargs={'idx': 0}, name='math'), FunctionMessage(content='7.565011820330969', additional_kwargs={'idx': 1}, name='math'), FunctionMessage(content='join', additional_kwargs={'idx': 2}, name='join')]}
{'join': [AIMessage(content='Thought: The first calculation resulted in 3307.0, and the second calculation gave 7.565011820330969. To find the sum of these two values, I will simply add them together.'), AIMessage(content='The sum of ((3*(4+5)/0.5)+3245) + 8 and 32/4.23 is approximately 3314.565.')]}
{'__end__': [HumanMessage(content="What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?"), FunctionMessage(content='3307.0', additional_kwargs={'idx': 0}, name='math'), FunctionMessage(content='7.565011820330969', additional_kwargs={'idx': 1}, name='math'), FunctionMessage(content='join', additional_kwargs={'idx': 2}, name='join'), AIMessage(content='Thought: The first calculation resulted in 3307.0, and the second calculation gave 7.565011820330969. To find the sum of these two values, I will simply add them together.'), AIMessage(content='The sum of ((3*(4+5)/0.5)+3245) + 8 and 32/4.23 is approximately 3314.565.')]}
In [61]:
# Final answer
print(step[END][-1].content)
The sum of ((3*(4+5)/0.5)+3245) + 8 and 32/4.23 is approximately 3314.565.