Metadata-Version: 2.4
Name: wassitai
Version: 0.2.0
Summary: Universal structured-generation & LLM-output parsing framework
Author-email: Abdelaziz Kella <kaa.kella.abdelaziz@gmail.com>, Abdelaziz Kella <a.kella@univ-chlef.dz>
License: MIT
Project-URL: Homepage, https://github.com/kaaaziz/wassitai
Project-URL: Repository, https://github.com/kaaaziz/wassitai
Project-URL: Issues, https://github.com/kaaaziz/wassitai/issues
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pydantic>=2
Requires-Dist: httpx>=0.24
Requires-Dist: json_repair>=0.25
Requires-Dist: xmltodict>=0.13
Provides-Extra: api
Requires-Dist: fastapi>=0.110; extra == "api"
Requires-Dist: uvicorn[standard]>=0.29; extra == "api"
Requires-Dist: python-multipart>=0.0.9; extra == "api"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: hypothesis>=6; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"
Requires-Dist: ruff<0.17,>=0.16; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Dynamic: license-file

# Wassitai

Ask an LLM a question, get back a **validated Python object** — not a string you have to parse.

Wassitai sends your prompt with a schema, picks the best structured-output method the provider
supports, then extracts, repairs, validates and coerces the reply through a deterministic pipeline.
If the result still doesn't fit the schema, it re-asks the model. Ships as the importable package
`wassitai`, with a REST API and a browser playground on top.

```bash
pip install wassitai            # library
pip install "wassitai[api]"     # + REST API and playground
```

Code written against the pre-0.2 name still works: `llm_parser` remains importable as a deprecated
alias for `wassitai` until 2.0. If you have an old editable install of the `llm_parser` *distribution*,
run `pip uninstall llm_parser` first — pip won't replace it, and its source tree would keep shadowing
the alias.

## Features

- 🤖 **Any OpenAI-compatible provider** — Groq, OpenAI, DeepSeek and OpenRouter as presets;
  Together, xAI, Ollama, vLLM or a local server with just `base_url=` and `model=`
- 🧱 **Five ways to write a schema** — Pydantic model, `{"field": "type"}` dict, JSON Schema, or a
  JSON or XML schema string
- 🔁 **Object, JSON or XML out** — the same validated result, rendered however you need it
- 📊 **Lists** — ask for `list[Invoice]`, get a list back
- 🧪 **Repairs before it fails** — lifts JSON out of markdown fences and prose, fixes trailing
  commas, smart quotes and Python-repr output, then re-asks the model if it still doesn't fit
- 🔒 **Strict or flexible validation** — exact types for production, coercion while prototyping
- 💡 **Diagnostics you can act on** — every stage reports what it did, a confidence score falls as
  repairs accumulate, and errors are a typed tree with fixes in the message
- 🎯 **Pydantic all the way through** — your model is the schema, the validator and the return type
- 🌐 **A playground, not just a library** — browser console plus a beginner's guide
- ✅ **329 tests** — unit, integration and end-to-end, with no network calls

## Python

```python
from wassitai import Client
from pydantic import BaseModel

class Invoice(BaseModel):
    number: str
    total: float

client = Client.from_provider("groq", api_key="…", model="llama-3.3-70b-versatile")
invoice = client.generate("Extract the invoice from: …", schema=Invoice)   # -> Invoice

# A list, by asking for a list schema:
invoices = client.generate("Extract all invoices: …", schema=list[Invoice])

# Never raises — for batch pipelines:
res = client.try_generate("…", schema=Invoice)     # -> Result[Invoice]
if res.ok:
    print(res.value, res.confidence)

# Text you already have, no provider call:
from wassitai import parse, render
invoice = parse('{"number": "A1", "total": 9.5}', Invoice)
text = render(invoice, "json")
```

A schema can be a Pydantic model, a plain `{"field": "type"}` dict, or a JSON Schema.
`strict=True` enforces exact types; the default coerces close-enough values (`"3"` → `3`).
`max_retries` caps how often the model is re-asked (default 1).

## REST API & playground

```bash
uvicorn wassitai.api.app:app --reload
```

Playground at `http://localhost:8000/`, beginner's guide at `/static/guide.html`,
OpenAPI reference at `/docs`.

| Method | Path | Does |
|---|---|---|
| `POST` | `/v1/generate` | prompt + schema → validated object |
| `POST` | `/v1/parse` | text + schema → validated object (no provider call) |
| `GET` | `/v1/providers` | providers and their capabilities |
| `GET` | `/health` | liveness |

