Open-source project
ExtensityAI/symbolicai avatar
ExtensityAI/symbolicai

SymbolicAI: Contracts and Semantic Projections on Top of Python Values

A neurosymbolic perspective on LLMs

1,757 stars95 forksPythonBSD-3-Clause

At a glance

What is it?
SymbolicAI wraps ordinary Python values in a Symbol object that can switch between literal and LLM-backed behaviour, and adds a contract decorator that retries bad inputs and outputs. It is a framework for people who want probabilistic behaviour inside typed Python, not a drop-in replacement for a plain LLM client.
Who is it for?
Adopt SymbolicAI if you are building Python code where LLM calls must sit behind typed data models and you want retries, remedies and error accumulation handled by a decorator rather than by hand. Do not adopt it if you need a thin, predictable API client, or if you cannot accept that the same operator can mean two different things depending on which projection the object is in.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 7 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem SymbolicAI targets: LLM calls that live outside your types

Most Python code that calls a language model does so at the edges. You build a string, send it, parse whatever comes back, and hope the parse holds. The type system stops at the API boundary. SymbolicAI moves that boundary. It introduces a Symbol object that behaves like the value you passed in, and a contract decorator that binds an LLM-backed expression to a Pydantic-compatible data model. The README frames the second half plainly: LLMs hallucinate, but code cannot afford to, so the project brings design-by-contract principles into LLM work. The intended audience is Python developers who already think in classes, fields and validators, and who want the model's output to be checked by the same machinery that checks everything else. It is not aimed at people who want a minimal HTTP wrapper, and the README does not present it as one.

Syntactic by default, semantic on request: how Symbol avoids surprising side effects

The design decision that shapes everything else is that Symbol is syntactic by default. Python operators are overloaded in symai, so if the engine fired on every comparison or bitshift, ordinary code would be slow and could produce side effects the caller never asked for. A syntactic Symbol behaves like a normal Python value. A semantic Symbol is wired to the neuro-symbolic engine and understands meaning. The README gives three ways to cross over. You can construct with semantic=True, you can project with .sem and flip back with .syn, or you can call a dot-notation operation such as .map(), which switches the symbol automatically. The projections return the same underlying object with a different behavioural coat, which is what makes chained syntactic and semantic operations on one symbol possible. The README's own example is short and worth reading closely: a Symbol holding a list of fruits and animals, passed to .map('convert all fruits to vegetables'), returns a list where the fruits are replaced and the animals are untouched. That example is also the clearest statement of the risk. The same operator means different things depending on the projection, and the caller has to track which one is active.

What the primitives table actually promises

The README includes a table of primitives and operators with separate syntactic and semantic columns. Comparison, arithmetic, logical and bitwise operators, item assignment and .startswith all carry a check in both columns. Semantic equality is described as fuzzy or conceptual equivalence, with 'Hi' == 'Hello' as the example. Semantic addition is described as meaningful composition or conceptual merge. Semantic & is described as logical conjunction or context merge. A second group has no syntactic column at all: .choice(cases, default) selects the best match from provided cases, .foreach(condition, apply) applies an action to each element, .cluster groups data semantically, and .similarity computes similarity between embeddings. Two details in that table matter more than the rest. The cluster primitive is documented as using scikit-learn's HDBSCAN and as requiring the cluster extra, installed as symbolicai[cluster]. And .similarity takes parameters for metric and normalization, which means the comparison is not left entirely to the model. The table is also explicitly truncated in the README, which points to the documentation for the full set. Treat the table as a sample, not an inventory.

Contracts: models, validators and remedies in one decorator

The contract decorator is the part of the project with the clearest engineering argument behind it. You define a data model that inherits from LLMDataModel, which the README states is compatible with Pydantic's BaseModel. Fields carry descriptions, and a field_validator can reject values that fall outside an allowed set; the README's example raises a ValueError listing the valid options and echoing the offending value. The decorator then wraps an Expression class. Its documented parameters are pre_remedy, post_remedy, accumulate_errors, verbose and a fifth whose name is cut off in the supplied text. The README's comments describe them: pre_remedy tries to fix bad inputs automatically, post_remedy tries to fix bad LLM outputs automatically, accumulate_errors feeds the history of errors into each retry, and verbose displays progress in the terminal. The README also states that field descriptions power validation, automatic prompt templating and remedies. That last claim is the interesting one, because it means the model's instructions and the validation rules are generated from the same declaration. If the two ever diverge in your head, they diverge in the prompt as well.

