Deterministic AI With OpenSymbolicAI
An agentic solution that follows the PlanAndExecute approach can leverage the power of LLMs while executing deterministic code to provide reliable results.
Join the DZone community and get the full member experience.
Join For FreeWhile AI agents have shifted programming away from deterministic algorithms toward probabilistic LLMs, there remains concern that the lack of determinism makes an agentic solution inherently unreliable. The question comes down to this: Is non-determinism acceptable?
The answer depends on what the solution is for. For creative endeavours such as ideation or writing fiction, non-deterministic responses can be a strength. But I'm sure we can agree that software that relies on precise results, such as those used in finance or scientific research, cannot accept non-determinism.
How to Achieve Deterministic Solutions While Leveraging the AI Agents
While approaches such as RAG and fine-tuned models can help with data accuracy, many solutions call for algorithmic precision that can only come from traditional Software Engineering principles.
OpenSymbolicAI provides a solution to this by implementing the PlanAndExecute paradigm instead of the commonly used the ReAct paradigm.
ReAct (Reason + Acting) is the standard pattern behind most "tool calling" agents. It works through a Thought-Action-Observation loop. In this pattern, the results of any tool call are passed back to the LLM, which then reasons about what to do next.
PlanAndExecute is a different approach. The LLM creates a plan for what to do (specifically, which code to run), then it executes the plan using deterministic code — code that you have written.
OpenSymbolicAI provides an implementation of this approach in Python. Let's see how it works.
Implementation
Install the module using uv:
uv add opensymbolicai-core
Create an agent that defines operations. This example shows a calculator agent that performs mathematical operations.
from opensymbolicai.blueprints import PlanExecute
from opensymbolicai.core import decomposition, primitive
from opensymbolicai.llm import LLMConfig
class Calculator(PlanExecute):
"""A calculator that answers math questions."""
def __init__(self, llm: LLMConfig | None = None) -> None:
if llm is not None:
super().__init__(
llm=llm,
name="Calculator",
description="A calculator that performs mathematical operations.",
)
@primitive(read_only=True)
def add(self, a: float, b: float) -> float:
"""Add two numbers."""
return a + b
@primitive(read_only=True)
def divide(self, a: float, b: float) -> float:
"""Divide a by b."""
return a / b
@decomposition(
intent="What is 10 plus 5?",
expanded_intent="Add 10 and 5",
)
def _add_example(self) -> float:
result = self.add(a=10, b=5)
return result
@decomposition(
intent="What is 15% of 200?",
expanded_intent="Divide percentage by 100, then multiply by value",
)
def _percentage_example(self) -> float:
decimal = self.divide(a=15, b=100)
result = self.multiply(a=decimal, b=200)
return result
Primitives are the basic operations your agent can use. The @primitive decorator exposes a method as a tool that the LLM can call.
Decompositions are examples that teach the agent how to break down complex problems. The LLM uses these to learn the pattern, then applies it to new questions. The @decomposition decorator exposes a method as a pattern that the LLM can apply to handle an intent derived from a prompt.
The Calculator class extends the PlanExecute class provided by the library. This makes its primitives and decompositions available for the LLM to use. The initializer takes an LLMConfig object that will be used to configure the LLM settings, such as the provider and model.
Running the Agent
This example uses Ollama as the LLM provider:
from opensymbolicai.llm import LLMConfig, Provider
from Calculator import Calculator
if __name__ == '__main__':
config = LLMConfig(
provider=Provider.OLLAMA,
model="gpt-oss:20b",
)
question = "What is 20% of 500?"
calc = Calculator(llm=config)
print(f"question = {question}")
response = calc.run(question)
print(f"answer = {response.result}")
Output:
question = What is 20% of 500?
answer = 100.0
When you ask, "What is 20% of 500?", the agent:
- Looks at the decomposition example for percentages
- Divides 20 by 100, then multiplies by 500
- Executes each step using your primitives
- Returns the final answer
Interactive Mode
You can also run agents interactively:
while True:
query = input(">>> ")
if query.lower() == "quit":
break
response = calc.run(query)
print(f"answer = {response.result}")
Interactive output:
>>> what is 20 plus 10?
answer = 30
>>> multiply 15 and 10
answer = 150
Here, I ask a slightly more complex question:
>>> What is 10 plus 5 minus 10?
answer = None
There is no answer, as I haven't defined a primitive for minus (or subtraction). Also, there is no definition for how to handle multiple operations in the same question.
Now, I add a primitive for subtraction and decompositions to allow the LLM to learn how to do subtraction and how to chain multiple operations:
@primitive(read_only=True)
def subtract(self, a: float, b: float) -> float:
"""Subtract two numbers."""
return a - b
@decomposition(
intent="What is 10 minus 5?",
expanded_intent="Subtract 5 from 10",
)
def _subtract_example(self) -> float:
result = self.subtract(a=10, b=5)
return result
@decomposition(
intent="What is 10 plus 5 minus 10?",
expanded_intent="Add 10 and 5 and subtract 10 from the result",
)
def _add_and_subtract_example(self) -> float:
result = self.add(a=10, b=5)
result = self.subtract(a=result, b=10)
return result
I re-run the program and ask the question again. This time, I get the correct answer:
>>> What is 10 plus 5 minus 10?
answer = 5
While this example is very basic, it illustrates the power of an LLM coupled with precise algorithms. Such a solution is testable in the traditional sense — you can expect the same output for the same input. This is something you cannot do when relying solely on the LLM for outputs.
OpenSymbolicAI allows you to trace the plan it follows for execution. This lets you find gaps and fix them through code. It can be used with various LLM providers and also allows integration with RAG. You can use it in conjunction with tools like Claude Code to accelerate development.
For more on OpenSymbolicAI, visit the blog: https://www.opensymbolic.ai/blog. It is open source. Git repo is here: https://github.com/OpenSymbolicAI/core-py.
For more examples, check out: https://github.com/OpenSymbolicAI/examples-py.
Opinions expressed by DZone contributors are their own.
Comments