Multi-turn conversations with multi_turn=True
Set multi_turn=True and call agent.run() multiple times on the same agent. State held in instance variables persists across turns. The model receives the conversation history on each turn.
Before you start
Every previous tutorial calls agent.run() once and reads the result. The one
new thing: set multi_turn=True in PlanExecuteConfig and call agent.run()
multiple times on the same agent instance. The model receives the full
conversation history on each turn. State held in instance variables persists
because the agent object is reused.
The agent#
A shopping cart whose state lives in self._cart, an instance variable the
model never sees.
# cart.py
from pydantic import BaseModel
from opensymbolicai.blueprints import PlanExecute
from opensymbolicai.core import primitive
from opensymbolicai.models import PlanExecuteConfig
class Item(BaseModel):
price: float
quantity: int
class Cart(BaseModel):
items: dict[str, Item] = {}
class ShoppingCart(PlanExecute):
def __init__(self, llm, config=None) -> None:
super().__init__(llm=llm, config=config)
self._cart = Cart()
@primitive(read_only=False)
def add_item(self, name: str, price: float, quantity: int) -> str:
"""Add an item to the cart by name, unit price, and quantity."""
self._cart = Cart(items={**self._cart.items,
name: Item(price=price, quantity=quantity)})
return f"added {name}"
@primitive(read_only=False)
def remove_item(self, name: str) -> str:
"""Remove an item from the cart by name."""
self._cart = Cart(items={k: v for k, v in self._cart.items.items()
if k != name})
return f"removed {name}"
@primitive(read_only=True)
def cart_total(self) -> float:
"""Return the total cost of all items in the cart."""
return sum(item.price * item.quantity
for item in self._cart.items.values())
@primitive(read_only=True)
def list_items(self) -> list[str]:
"""Return the names of all items currently in the cart."""
return list(self._cart.items.keys())There is no new_cart primitive. The cart is created in __init__ and mutated
in place. The model never sees the cart object; it just calls add_item and
remove_item by name and price.
Four turns on one agent#
# main.py
from opensymbolicai.llm import LLMConfig
from opensymbolicai.models import PlanExecuteConfig
TASKS = [
"Add to the cart: milk at $2.50, eggs at $3.99, bread at $4.50, "
"butter at $5.99, yogurt at $1.99, and cheese at $6.50.",
"Add to the cart: chicken at $8.99, pasta at $2.25, tomatoes at $3.50, "
"garlic at $1.50, olive oil at $7.99, and onions at $1.25.",
"Remove eggs and butter from the cart.",
"What is the total?",
]
llm = LLMConfig(provider="ollama", model="qwen2.5-coder:7b")
config = PlanExecuteConfig(multi_turn=True)
agent = ShoppingCart(llm=llm, config=config)
for task in TASKS:
result = agent.run(task)
print(f"Task: {task}")
print(f"Plan: {result.plan}")
print(f"Result: {result.result}")
print()uv run main.pyOutput:
Task: Add to the cart: milk at $2.50, eggs at $3.99 ...
Plan:
r1 = add_item('milk', 2.50, 1)
r2 = add_item('eggs', 3.99, 1)
r3 = add_item('bread', 4.50, 1)
r4 = add_item('butter', 5.99, 1)
r5 = add_item('yogurt', 1.99, 1)
r6 = add_item('cheese', 6.50, 1)
return r1 + ', ' + r2 + ', ' + r3 + ', ' + r4 + ', ' + r5 + ', ' + r6
Result: added milk, added eggs, added bread, added butter, added yogurt, added cheese
Task: Add to the cart: chicken at $8.99, pasta at $2.25 ...
Plan:
r1 = add_item('chicken', 8.99, 1)
...
Result: added chicken, added pasta, added tomatoes, added garlic, added olive oil, added onions
Task: Remove eggs and butter from the cart.
Plan:
r1 = remove_item('eggs')
r2 = remove_item('butter')
return r1 + ', ' + r2
Result: removed eggs, removed butter
Task: What is the total?
Plan:
total = cart_total()
return total
Result: 40.97What to notice#
- No cart variable in any plan. The model calls
add_item('milk', 2.50, 1)and gets back"added milk". It never holds a reference to the cart. State lives inself._cart, which persists because the agent object is reused. - Turn 3 knows what was added in turns 1 and 2.
multi_turn=Trueputs the conversation history in the prompt, so the model knows eggs and butter exist to remove. - The total reflects all four turns. Turn 4 calls
cart_total()with no arguments. The right answer comes back becauseself._cartaccumulated everything from the previous turns. - Two separate instances have separate carts. Create a second
ShoppingCartand itsself._cartstarts empty. There is no shared state between instances.