verbai
Visibly Enhanced Reasoning Blocks for AI. A Haskell-flavored DSL for AI model orchestration.
verbai is a tiny scripting language and PyPI package for composing and orchestrating AI model calls — local and cloud, text and multi-modal. It is text-based, has a hybrid declarative + procedural syntax, and treats every integration as a pluggable provider: Ollama, OpenAI, Anthropic, MiniMax, Gemini, Mistral, xAI, DeepSeek, Cohere, HuggingFace, OpenRouter, AWS Bedrock, Azure OpenAI, Vertex AI, Groq, Together.ai, Fireworks.ai, Replicate, MCP servers, HTTP APIs, webhooks, WebSockets, and more.
Table of contents
- Quickstart
- Installation
- How verbai works — the mental model
- Provider setup guide
- Built-in verbs (stdlib)
- The example programs
- CLI reference
- Project layout
- Running the tests
- Glossary
- What's next
- License
Quickstart
pip install verbai
verbai run examples/01_hello.verbai
You should see:
hello, world
The examples/ directory ships inside the installed wheel — no git clone required. The package itself is pure-stdlib Python 3.10+; no third-party dependencies for the core.
Installation
pip install verbai # base: Mock provider + MiniMax (cloud extra)
pip install verbai[cloud] # base + the same MiniMax module (already bundled in v0.1)
pip install verbai[local] # + local-runtime providers (v0.2)
pip install verbai[all] # everything that ships
verbai[cloud] is the install target PyPI users should use. It pulls in the MiniMax provider out-of-the-box (no extra packages required — MiniMax is implemented with urllib.request only).
Verify the install
python -m verbai --version # should print: verbai 0.1.0
python -m verbai run examples/01_hello.verbai
How verbai works — the mental model
A verbai script is plain text in a .verbai file. When you run verbai run my.verbai:
┌─────────────────────────────────────────────────────────────┐
│ .verbai source │
│ (text file you wrote) │
└───────────────────────┬─────────────────────────────────────┘
│ Lexer (src/verbai/lexer.py)
▼
┌───────────────┐
│ token stream │ e.g. Print "hello, world" EOF
└───────┬───────┘
│ Parser (src/verbai/parser.py)
▼
┌───────────────┐
│ AST │ CallExpr(Print, [PosArg("hello, world")])
└───────┬───────┘
│ Interpreter (src/verbai/interpreter.py)
▼
┌───────────────┐
│ runtime values│ Print() invoked → stdout
└───────────────┘
The interpreter walks the AST and runs each node:
let NAME = EXPRat module top-level → bindNAMEto the evaluated value in the global scope.do ... end→ run the body top-to-bottom, each<-binding introducing a fresh child scope (immutable within itself).Run { provider = "..." ; model = "..." ; input = "..." }→ look up the named provider in the registry, callprovider.run_inference(model, input, **kwargs), return the resultingValue.- Function calls (
Print "hello, world"with explicit parens, orPrint "x"if the heuristic allows) → look up the callee by name and invoke it with the evaluated arguments.
Two syntactic conventions worth memorising
- Calls are explicit-paren'd.
Print (10 - 4)is one Print call.Print 10 - 4isPrint(10) - 4, which fails at runtime (you can't subtract fromNil). Wrap binary expressions in parens. - Object literals use semicolons, lists use commas.
{ host = "x" ; n = 7 }is an object;[1, 2, 3]is a list. To pass an object/list as a single positional argument, wrap it in parens:Print ({ a = 1 }). Otherwise the{ a = 1 }is parsed as a keyword-block call (f { a = 1 }→f(a=1)), used byRun.
Provider setup guide
Every provider is a Python class registered with verbai.providers.register_provider. v0.1 ships two: Mock (always available, no key, returns canned responses) and MiniMax (real API, key from env var). Other providers in the spec roadmap (Ollama, OpenAI, Anthropic, …) raise NotImplementedError in v0.1 and are wired in v0.2.
Mock provider (no setup)
The default. No configuration required.
do
out <- Run { provider = "mock" ; model = "echo" ; input = "hi" }
Print out
end
Output:
echo: 'hi'
The Mock provider:
- Echoes back
f"{model}: {input!r}"for every call. - Records every call in
mock.calls(useful for test fixtures). - Can be configured to raise after N calls via
error_after.
from verbai.providers.mock import MockProvider
m = MockProvider()
m.error_after = 2 # raise ProviderError starting at call #3
m.error_message = "rate limited"
MiniMax provider
Sign up at platform.minimax.io (or the China-region host if you're there). Get an API key from your dashboard.
Security: the key is never read from a file. You inject it as an environment variable. Disable it on the dashboard as soon as you're done testing.
# bash / gitbash
export MINIMAX_API_KEY='sk-cp-...'
verbai run examples/14_minimax_pong.verbai
unset MINIMAX_API_KEY
# PowerShell
$env:MINIMAX_API_KEY = 'sk-cp-...'
verbai run examples\14_minimax_pong.verbai
Remove-Item Env:MINIMAX_API_KEY
Default model is MiniMax-M3. To use another model, override model in your script:
do
reply <- Run {
provider = "minimax"
model = "MiniMax-M3.1-Flash-Preview"
input = "Explain retrieval-augmented generation in 3 sentences."
max_completion_tokens = 256
}
Print reply
end
Optional kwargs passed through to the provider:
| kwarg | effect |
|---|---|
max_completion_tokens |
cap response length (default 256) |
temperature |
sampling temperature |
top_p |
nucleus sampling |
Endpoint: POST https://api.minimax.io/v1/chat/completions (OpenAI-compatible).
Using a different base URL (China region, self-hosted, etc.)
Construct the provider directly:
from verbai.providers.minimax import MiniMaxProvider
from verbai.providers import register_provider
register_provider(
MiniMaxProvider(
base_url="https://api.minimaxi.com/v1", # China-region host
api_key="sk-cp-...",
)
)
Or set the env var and pass a one-off base URL via your script.
Other providers (Ollama, OpenAI, Anthropic, …)
These are stubs in v0.1 — Run { provider = "ollama" ; ... } raises ProviderError("unknown provider: 'ollama'"). Wiring each is a single Python file in src/verbai/providers/. See PUBLISHING.md (maintainer-only) for the roadmap.
Adding your own provider
from verbai.providers import Provider, register_provider
from verbai.runtime import TextValue
class MyProvider(Provider):
name = "my-provider"
def run_inference(self, model, input, **kwargs):
# Call your backend here.
return TextValue(f"{model}: {input}")
register_provider(MyProvider())
Drop this into a script or module that's imported before verbai run. Your provider then works the same way the built-ins do:
do
out <- Run { provider = "my-provider" ; model = "v1" ; input = "hello" }
Print out
end
Built-in verbs (stdlib)
These names are bound in every script's global scope:
| Verb | Signature | Notes |
|---|---|---|
Print |
Print value or Print v1, v2, … sep = " ", end = "\n" |
Writes to stdout, returns Nil. |
LoadFrom |
LoadFrom "path.verbai" as "Text" |
Reads a file; default as = "Text" returns its contents. |
SaveTo |
SaveTo "path" data |
Writes data's display string to disk; auto-creates parent dirs. |
Env |
Env "KEY" default = "fallback" |
Reads an env var; raises VerbError(NAME) if missing and no default. |
Run |
Run { provider = "x" ; model = "y" ; input = "z" } |
Invokes the named provider's run_inference. |
Fallback |
Fallback expr1 expr2 … |
Returns the first expr that doesn't raise ProviderError. |
Retry |
Retry n expr backoff = "exponential" |
Re-runs expr up to n times on failure. |
ForEach |
ForEach list (x -> body) or ForEach.Parallel list (x -> body) |
Iterates; sequential or parallel. |
Return |
Return value |
Short-circuits the enclosing function. |
How Run resolves a name
Run { provider = "minimax" ; model = "MiniMax-M3" ; input = "hi" } is sugar over minimax.run_inference(model="MiniMax-M3", input="hi"). The verb looks up provider in the registry (built-ins auto-register on first lookup; third-party providers via register_provider() at import time). The first time a provider is referenced, verbai lazy-imports its module — so you only load what you use.
How Print handles each type
| Value | Prints |
|---|---|
Int 42 |
42 |
Float 3.14 |
3.14 |
Text "hi" |
hi |
Bool True |
True |
Nil |
Nil |
[1, 2, 3] |
[1, 2, 3] |
{ a = 1 } |
{a = 1} |
The example programs
All 14 example scripts ship inside the installed
examples/directory. Each one is self-contained — copy-paste the body into a.verbaifile on your machine and run it. None of them require an API key except example 14 (real MiniMax).
01 01_hello.verbai
Print "hello, world"
$ verbai run examples/01_hello.verbai
hello, world
02 02_constants.verbai
let PI = 3.14
let NAME = "verbai"
let MAX_TOKENS = 4096
Print PI
Print NAME
Print MAX_TOKENS
$ verbai run examples/02_constants.verbai
3.14
verbai
4096
03 03_variables.verbai
-- 03_variables.verbai
-- `let` at module scope is an immutable binding.
-- Inside a `do` block, `<-` shadows with a local value.
let global = 100
do
local <- 50
Print (global + local)
Print local
end
Print global
$ verbai run examples/03_variables.verbai
150
50
100
04 04_expressions.verbai
-- 04_expressions.verbai
Print (1 + 2 * 3)
Print ((1 + 2) * 3)
Print (10 - 4)
Print (10 / 3)
Print ("Hello, " + "world!")
Print (5 == 5)
Print (5 < 10)
$ verbai run examples/04_expressions.verbai
7
9
6
3.3333333333333335
Hello, world!
True
True
Important: verbai calls are explicit-paren'd.
Print (10 - 4)is unambiguous;Print 10 - 4parses asPrint(10) - 4and fails at runtime. Wrap binary expressions in parens.
05 05_types.verbai
-- 05_types.verbai
-- Primitive types and how they print.
Print 42 -- Int
Print 3.14 -- Float
Print "hello" -- Text
Print True -- Bool
Print False -- Bool
Print nil -- Nil
Print ([1, 2, 3]) -- List of Int
Print ({ name = "verbai" ; version = 0 }) -- Object
$ verbai run examples/05_types.verbai
42
3.14
hello
True
False
Nil
[1, 2, 3]
{name = verbai, version = 0}
Object literals use semicolon between entries; commas are for lists. To pass a list or object as a single argument, wrap it in parens.
06 06_conditionals.verbai
-- 06_conditionals.verbai
let x = 5
if x > 0 then
Print "positive"
else
Print "non-positive"
end
let y = -3
if y > 0 then
Print "positive"
else
Print "non-positive"
end
$ verbai run examples/06_conditionals.verbai
positive
non-positive
07 07_loops.verbai
-- 07_loops.verbai
ForEach [1, 2, 3, 4, 5] (x -> Print x)
$ verbai run examples/07_loops.verbai
1
2
3
4
5
08 08_functions.verbai
-- 08_functions.verbai
let double x = x * 2
let add3 a b c = a + b + c
Print (double 21)
Print (add3 1 2 3)
$ verbai run examples/08_functions.verbai
42
6
09 09_recursion.verbai
-- 09_recursion.verbai
let factorial n =
if n <= 1 then
1
else
n * (factorial (n - 1))
end
Print (factorial 5)
Print (factorial 10)
$ verbai run examples/09_recursion.verbai
120
3628800
10 10_lists.verbai
-- 10_lists.verbai
let xs = [10, 20, 30, 40]
Print xs
Print (xs[0])
Print (xs[2])
ForEach xs (x -> Print (x * 2))
$ verbai run examples/10_lists.verbai
[10, 20, 30, 40]
10
30
20
40
60
80
11 11_objects.verbai
-- 11_objects.verbai
let config = { host = "api.example.com" ; port = 443 ; retries = 3 }
Print config.host
Print config.port
do
msg <- Run { provider = "mock" ; model = "echo" ; input = "ping" }
Print msg
end
$ verbai run examples/11_objects.verbai
api.example.com
443
echo: 'ping'
12 12_errors.verbai
-- 12_errors.verbai
do
out <- try
do
x <- Run { provider = "mock" ; model = "fail-once" ; input = "boom" }
Print x
end
catch err ->
Print "recovered from error"
end
Print out
end
do
msg <- Run { provider = "mock" ; model = "echo" ; input = "hello" }
Print msg
end
$ verbai run examples/12_errors.verbai
fail-once: 'boom'
Nil
echo: 'hello'
The
try ... catch err -> ... endform binds the error toerrand runs the catch body. Use_instead oferrif you don't need the error value. Theerrbinding also works as a kind filter (catch DivisionByZeroError -> ...).
13 13_first_inference.verbai
-- 13_first_inference.verbai
do
result <- Run { provider = "mock" ; model = "echo" ; input = "hi" }
Print result
end
$ verbai run examples/13_first_inference.verbai
echo: 'hi'
14 14_minimax_pong.verbai (requires real API key)
-- 14_minimax_pong.verbai
do
reply <- Run { provider = "minimax" ; model = "MiniMax-M3" ; input = "Reply with exactly one short sentence." }
Print reply
end
Run it:
export MINIMAX_API_KEY=sk-cp-...
verbai run examples/14_minimax_pong.verbai
unset MINIMAX_API_KEY
You should see a one-sentence reply from real MiniMax M3 printed to stdout.
Disable the API key on your MiniMax dashboard as soon as you're done testing. The MiniMax provider reads
MINIMAX_API_KEYfrom your environment; verbai never stores it.
CLI reference
verbai --version # print version
verbai run <file.verbai> # execute a script
verbai check <file.verbai> # parse + structurally check, no execution
verbai fmt <file.verbai> # format to stdout
verbai fmt <file.verbai> --write # rewrite the file in place
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Runtime error or missing file |
| 2 | Parse error |
Script path resolution
When you write verbai run examples/01_hello.verbai, the CLI looks first in the current working directory. If the file is not there, it falls back to the bundled copy inside the installed verbai package — so PyPI users don't need to clone the private GitHub repo to follow along with this README.
Project layout
verbai/
├── src/verbai/ # the package
│ ├── __init__.py
│ ├── __main__.py # `python -m verbai` entry point
│ ├── cli.py # argparse + subcommand routing
│ ├── lexer.py # tokens for the verbai source language
│ ├── parser.py # tokens → AST
│ ├── ast_nodes.py # dataclass AST node types
│ ├── interpreter.py # tree-walking evaluator
│ ├── runtime.py # Value types, RuntimeContext, run_source()
│ ├── symbol_table.py # scope chain + name resolution
│ ├── stdlib.py # built-in verbs (Print, Run, …)
│ ├── format.py # canonical formatter
│ ├── providers/
│ │ ├── __init__.py # Provider ABC + registry
│ │ ├── base.py # base re-export
│ │ ├── mock.py # MockProvider (default, no key)
│ │ └── minimax.py # MiniMaxProvider (real API)
│ └── examples/ # shipped inside the wheel
│ ├── 01_hello.verbai
│ ├── 02_constants.verbai
│ └── … (14 total)
├── tests/ # pytest suite (169 passing)
├── docs/
│ ├── specs/ # language design spec
│ └── cookbook.md # chapter 1 prose walk-through
├── pyproject.toml # build + deps + pytest config
├── LICENSE # Apache 2.0
├── README.md # this file
├── PUBLISHING.md # maintainer-only PyPI release guide
└── AGENTS.md # notes for AI coding agents
Running the tests
pip install -e ".[dev]" # installs pytest, pytest-cov, build, twine
python -m pytest # 169 passing, 2 skipped (real-API gated)
python -m pytest --cov=verbai --cov-fail-under=85 # coverage gate (≥ 85 %)
python -m pytest -m real_api # opt-in: runs example 14 against real MiniMax
Use
python -m pytestrather than barepytestso the right Python interpreter wins (your gitbash PATH may point at a different Python than yourpythondoes).
Glossary
| Term | Meaning |
|---|---|
| AST | Abstract Syntax Tree. The structured representation of your script that the interpreter walks. |
| Capability check | A check (planned for v0.2) that a provider supports the model + output type your script asks for. v0.1's substitute is that Run simply raises ProviderError if the provider doesn't know what to do. |
| Closure | The captured lexical scope that travels with a FunctionValue. Lets lambdas see the bindings that were in scope where they were defined. |
| do block | do … end — a block of statements and <- bindings. Each binding is immutable within the block; child scopes can shadow outer names. |
| Fallback | The Fallback verb: tries each expression in turn, returning the first that doesn't raise ProviderError. |
| Field access | obj.field — looks up a key in an ObjectValue. Returns Nil if the key is missing. |
| ForEach | The verb that iterates over a list, optionally in parallel via ForEach.Parallel. |
| Function declaration | let name p1 p2 = body — desugars to let name = \p1 p2 -> body. |
| Hybrid syntax | A single script can mix declarative let/do blocks with procedural expression statements. |
| Immutable binding | A let NAME = … or <- NAME binding cannot be reassigned. Re-declaring in the same scope raises NameError; shadow with a nested scope. |
| Keyword-block call | f { a = 1 ; b = 2 } — sugar for f(a=1, b=2). Used by Run. Object literals ({ a = 1 }) without a leading function are wrapped in parens to force positional treatment. |
| Lambda | Anonymous function. \(x -> body) in source, written as (x -> body) in the language. |
| Mock provider | The built-in mock provider used in tests and offline examples. No API key, returns canned responses. |
| Module scope | The outermost lexical scope of a .verbai script. let bindings at the top level land here. |
| Multi-modal | Inputs/outputs beyond text: image, audio, video. Planned for v0.2. |
| MCP | Model Context Protocol. A protocol for connecting verbai to tool-using agents. v0.2. |
Pattern (in catch) |
Either _ (always catch) or a name like err (bind the error to that name and run the catch). Some error-kind names also match by kind. |
| Pipe | `a |
| Provider | A pluggable Python class implementing verbai.providers.Provider. Each one wraps an external model API (or mock). |
| ProviderError | A VerbError with kind=kValue("ProviderError"). Raised when a provider call fails. Fallback catches only this kind. |
| PyPI | Python Package Index — the public package registry at pypi.org. pip install verbai pulls from here. |
| REPL | Read-Eval-Print Loop. v0.2. |
| Retry | The Retry verb: re-runs an expression up to n times on failure, with optional exponential backoff. |
| Run | The verb that dispatches to a provider's run_inference(model, input, **kwargs). |
| Stdlib | The standard library of built-in verbs in src/verbai/stdlib.py. |
| Test PyPI | test.pypi.org — a sandbox PyPI instance. We do not use this in v0.1; the pipeline goes straight to real PyPI. |
| Try/catch | try EXPR catch err -> EXPR end — runs the body, recovers if it raises VerbError. |
| Value | A runtime value: IntValue, FloatValue, TextValue, BoolValue, NilValue, ListValue, ObjectValue, FunctionValue, or ProviderHandleValue. |
| Verb | A callable exposed in the global scope. Built-in verbs (Print, Run, …) live in stdlib.py; user-defined functions become verbs when bound at module scope. |
| Wheel | A built distribution (.whl) that pip install consumes. We build both a wheel and an sdist; the wheel contains the example programs as package data. |
What's next
- v0.2: examples 15–31 from the spec curriculum (multi-modal, MCP, HTTP, webhooks, more providers).
- v0.2: formal type checker (capability checks stay as the v0.1 substitute).
- v0.3: LSP server, syntax highlighting, REPL.
License
Apache 2.0. See LICENSE.
Metadata
Release files for verbai 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| verbai-0.1.1.tar.gz | 66.5 kB | Without provenance | Details for verbai-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| verbai-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for verbai-0.1.1-py3-none-any.whl |
Total release size: 123.0 kB
Release files / verbai-0.1.1.tar.gz
| Download URL | verbai-0.1.1.tar.gz |
|---|---|
| Size | 66.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9aee130eac3250839370fd8ac46793eeda1b542037960900701084f5e5c10989
|
|
BLAKE2b-256 checksum How to use checksums |
6c7c2158d8cda16ec706922b8cc695d8448b2612659788635ec2bde9ddbf7d52
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|
Release files / verbai-0.1.1-py3-none-any.whl
| Download URL | verbai-0.1.1-py3-none-any.whl |
|---|---|
| Size | 56.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
10ae54026700194b9e4e5c35714d965509c94b4d5b4198c92c156da26f8fcd13
|
|
BLAKE2b-256 checksum How to use checksums |
078f9abd8501fa762a192ec6255e2b4872460f7735ca4a81c1fc036e2ccd2b2b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|
Security for verbai 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.