Research at UFRGS · v0.3.2 Accepted at SBLP 2026 · CBSoft

A type discipline for
AI-augmented applications.

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).

avaliacao-pedagogica.epi
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
}
127/127
tests passing
~30 → 13
lines of .epi → files generated
2
LLM providers (Anthropic · Ollama)
~3s
end-to-end on a local 3B model

What is Epi

One file in.
Full stack out.
Every AI output validated.

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

Grading student answers with AI help.

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.

The problem without Epi

  • The LLM might output "Mais ou menos" instead of one of the three allowed labels. Nothing in the type system rejects it — it lands in the database.
  • The model's confidence is invisible. A 0.51 guess and a 0.95 conviction are stored identically.
  • No checkpoint: the teacher reviews assessments after they are recorded, not before. Reversal is messy.
  • No audit trail of why the model produced this label, what it interpreted, what alternatives it ranked.
  • Validation, retries, fallback queues, prior distributions — every team rewrites them, slightly differently, every project.

How Epi handles each problem

  • Enum constraint compiled into a runtime Zod schema — invalid labels are rejected before persistence.
  • Confidence reporting required in the JSON output — the field is part of the type, not an afterthought.
  • Checkpoint route generated by the transpiler — low-confidence outputs pause for the teacher, exposed via an inspect/resume API.
  • Trace with Expose — the reasoning is made visible before the final classification, persisted as an audit record.
  • Bayesian prior on the class distribution combines historical incidence with the model output. The hallucination does not erase the domain knowledge.

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

What you write without Epi — and with it.

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.

Conventional TypeScript / Prisma ≈ 200 lines · 6 files · hand-written
// 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 });
}
avaliacao-pedagogica.epi 30 lines · 1 file · transpiled
@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
}
−85%

lines of source code maintained by hand

+5

guarantees emitted automatically: enum, confidence, checkpoint, prior, audit

0

opportunities to forget the validation contract

How it works

A three-layer transpiler. The LLM lives in only one of them.

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.

Layer 1

Parser

100% deterministic

EBNF grammar in Lark. The .epi source becomes a typed AST in Pydantic.

Layer 2

Rigid Generator

100% deterministic

From the AST: Prisma schema, auth middleware, API routes, Zod validators. No LLM. Pure code generation.

Layer 3

Epistemic Generator

constrained by Layer 2

LLM call wrappers, confidence extraction, Trace + Checkpoint infrastructure, Bayesian posterior. Every output passes through the validators of Layer 2 before persistence.

Empirical evidence

Verified end-to-end. Local model. No cloud.

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.

qwen2.5:3b-instruct · Q4_K_M, ~1.9 GB, local via Ollama Question: "What is photosynthesis?"
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

One scenario shows the boundary can fire. It does not show how often it fires when it should.

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.

0
values outside the declared enum ever persisted
6/12
wrong values kept out of the database
20/40
items sent to human review, the cost of that
621ms
median latency per call, local

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

Running in five minutes. Zero cloud.

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

Epi is a sharp tool with a narrow edge.

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.

Use Epi when

  • Audit-by-construction is required (education, legal, healthcare, government)
  • AI-inferred values must be persisted and traceable, not just shown
  • Human-in-the-loop is structural, not exceptional
  • The domain has a meaningful prior distribution
  • You ship a focused product, not a general AI platform

Do not use Epi for

  • Conversational chatbots or generic assistants
  • Complex RAG, multi-tool agents, or fine-tuning workflows
  • Teams already invested in a custom AI platform
  • Pure creative generation
  • Sub-100ms latency paths

Full honest list of current gaps: LIMITATIONS.md

Research

Active research at UFRGS.

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.

Authors

RR
Randerson Rebouças
UFRGS — PPGIE (PhD candidate). Language design and implementation.
DB
Prof. Dante Barone
UFRGS — PPGIE. Stochastic-computing foundations behind Epi's prior and confidence_threshold primitives.
ER
Prof. Eliseo Reátegui
UFRGS — PPGIE. Educational technology and the pedagogical-assessment framing of the work.

Trace + Checkpoint design informed by ongoing discussion with Profa. Rosa Maria Vicari (UFRGS / PPGIE).