A Deterministic Compiler for Data
Upload a spreadsheet, ask a question the way you'd ask a person, and get back a real, checked answer instead of a script that might quietly be wrong. An AI figures out what to calculate, but it never writes or runs the code itself. That part is handled by the same deterministic Python every time.
Try the Live DemoBefore Scrygent tries to answer anything, it looks at the data itself. Most tools hand a CSV straight to an LLM and hope it guesses the right column names and values. Scrygent profiles the dataset first, so the planner starts from facts instead of assumptions.
The Profiler runs in milliseconds and works structurally: it flags sequential IDs so they don't get mistaken for real numbers, builds regex skeletons to spot patterned columns like formatted IDs or names, and matches query tokens directly against categorical values, all before the prompt ever reaches the LLM.
| rank | personName | country | category | finalWorth |
|---|---|---|---|---|
| 1 | Elon Musk | United States | Technology | 250.0 |
| 2 | Jeff Bezos | United States | Technology | 195.0 |
| 3 | Bernard Arnault | France | Fashion | 187.0 |
| 4 | Mark Zuckerberg | United States | Technology | 172.0 |
| 5 | Larry Ellison | United States | Technology | 159.0 |
| 6 | Warren Buffett | United States | Finance | 148.0 |
| 7 | Bill Gates | United States | Technology | 135.0 |
| 8 | Mukesh Ambani | India | Diversified | 118.0 |
Asking an LLM to reason about your question and produce perfectly formed JSON in the same breath is asking for trouble. Scrygent splits that work into three passes, so the model never has to think about logic and syntax at the same time.
Pass 1 reads intent and pulls structurally similar past plans from the vector store as few-shot examples. Pass 2 applies query optimizations like filter pushdown, metric consolidation, and top-N entity routing. Pass 3 takes that optimized plan and emits it as a strict, validated Pydantic payload, locked behind json_mode so the output can only ever be well-formed JSON.
First, the model just reads. No schema to worry about, no JSON to format, only what the question is actually asking for. It also checks the vector store for past plans that solved something similar, so it isn't starting cold.
With intent settled, this pass rewrites the plan to run efficiently. Filters move earlier so the dataset shrinks before any aggregation touches it, redundant steps get merged, and queries chasing extremes (biggest, smallest, top N) get routed down a faster path.
Only now does the plan become JSON. This pass translates it into strict Pydantic parameters, locked behind json_mode so the model's output vocabulary is physically restricted to valid tokens. Nothing malformed makes it through.
request_column_stats call and waits until the Executor enriches the profile with what it asked for. A has_replanned guard keeps this from looping: one mid-session augmentation per query, never more.
{
"steps": [
{
"step_id": "step_1",
"rationale": "Filter pushdown: Isolate US and China Technology billionaires to minimize data footprint before aggregation.",
"tool_name": "filter_dataset",
"parameters": {
"filters": [
{ "column": "country", "operator": "in", "value": ["United States", "China"] },
{ "column": "category", "operator": "==", "value": "Technology" }
]
},
"required": true
},
{
"step_id": "step_2",
"rationale": "Generate a bar chart comparing finalWorth across the filtered countries.",
"tool_name": "generate_plot",
"parameters": {
"plot_type": "bar",
"columns": ["country", "finalWorth"],
"title": "Avg Net Worth: US vs China (Tech)"
},
"required": true
}
]
}
Once a plan is verified, the Executor runs it against a handwritten suite of pure Python tools, nothing generated, nothing improvised. Watch the log on the left: step one runs clean, but step two hits a wall the LLM couldn't have predicted.
Transforming tools pass state through immutable temporary files instead of holding DataFrames in memory, and all row-wise math runs through numexpr with a wiped global dictionary, so there's no path to an eval() injection. When a step fails, as it does below, that failure doesn't crash the run. It becomes context for the correction loop explained to the right.
By the time the query reaches the Reporter, every number in it has already been verified. The Reporter's only job is to state that answer plainly before it says anything else.
It's prompted to fill a dedicated primary_answer field, built entirely from the tool outputs upstream. Everything else, the supporting observations below, comes after, and only from what was actually computed.
A demo only has to work once. A tool people actually rely on has to survive rate limits and dropped connections, and shouldn't be rebuilt from scratch the day one LLM provider stops being the right choice. These are the patterns that keep Scrygent running, and adaptable.
Hit a real 429 mid-query and the whole run shouldn't die. A RunnableLambda catches it and backs off with deterministic jitter, while a ContextVar carries live cooldown status up to the UI without pulling a single Streamlit import into the core engine, so the engine stays usable outside Streamlit too.
The Planner and Reporter nodes never call Groq or OpenRouter directly, they call an LLM factory. Swapping which provider backs a given run is a configuration change, not a rewrite. Nothing hot-switches mid-query, but nothing is welded to one vendor either.
Every successful plan is worth remembering. It's embedded with FastEmbed and stored in Qdrant Cloud, so a future query that looks structurally similar can retrieve it as a few-shot example. The planner gets sharper with use, with no retraining and no server to maintain.