All tutorials
Track 24·Data & Types

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.

intermediate8 min
Video coming soon
Browse this tutorial's folder in tutorials-pygithub.com/OpenSymbolicAI/tutorials-py/tree/main/24-pydantic-model

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#

python
# 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#

python
# 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()
bash
uv run main.py

Output:

text
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 result

Read 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. Product was never passed to the framework explicitly. Using it as a type annotation was enough.
  • Field access in the plan is just Python. product.price is not a special syntax; it is a normal attribute read on the object that get_product returned.
  • The object passes between primitives whole. apply_discount receives the full Product and returns a new one. The plan does not destructure it into individual arguments; it threads the object through as a variable.