Your code is the prompt
There is no prompt string to write. The primitives you define, their type signatures, and their docstrings are assembled into the prompt automatically. Changing your code changes what the model sees.
Before you start
Most AI frameworks ask you to write a prompt. OpenSymbolicAI does not. You write Python methods. The framework assembles the prompt from them.
This is not an implementation detail. It changes how you think about building agents.
What the model actually receives#
When you call agent.run("what is 7 times 8 minus 3?"), the model receives
something like this:
## Available Primitive Methods
add(a: float, b: float) -> float
"""Add two numbers."""
multiply(a: float, b: float) -> float
"""Multiply two numbers."""
subtract(a: float, b: float) -> float
"""Subtract b from a."""
Generate Python code to accomplish this task: what is 7 times 8 minus 3?No instructions written by hand. No role description. No few-shot examples (yet). Just the signatures and docstrings of the three methods you defined, followed by the task.
Where each part came from#
@primitive(read_only=True)
def multiply(self, a: float, b: float) -> float:
"""Multiply two numbers."""
return a * bThe method name multiply, the parameter names a and b, the types
float, the return type float, and the docstring "Multiply two numbers."
all go into the prompt. The body return a * b does not. The model never
sees it.
So when you write a primitive, you are writing a prompt entry. The name, the types, and the docstring are your interface to the model. The body is your implementation.
Changing the code changes the prompt#
- Rename
multiplytotimesand the model seestimes(a: float, b: float). - Change
a: floattoa: intand the model plans with integers. - Improve the docstring and the model gets a better description.
- Add a new primitive and the model can call it. Remove one and it cannot.
There is no separate prompt file to keep in sync. The code and the prompt are the same thing.
What you never write#
You do not write:
- "You are a calculator agent."
- "You can call the following functions:"
- "Always return valid Python."
The framework handles all of that. The only thing you control is the primitives you define and the docstrings you write on them.
What this means in practice#
When a plan comes out wrong, the first place to look is the primitive definition, not a prompt string. Is the name clear? Is the docstring accurate? Are the types right? Those are the levers.
Type annotations are the contract goes deeper on how types shape the plan. Read the planning prompt shows the full prompt string if you want to inspect it directly.