Epi makes the boundary between deterministic computation and AI inference visible to the type system — so a hallucinated value cannot silently land in your database.
Active research at UFRGS / PPGIE. Authors: Randerson Rebouças, Prof. Dante Barone, Prof. Eliseo Reátegui.
Accepted at SBLP 2026, the 30th Brazilian Symposium on Programming Languages (CBSoft 2026).
Entity Submissao {
id: UUID(auto),
resposta_aluno: Text,
avaliacao: AI.Enum(
Correto, Parcial, Incorreto,
prior: Distribution(
Correto: 0.40,
Parcial: 0.45,
Incorreto: 0.15
),
confidence_threshold: 0.85
)
}
Pulse AvaliarResposta {
Input: Submissao
Process: Execute: AI.classify(
source: Input.resposta_aluno,
on_low_confidence:
Checkpoint.ReviewRequired(
role: "Professor"
)
)
Output: Submissao.avaliacao
}
What is Epi
Epi is a small programming language designed for one job: building applications where some values come from a database and others come from a large language model — and the difference must matter to the type system.
You describe your application in a single .epi file: entities, authorization rules, AI inference steps, pipelines, and UI surfaces. The Epi transpiler reads that source and emits a complete runnable Next.js project — schema, API routes, middleware, runtime validators, LLM call wrappers, and human-review checkpoints.
Every AI-inferred field carries a contract: an enum or range to honor, a confidence to report, a fallback if the contract is broken. The transpiler emits that contract as runtime code. The developer does not write it. The developer cannot forget it.
The thesis is narrow: LLM hallucination cannot be prevented, but it can be contained at the type-system boundary, by construction, in the generated code.
Use case · Education
Consider a learning platform where a teacher submits a question and a student's answer. An LLM proposes a classification — Correto, Parcial, Incorreto — and the teacher needs to see, audit, and approve the assessment before it is recorded. This is the exact scenario behind Epi's design, in conversation with researchers at UFRGS / PPGIE.
"Mais ou menos" instead of one of the three allowed labels. Nothing in the type system rejects it — it lands in the database.The same mechanisms apply to any decision-grade flow: legal contract triage, clinical pre-screening, regulatory classification. Wherever AI outputs become facts of record.
Side by side
The same feature, expressed two ways. On the left, the artifacts a team typically produces by hand. On the right, the Epi declaration. The transpiler emits everything on the left from everything on the right.
// prisma/schema.prisma
model Submissao {
id String @id @default(uuid())
resposta_aluno String
avaliacao String // no enum constraint at type level
createdAt DateTime @default(now())
}
// app/api/avaliar/route.ts (truncated)
export async function POST(req: NextRequest) {
const session = await getServerSession();
if (session?.role !== "Professor") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const input = await req.json();
// Hand-rolled LLM call
const res = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 256,
system: SYSTEM_PROMPT,
messages: [{ role: "user", content: input.resposta_aluno }],
});
const text = (res.content[0] as any).text;
// No schema — manual parsing, frequently broken
let parsed: any;
try { parsed = JSON.parse(text); } catch { return error(...); }
if (!["Correto","Parcial","Incorreto"].includes(parsed.avaliacao)) {
// every team writes this branch differently
return NextResponse.json({ queue: "manual_review" });
}
// No confidence threshold. No Bayesian update.
// No trace. No checkpoint. No audit.
await prisma.submissao.update({
where: { id: input.id },
data: { avaliacao: parsed.avaliacao },
});
return NextResponse.json({ ok: true });
}
@Language: Epi v0.3
@Database: sqlite
Entity Submissao {
id: UUID(auto),
resposta_aluno: Text,
criado_em: DateTime(auto),
avaliacao: AI.Enum(
Correto, Parcial, Incorreto,
strict: true,
prior: Distribution(
Correto: 0.40,
Parcial: 0.45,
Incorreto: 0.15
),
confidence_threshold: 0.85
)
}
Guard SoProfessores {
Condition: Auth.Role == "Professor"
}
Pulse AvaliarResposta {
Input: Submissao
Protect: Guard.SoProfessores
Process:
Execute: AI.classify(
source: Input.resposta_aluno,
prompt: file("@prompts/avaliar.md"),
temperature: 0.1,
confidence_threshold: 0.85,
on_low_confidence:
Checkpoint.ReviewRequired(role: "Professor"),
on_fail:
Fallback.ManualReview(Queue: "Professores")
)
Output: Submissao.avaliacao
}
lines of source code maintained by hand
guarantees emitted automatically: enum, confidence, checkpoint, prior, audit
opportunities to forget the validation contract
How it works
Parsing and rigid generation are fully deterministic. The epistemic layer — the part that calls the LLM — operates strictly inside the contract emitted by the deterministic layer above it.
EBNF grammar in Lark. The .epi source becomes a typed AST in Pydantic.
From the AST: Prisma schema, auth middleware, API routes, Zod validators. No LLM. Pure code generation.
LLM call wrappers, confidence extraction, Trace + Checkpoint infrastructure, Bayesian posterior. Every output passes through the validators of Layer 2 before persistence.
Empirical evidence
The exact pipeline shown above — student answer in, validated assessment out — was generated by epi transpile and exercised against Qwen 2.5 3B Instruct running locally via Ollama on a laptop. No cloud API, no credit card, no proxy.
| Student answer | Model JSON | Boundary action | Latency |
|---|---|---|---|
| "Photosynthesis is the process by which plants convert sunlight, water, and CO2 into glucose and oxygen, using chlorophyll." | {"avaliacao":"Parcial", "_confidence":0.8} |
0.8 < 0.85 → Checkpoint → Professor |
2.6s |
| "Plants eat sunlight." | {"avaliacao":"Incorreto", "_confidence":0.9} |
Validated → "Incorreto" |
0.8s |
| "Photosynthesis is the name of my dog." | {"avaliacao":"Incorreto", "_confidence":0.9} |
Validated → "Incorreto" |
0.8s |
The first row is the point. A small 3B model misclassified a textbook-correct answer as "Parcial" — exactly the kind of false negative that would unfairly affect a student. Without Epi, that label would have been written to the database. With Epi, the model's own reported confidence (0.8 < threshold 0.85) tripped the Checkpoint route. The teacher is summoned before the assessment is recorded — not after, in a complaint.
The same .epi source compiles unchanged against Anthropic Claude. Provider choice is a runtime .env setting, not a code change.
40 curated inputs
So we ran the generated project over a curated set of 40 inputs with gold labels, three repeats each, against Qwen 2.5 3B. The set is deliberately loaded with hard cases: right but incomplete answers, fluent answers wrong on one word, items with no defensible single label, and adversarial inputs that try to force a label outside the declared enum. The harness imports the transpiler-emitted Pulse, so the threshold, the validator and the dispatch all run in generated code.
What holds structurally
No value outside the declared enum reached the persistence path, in any run. Not for lack of trying: repeating the set against a deliberately weaker model, Llama 3.2 1B, one adversarial item drove it to invent the label CORRETÍSSIMO on all three repeats, and the generated validator rejected it into the manual-review queue. Another drove it to emit unparseable output, caught by the same fallback. Neither path was written by hand.
What it does not buy you
Containment of merely wrong values is bounded by something the type system cannot supply: the model's self-reported confidence has to actually track its errors. The 1B model shows the bound by breaking it, reporting high confidence almost everywhere and escaping with all 18 of its wrong values. A type system can guarantee that an out-of-domain value is never written. It cannot detect a confident, well-formed lie.
Changing one token of the .epi source, so the model receives the question along with the answer instead of the answer alone, halves the raw error rate and takes wrong persisted values to zero, at 24 of 40 items sent to review. The harness, the input set with its gold labels, and the raw per-call results are in eval/.
Quick start
You do not need to clone the repository, configure cloud credentials, or provision a database. Ollama on localhost and SQLite as a file are enough.
To switch to Anthropic Claude, edit .env and set EPI_AI_PROVIDER=anthropic. The .epi source does not change. The generated code does not change.
# once — install Ollama and pull a small instruction-tuned model
ollama pull qwen2.5:3b-instruct
ollama serve &
# install Epi and bootstrap a project
pip install epi-lang
epi init meu-app
cd meu-app
# transpile to a runnable Next.js project
epi transpile contrato.epi --target nextjs --outdir ./app
# run it
cd app
npm install
cp .env.example .env # pre-configured for Ollama local
npx prisma migrate dev --name init
npm run dev
Scope
It is not a general AI framework. It is a discipline for cases where AI outputs must be persisted, audited, and challenged before they become ground truth.
Full honest list of current gaps: LIMITATIONS.md
Research
Epi is developed in the Postgraduate Program in Informatics in Education (PPGIE) at the Federal University of Rio Grande do Sul. The short paper "Epi: An Epistemic Type System for Containing LLM Hallucination in Generated Code" was accepted at SBLP 2026, the 30th Brazilian Symposium on Programming Languages, part of CBSoft 2026. The implementation is open source, under continuous integration, and archived on Zenodo.
prior and confidence_threshold primitives.Trace + Checkpoint design informed by ongoing discussion with Profa. Rosa Maria Vicari (UFRGS / PPGIE).