Structured Outputs & Batch

Overview

Two related features let you treat eVi like a programmable text-to-data tool instead of just a chat assistant:

The two compose: each batch item can carry its own schema, so you can run hundreds of schema-constrained extractions in one command.

How it works

Structured Outputs

A JSON Schema is wrapped into the OpenAI-style response_format object that eVi's agent already forwards to the backend:

{
  "type": "json_schema",
  "json_schema": { "name": "output", "schema": { ... }, "strict": true }
}

The wrapping is done in evi/structured.py:

That response_format is passed through agent.chat(...) and only attached to the backend request when non-None (see evi/llm/agent.py). Backends that support schema-constrained decoding (OpenAI, LM Studio, recent llama.cpp / Ollama) honor it; backends that don't simply ignore it and fall back to best-effort JSON — eVi does not crash or hard-fail on an unsupporting backend.

Batch

evi/batch.py is a small, model-agnostic engine:

The CLI's batch command supplies a run_one that builds a fresh agent per item, applies the item's mode/schema (falling back to the command flags), and runs it through the headless engine.

Headless engine (shared substrate)

Both evi run and evi batch drain a single agent turn-loop via run_headless(agent, prompt, max_turns=12, response_format=None) in evi/headless.py. It collects streamed text, tool results (truncated to 2000 chars each), usage stats, and any error into a HeadlessResult, and forwards response_format to the backend when given. to_json(res) serializes it to {"text", "tools", "usage", "error"}.

Setup

There is no dedicated config section for Structured Outputs or Batch — they are stateless features driven entirely by command flags, slash-command arguments, and request fields. They reuse your existing model/backend configuration.

Usage

REPL slash command

Inside the interactive REPL:

(Listed in REPL help as /schema <file> [prompt] — "constrain the next turn to a JSON Schema".)

CLI: single headless run

evi run "<prompt>" --schema <file|inline-json> [--format text|json] [--mode chat|cowork|code] [--yes]

CLI: batch

evi batch <input_file> [--out results.jsonl] [--parallel N] [--mode <preset>] [--yes]

Web UI / API

The web chat endpoint accepts an optional output_schema field:

POST /api/chat
{ "session_id": "...", "message": "...", "output_schema": { ...JSON Schema... } }

Examples

Example 1 — Extract structured contact data with evi run

Create a schema file contact.json:

{
  "type": "object",
  "properties": {
    "name":  { "type": "string" },
    "email": { "type": "string" },
    "title": { "type": "string" }
  },
  "required": ["name", "email"],
  "additionalProperties": false
}

Run a one-shot extraction and get the parsed JSON envelope:

echo "Jane Doe, VP Engineering, jane@acme.io" | \
  evi run "Extract the contact fields from this signature." \
  --schema contact.json --format json

The text field of the result will contain a JSON object matching the schema, e.g.:

{"text":"{\"name\":\"Jane Doe\",\"email\":\"jane@acme.io\",\"title\":\"VP Engineering\"}","tools":[],"usage":{"prompt":120,"completion":24,"total":144},"error":null}

You can also pass the schema inline instead of a file:

evi run "Classify the sentiment." \
  --schema '{"type":"object","properties":{"sentiment":{"type":"string","enum":["pos","neg","neu"]}},"required":["sentiment"]}' \
  "I absolutely love this product"

Example 2 — Batch extraction with per-item schemas

Input file jobs.jsonl — each line is its own job, and the first two pin a schema (the schema path is resolved per item, so contact.json from Example 1 is reused):

{"id": "a1", "prompt": "Extract contacts from: Bob Lee, CTO, bob@x.io", "schema": "contact.json"}
{"id": "a2", "prompt": "Extract contacts from: Amy Ng, amy@y.io", "schema": "contact.json"}
{"id": "a3", "prompt": "Summarize in one sentence: the quick brown fox..."}

Run all three, four at a time, writing results to a file:

evi batch jobs.jsonl --out out.jsonl --parallel 4 --yes

eVi prints a summary (wrote 3 results (3 ok) -> out.jsonl) and out.jsonl contains one JSON object per line, keyed by your id:

{"id": "a1", "prompt": "Extract contacts from: Bob Lee, CTO, bob@x.io", "text": "{\"name\":\"Bob Lee\",\"email\":\"bob@x.io\",\"title\":\"CTO\"}", "error": null, "usage": {"prompt": 90, "completion": 22, "total": 112}}
{"id": "a2", "prompt": "Extract contacts from: Amy Ng, amy@y.io", "text": "{\"name\":\"Amy Ng\",\"email\":\"amy@y.io\"}", "error": null, "usage": {"prompt": 84, "completion": 18, "total": 102}}
{"id": "a3", "prompt": "Summarize in one sentence: the quick brown fox...", "text": "A fast brown fox leaps over a lazy dog.", "error": null, "usage": {"prompt": 60, "completion": 12, "total": 72}}

A plain one-prompt-per-line file works too (no schema, no ids needed — eVi assigns indexes):

# translation batch — one prompt per line, blank lines and # comments ignored
Translate to French: Good morning
Translate to French: See you tomorrow
evi batch prompts.txt -j 2

Notes / limits

Edit on GitHub ↗