```bash
curl -X POST http://localhost:8000/v1/generate \
  -H "X-Provider-Key: $GROQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"provider":"groq","prompt":"Extract the invoice: …",
       "schema":{"number":"str","total":"float"}}'
```

Provider keys travel per-request in the `X-Provider-Key` header (or come from `<PROVIDER>_API_KEY`
on the server) and are never stored, logged, or pre-filled into the browser. Errors map to status
codes: `422` validation · `401` auth · `429` rate limit · `502` provider · `504` timeout.

## How it works

`generate()` → schema adapter → strategy → provider → **pipeline** → validated object.

- **Strategy** — resolved per provider in order `native` (JSON-schema mode) → `tool_calling` →
  `prompt_repair`, which is the universal floor and needs nothing from the provider.
- **Pipeline** — six ordered stages: `normalize → extract → syntax_repair → deserialize →
  validate → coerce`. Each records a diagnostic, and the confidence score falls as repairs and
  re-asks accumulate.
- **Extending** — `Provider`, `Strategy`, `SchemaAdapter` and `Stage` are the four interfaces;
  most providers need only a preset in `wassitai/providers/__init__.py`.

Errors form one tree under `LLMParserError`: `ConfigurationError` (your bug), `ProviderError`
(the network/provider's), and `ParseError` (the model's).

## Providers

Built-in presets speak the OpenAI wire format: **groq**, **openai**, **deepseek**, **openrouter**.
Any other OpenAI-compatible endpoint (Together, xAI, Ollama, vLLM, local) works by passing
`base_url=` and `model=`. Native adapters for Anthropic, Gemini and Bedrock are planned behind the
same `Provider` interface.

## Why Wassitai

| | Wassitai | Typical structured-output library |
|---|---|---|
| **Providers** | 4 presets, plus any OpenAI-compatible endpoint via `base_url=` | An adapter to write per provider |
| **Output** | A validated object, or the same data as JSON or XML text | JSON only |
| **Validation** | Strict *and* flexible, chosen per call | One fixed mode |
| **On a bad answer** | Repairs it, then re-asks the model | Raises and stops |
| **When it fails** | Per-stage diagnostics and a confidence score | A single validation error |
| **Ships a UI** | Playground + beginner's guide | Library only |

### Key differentiators

1. **Dual validation modes** — coerce close-enough values while prototyping, enforce exact types in
   production, without changing your schema.
2. **It repairs before it gives up** — malformed JSON is fixed, and a reply that still doesn't fit
   the schema is sent back to the model rather than thrown at you.
3. **It tells the truth about what happened** — every stage records a diagnostic, and confidence
   drops as repairs and re-asks accumulate, so a "valid" object you shouldn't trust is visible.
4. **Any OpenAI-compatible endpoint** — Together, xAI, Ollama, vLLM or a local server need a URL
   and a model name, not an adapter.
5. **JSON *and* XML** — the same validated object renders to either.
6. **A playground, not just a library** — a browser console that exercises the real REST API, with
   a guide written for people who have never called an LLM.

### Use cases

- **Development** — flexible validation for rapid prototyping
- **Production** — strict validation for data integrity
- **Research** — compare providers and strategies on the same prompt and schema
- **Education** — plain-language guide and honest error messages
- **Experimentation** — the playground, with no code to write

## From a checkout

```bash
pip install -e ".[api,dev]"              # editable install with the API and test deps
pytest                                   # 329 tests, no network calls
pytest -m unit                           # or: integration, e2e, provider
pytest --cov=wassitai --cov-report=html
```

CI runs the same suite on Python 3.9–3.13, plus `ruff` over the current engine, the REST API and the
`llm_parser` alias (the pre-0.2 `core/` and `llms/` modules are grandfathered).

## Legacy API (removed in 2.0)

`LLMParser.ask` still works and still supports Gemini and HuggingFace, but is deprecated.

| Old | New |
|---|---|
| `LLMParser("groq", api_key=k).ask(q, schema=S, fmt="json")` | `Client.from_provider("groq", api_key=k).generate(q, schema=S)` |
| `...ask(q, schema=S, many=True)` | `...generate(q, schema=list[S])` |
| `strict_validation=True` | `generate(..., strict=True)` |
| need the JSON string back | `render(obj, "json")` |

The Streamlit UI at `ui/app.py` (`streamlit run ui/app.py`) targets this legacy path; the
playground above is the current one.

## License

MIT.

## Contact

Issues and discussions on [GitHub](https://github.com/kaaaziz/wassitai) ·
kaa.kella.abdelaziz@gmail.com