Engines, hosting and the installation surface

SymbolicAI is designed to be extended at the engine layer. The README links to documentation for writing a custom engine, hosting a local engine, and interfacing with a search engine or a drawing engine for image generation. It describes the design as modular, which in practice means the choice of backend is yours and the framework does not settle it for you. That is a real advantage for anyone who needs to run against a local model, and a real cost for anyone who wants the framework to make the decision. The README does not state which engine is used by default, and the supplied material does not include the installation commands, so the exact pip invocation cannot be quoted here. What is confirmable is the extras pattern: the cluster primitive is documented as requiring symbolicai[cluster]. If your work depends on .cluster, that extra is part of your install, not an optional nicety. The repository is BSD-3-Clause, which permits commercial use and modification; this is a description of the licence identifier, not legal advice, and you should read the licence text before relying on it.

Where the abstraction gets in the way

The syntactic-by-default choice is a good one, but it creates a class of bug the framework cannot prevent. A symbol can be syntactic in one place and semantic in another, and the README's own example shows that 'feline' in S is False while 'feline' in S.sem is True. Nothing in the type signature distinguishes the two cases. A function that accepts a Symbol and tests membership will behave differently depending on what the caller did upstream. That is an unusual failure mode for Python code, and it is the price of overloading operators for meaning. The contract decorator has a related cost. Remedies retry, and retries call the model again. A contract with both pre_remedy and post_remedy enabled can produce several model calls for one logical operation, and the README does not state a retry ceiling in the supplied material. If you are counting tokens or latency, that is a parameter you need to find in the documentation before you ship. There is also a version signal worth noting: releases 2.0.0 and 2.1.0 landed within about six weeks of each other, which suggests the API is still moving. Pin your version.

How this differs from a plain structured-output client

The obvious alternative is a general-purpose LLM client with structured output, or a framework that treats prompts as templates and parsing as a separate step. The difference is where the checks live. In a template-and-parse setup, the schema is applied after the model returns, and a failure means an exception at the call site. In SymbolicAI, the schema is declared as a Pydantic-compatible model, the field descriptions are used to build the prompt, and the contract decorator owns the retry loop, with accumulate_errors feeding prior failures back into the next attempt. That is a different division of labour: validation and prompting are generated from one declaration, and recovery is a decorator parameter rather than application code. If you already have a Pydantic model and a retry helper, SymbolicAI's contract layer overlaps with what you have built. If you do not, it is the part of the project most likely to save you work. The Symbol primitives are more of a stylistic commitment, and they are the part I would evaluate separately from the contracts.

Who should adopt it, and what to check first

Adopt SymbolicAI if your Python code already has typed models at its boundaries and you want LLM calls to be governed by them, with retries and error accumulation configured in one place. The contract decorator is the reason to look, and the LLMDataModel compatibility with Pydantic means the migration path from an existing model is short. Do not adopt it if you want a thin client, if you dislike operator overloading, or if you cannot tolerate a value whose behaviour depends on a projection applied somewhere else in the call chain. Before you commit, verify the engine default and the local-hosting path in the documentation, confirm whether symbolicai[cluster] is needed for the primitives you intend to use, and read how the remedy loop terminates when pre_remedy and post_remedy are both on. The README does not answer those questions, and they are the ones that decide whether this framework fits your code or fights it.

Editorial conclusion

Adopt SymbolicAI if you are building Python code where LLM calls must sit behind typed data models and you want retries, remedies and error accumulation handled by a decorator rather than by hand. Do not adopt it if you need a thin, predictable API client, or if you cannot accept that the same operator can mean two different things depending on which projection the object is in. Before committing, verify three things against the version you install: which engine the package resolves to by default, whether the cluster extra is required for the primitives you plan to use, and how the contract decorator behaves when both pre_remedy and post_remedy are enabled and the remedy itself fails.

Official sources

  1. ExtensityAI/symbolicai on GitHub
  2. Issues
  3. License: BSD-3-Clause
  4. README
  5. Releases
Community notes

Community notes