A primitive that takes and returns a Pydantic model
Define a BaseModel, use it as a primitive param or return type, and it appears automatically under Type Definitions in the plan prompt. The plan reads fields and passes the whole object to the next primitive.
Before you start
The one new thing: a Pydantic BaseModel used as a primitive param or return
type is understood by the framework automatically. The model plans against the
field names and types, reads them with dot notation, and passes the whole object
to the next primitive the same way it passes any other variable.
Define the model and the agent#
# store.py
from pydantic import BaseModel
from opensymbolicai.blueprints import PlanExecute
from opensymbolicai.core import primitive
class Product(BaseModel):
name: str
price: float
in_stock: bool
_INVENTORY = {
"APPLE-01": Product(name="Apple", price=1.20, in_stock=True),
"LAPTOP-01": Product(name="Laptop Pro", price=999.00, in_stock=True),
"CABLE-05": Product(name="USB-C Cable", price=12.50, in_stock=False),
}
class StoreAgent(PlanExecute):
@primitive(read_only=True)
def get_product(self, sku: str) -> Product:
"""Return the Product for a given SKU."""
return _INVENTORY[sku]
@primitive(read_only=True)
def apply_discount(self, product: Product, pct: float) -> Product:
"""Return a new Product with price reduced by pct (0.0-1.0)."""
return Product(
name=product.name,
price=round(product.price * (1 - pct), 2),
in_stock=product.in_stock,
)
@primitive(read_only=True)
def is_available(self, product: Product) -> bool:
"""Return True if the product is in stock."""
return product.in_stock
@primitive(read_only=True)
def fmt_product(self, product: Product) -> str:
"""Format a product as a human-readable string."""
stock = "in stock" if product.in_stock else "out of stock"
return f"{product.name} -- ${product.price:.2f} ({stock})"Product is referenced in three signatures: as the return type of
get_product, and as both a param and return type of apply_discount. That
is enough for the framework to understand its shape. The model sees the field
names and types; it does not see the class body, the inventory dict, or any
values.
Run two tasks#
# main.py
from store import StoreAgent
from opensymbolicai.llm import LLMConfig
TASKS = [
"What is the price of SKU 'LAPTOP-01'?",
"Apply a 15% discount to 'APPLE-01' and show the formatted result.",
]
llm = LLMConfig(provider="ollama", model="qwen2.5-coder:7b")
for task in TASKS:
agent = StoreAgent(llm=llm)
result = agent.run(task)
print(f"Task: {task}")
print(f"Result: {result.result}")
print(f"Plan:")
for line in result.plan.splitlines():
print(f" {line}")
print()uv run main.pyOutput:
Task: What is the price of SKU 'LAPTOP-01'?
Result: 999.0
Plan:
product = get_product('LAPTOP-01')
price = product.price
return price
Task: Apply a 15% discount to 'APPLE-01' and show the formatted result.
Result: Apple -- $1.02 (in stock)
Plan:
product = get_product('APPLE-01')
discounted_product = apply_discount(product, 0.15)
result = fmt_product(discounted_product)
return resultRead the second plan. get_product returns a Product. The model passes the
whole object to apply_discount, which also returns a Product. That result
goes straight into fmt_product. No field is extracted manually; the object
travels intact.
In the first plan, the model does extract a field: product.price. It knows
price is a float from the Product definition. The dot notation works in
the plan the same way it works in Python.
What to notice#
- No registration step.
Productwas never passed to the framework explicitly. Using it as a type annotation was enough. - Field access in the plan is just Python.
product.priceis not a special syntax; it is a normal attribute read on the object thatget_productreturned. - The object passes between primitives whole.
apply_discountreceives the fullProductand returns a new one. The plan does not destructure it into individual arguments; it threads the object through as a variable.