Symbolic calculus with SymPy
Wrap SymPy as three primitives and let the LLM pick the right operation. SymPy returns exact symbolic answers, not floating-point approximations.
Before you start
SymPy differentiates, integrates, and simplifies expressions symbolically, returning exact answers. The LLM reads the question and picks which operation to apply. SymPy does the math.
Why not just ask the LLM?#
An LLM can recall the derivative of common functions, but it applies rules by pattern matching, not by executing an algorithm. Ask it to differentiate a polynomial with several terms, apply the chain rule to a trigonometric expression, or integrate an expression involving multiple symbolic constants, and small errors appear. The answer looks right, but it is not verified.
SymPy applies differentiation and integration rules algebraically. The result is exact: 3*x**2 + 4*x, not 3.000001*x**2. The gravity problem in this tutorial returns -G*M/r with G and M as symbolic constants, not numeric approximations. There is no rounding, no pattern matching, no guessing.
The agent#
Three primitives: differentiate, integrate, simplify.
# calculus.py
import sympy
from opensymbolicai.blueprints import PlanExecute
from opensymbolicai.core import primitive
class CalculusAgent(PlanExecute):
@primitive(read_only=True)
def differentiate(self, expr: str, variable: str) -> str:
"""Differentiate expr with respect to variable and return the result as a string.
Example: differentiate("x**3 + 2*x**2 - 5", "x") -> "3*x**2 + 4*x"
"""
x = sympy.Symbol(variable)
return str(sympy.diff(sympy.sympify(expr), x))
@primitive(read_only=True)
def integrate(self, expr: str, variable: str) -> str:
"""Integrate expr with respect to variable and return the result as a string.
Example: integrate("3*x**2 + 4*x", "x") -> "x**3 + 2*x**2"
"""
x = sympy.Symbol(variable)
return str(sympy.integrate(sympy.sympify(expr), x))
@primitive(read_only=True)
def simplify(self, expr: str) -> str:
"""Simplify expr and return the result as a string.
Example: simplify("sin(x)**2 + cos(x)**2") -> "1"
"""
return str(sympy.simplify(sympy.sympify(expr)))Each primitive accepts a SymPy expression string, applies the operation, and returns the result as a string. sympy.sympify parses the string; the operation runs symbolically; str() converts the result back for the plan.
Run it#
The same agent handles all four problems. Unlike Z3, SymPy has no accumulating state between calls, so one instance is enough.
# main.py (excerpt)
llm = LLMConfig(provider="ollama", model="qwen2.5-coder:7b")
agent = CalculusAgent(llm=llm)
result = agent.run(
"A ball is thrown upward. Its height is h = -5*t**2 + 20*t + 2. "
"What is the velocity v(t) = dh/dt?"
)
print(result.result) # -10*t + 20uv run main.pyOutput:
[Projectile velocity]
Throw a ball upward and its height follows a parabola. Differentiate to find speed.
result: -10*t + 20
expected: -10*t + 20 [PASS]
[Projectile acceleration]
Differentiate velocity to recover the acceleration due to gravity.
result: -10
expected: -10 [PASS]
[Pendulum velocity]
A pendulum swings back and forth. Its position is a cosine wave; its velocity is a sine wave.
result: -6*sin(2*t)
expected: -6*sin(2*t) [PASS]
[Gravity: force to potential]
Newton's inverse square gravity law integrated gives the 1/r potential.
result: -G*M/r
expected: -G*M/r [PASS]The plan for the first problem is one line:
result = differentiate("-5*t**2 + 20*t + 2", "t")The LLM extracted the expression and the variable from the sentence. SymPy returned the exact derivative.
What to notice#
The LLM picks the operation. "Find the velocity" maps to differentiate. "Compute the antiderivative" maps to integrate. "Simplify sin squared x plus cos squared x" maps to simplify. The primitive signatures make the intent unambiguous.
Variable names come from the expression, not the output. The task asks for "v(t) = dh/dt", but the call is differentiate("-5*t**2 + 20*t + 2", "t"). The variable must be the symbol that appears in the expression ("t"), not the output name ("v"). The decomposition examples in calculus.py show this pattern for polynomial, trigonometric, and integral cases.
Results are exact. The gravity problem integrates G*M/r**2 with respect to r and returns -G*M/r. G and M stay as symbols. A numeric approach would need values plugged in first; the symbolic approach returns the general form directly.