jq for the text only an LLM can parse

Messy text in. Validated JSON out.

One static binary that turns messy, human-written text into schema-validated NDJSON in a single pipe stage. The script is the filter: it reads stdin, asks an LLM for typed fields, and prints one validated row per line. Every row matches your schema or the run fails naming the field that broke, so the next pipe stage never sees garbage.

$ cat tickets.txt | loqi extract.lq > tickets.ndjson

Dependency-free C11 Single native binary MIT licensed
extract.lq
# one record per input line, type-checked against your schema
let schema = { type: "object", required: ["id", "severity"],
  fields: { id: { type: "int" }, severity: { type: "string", enum: ["low", "high"] } } }

for line in lines(stdin()) {
  print(json.stringify(ai_json("Extract id, severity from:\n{line}", schema)))
}

Why Loqi

The extraction step shouldn't need a virtualenv.

You have prose on stdin and you want typed JSON on stdout. Loqi makes that one pipe stage: a short script, a schema, one static binary.

Validated, or it fails

ai_json(prompt, schema) returns typed fields, retried against the model, or the run exits naming the field that broke. The next stage in your pipe never gets garbage.

One binary in your pipe

Reads stdin, writes one JSON object per line. Composes with jq, grep, sort and xargs. Nothing to pip install, no virtualenv, no SDK to vendor.

Parallel by default

ai_all fires prompts concurrently, so a 10k-line file isn't 10k sequential round-trips. The slow part is the network, and Loqi overlaps it.


Batteries included

Everything the extraction job needs, in the binary.

The model call, the JSON codec, the HTTP client, vector search and file/env I/O all live in the runtime. No installs, no SDK, no virtualenv.

ai(prompt)

A first-class LLM call to the Anthropic Messages API. Reads ANTHROPIC_API_KEY; pass a model as a second arg or via LOQI_AI_MODEL. Rate limits and transient errors are retried with backoff automatically.

let poem = ai("Write a haiku about clean code")
print(poem)
json

json.parse(str) and json.stringify(value), a built-in codec between JSON text and native Loqi values.

let cfg = json.parse(`{"name": "Ada"}`)
print(cfg.name)
http

http.get(url) and http.post(url, body, content_type?), a built-in HTTP client returning the response body as a string.

let body = http.get("https://api.github.com")
print(len(body))
similarity(a, b)

Cosine similarity over numeric vectors, the primitive behind semantic search, ranking and clustering.

let s = similarity(a, b)
print(s)  # a float in [-1, 1]
read / write

read(path) returns a whole file as a string; write(path, content) writes one back. Simple, whole-file I/O.

write("out.txt", "hi")
print(read("out.txt"))
env(name)

Read environment variables straight from the runtime, keys, config and feature flags, no wrapper library.

let key = env("ANTHROPIC_API_KEY")
print(type(key))

Honest about speed

Instant startup, Python-class compute.

For a text-extraction pipe the wall-clock is dominated by model calls, which Loqi overlaps with ai_all. The interpreter is a stack bytecode VM: measured on Apple Silicon with process_time, on par with CPython 3.13 on fib(30) and about 1.4× faster on a tight loop to 50M. No virtualenv to spin up, no JIT to warm: the binary starts and the pipe runs.

The honest part: Node/V8 runs fib(30) faster (0.017s) because it JITs. Loqi does not JIT yet. We do not claim to beat Go, Rust, C or Node, just a single compiled binary with Python-class speed that gets faster each iteration.

fib(30), lower is faster

Loqi0.097s
CPython 3.130.094s
Node / V8 (JIT)0.017s

tight loop to 50M, Loqi 3.0s vs CPython 4.3s (~1.4× faster). Numbers measured on Apple Silicon.


By example

Real Loqi, top to bottom.

Recursion, closures, AI-powered extraction, and verbatim raw strings for JSON, every snippet below runs as-is.


Quickstart

Up and running in three commands.

Built with nothing but clang, portable by default (LOQI_NATIVE=1 for -mcpu=apple-m1 -flto). No package manager, no toolchain zoo. Clone, build, run.

1

Clone the repo

Grab the source, reference implementation lives in src/loqi.c.

2

Build the binary

One script compiles a single native binary into build/loqi.

3

Run an example

Point Loqi at any .lq file in examples/ and go.

terminal
# 1. clone
$ git clone <repo-url> loqi
$ cd loqi

# 2. build, single native binary
$ ./scripts/build.sh

# 3. run your first program
$ ./build/loqi examples/01_hello.lq
Hello, world!

Frequently asked questions

What is Loqi?

A command-line tool that turns messy, unstructured text into schema-validated NDJSON in a single pipe stage, the way jq transforms structured JSON. A short .lq script reads stdin, calls ai_json(prompt, schema) to extract typed fields with an LLM, and prints one validated row per line: cat tickets.txt | loqi extract.lq > tickets.ndjson. If a row can't be made to validate, the run fails naming the field that broke, so the next stage never gets garbage. One static binary, macOS and Linux.

How is it different from jq, grep/awk, or Python + an SDK?

jq needs structured input; Loqi produces the structure from prose. grep and awk match and slice text but can't reason about it. Python plus an SDK drags a virtualenv, pip install and import boilerplate into what should be one pipe stage, then you hand-roll the client, JSON parse, schema check and retries. Loqi bakes typed, validated-or-fails, parallel extraction into one binary where the script is the filter.

How fast is it?

A single static binary with instant startup (no virtualenv or import cost). The interpreter is a stack bytecode VM in C, roughly CPython-class on compute, but for a text-extraction pipe the time is dominated by the model calls, which Loqi runs in parallel with ai_all.

How do I install it?

Clone github.com/ferdinandobons/loqi and run ./scripts/build.sh to get ./build/loqi. It builds on macOS and Linux. At runtime it needs curl and, for model calls, an ANTHROPIC_API_KEY.