wassitai 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. wassitai-0.2.0/CHANGELOG.md +46 -0
  2. wassitai-0.2.0/LICENSE +21 -0
  3. wassitai-0.2.0/MANIFEST.in +9 -0
  4. wassitai-0.2.0/PKG-INFO +224 -0
  5. wassitai-0.2.0/README.md +184 -0
  6. wassitai-0.2.0/llm_parser/__init__.py +116 -0
  7. wassitai-0.2.0/pyproject.toml +86 -0
  8. wassitai-0.2.0/pytest.ini +47 -0
  9. wassitai-0.2.0/setup.cfg +4 -0
  10. wassitai-0.2.0/tests/README.md +354 -0
  11. wassitai-0.2.0/tests/__init__.py +15 -0
  12. wassitai-0.2.0/tests/api/__init__.py +0 -0
  13. wassitai-0.2.0/tests/api/conftest.py +9 -0
  14. wassitai-0.2.0/tests/api/test_deps.py +20 -0
  15. wassitai-0.2.0/tests/api/test_error_mapping.py +23 -0
  16. wassitai-0.2.0/tests/api/test_generate_route.py +63 -0
  17. wassitai-0.2.0/tests/api/test_health.py +6 -0
  18. wassitai-0.2.0/tests/api/test_models.py +17 -0
  19. wassitai-0.2.0/tests/api/test_parse_route.py +36 -0
  20. wassitai-0.2.0/tests/api/test_providers_route.py +5 -0
  21. wassitai-0.2.0/tests/api/test_spa.py +24 -0
  22. wassitai-0.2.0/tests/conftest.py +411 -0
  23. wassitai-0.2.0/tests/e2e/scenarios/test_real_world_scenarios.py +568 -0
  24. wassitai-0.2.0/tests/fixtures/__init__.py +6 -0
  25. wassitai-0.2.0/tests/fixtures/test_data.py +274 -0
  26. wassitai-0.2.0/tests/integration/providers/test_llm_parser_integration.py +661 -0
  27. wassitai-0.2.0/tests/integration/test_strict_validation_ui.py +388 -0
  28. wassitai-0.2.0/tests/integration/test_ui_error_handling.py +386 -0
  29. wassitai-0.2.0/tests/newengine/__init__.py +0 -0
  30. wassitai-0.2.0/tests/newengine/fakes.py +16 -0
  31. wassitai-0.2.0/tests/newengine/test_client.py +49 -0
  32. wassitai-0.2.0/tests/newengine/test_compat_shim.py +41 -0
  33. wassitai-0.2.0/tests/newengine/test_config.py +13 -0
  34. wassitai-0.2.0/tests/newengine/test_dict_adapter.py +46 -0
  35. wassitai-0.2.0/tests/newengine/test_errors.py +29 -0
  36. wassitai-0.2.0/tests/newengine/test_import_alias.py +106 -0
  37. wassitai-0.2.0/tests/newengine/test_json_string_adapter.py +18 -0
  38. wassitai-0.2.0/tests/newengine/test_native_strategy.py +18 -0
  39. wassitai-0.2.0/tests/newengine/test_openai_compatible.py +87 -0
  40. wassitai-0.2.0/tests/newengine/test_packaging.py +11 -0
  41. wassitai-0.2.0/tests/newengine/test_pipeline.py +28 -0
  42. wassitai-0.2.0/tests/newengine/test_pipeline_base.py +20 -0
  43. wassitai-0.2.0/tests/newengine/test_pipeline_properties.py +25 -0
  44. wassitai-0.2.0/tests/newengine/test_prompt_repair_strategy.py +17 -0
  45. wassitai-0.2.0/tests/newengine/test_provider_base.py +32 -0
  46. wassitai-0.2.0/tests/newengine/test_provider_registry.py +30 -0
  47. wassitai-0.2.0/tests/newengine/test_pydantic_adapter.py +39 -0
  48. wassitai-0.2.0/tests/newengine/test_render.py +31 -0
  49. wassitai-0.2.0/tests/newengine/test_result.py +22 -0
  50. wassitai-0.2.0/tests/newengine/test_review_fixes.py +109 -0
  51. wassitai-0.2.0/tests/newengine/test_schema_registry.py +14 -0
  52. wassitai-0.2.0/tests/newengine/test_smoke_public_api.py +16 -0
  53. wassitai-0.2.0/tests/newengine/test_stages_parsing.py +37 -0
  54. wassitai-0.2.0/tests/newengine/test_stages_validation.py +34 -0
  55. wassitai-0.2.0/tests/newengine/test_strategy_resolver.py +31 -0
  56. wassitai-0.2.0/tests/newengine/test_tool_calling_strategy.py +31 -0
  57. wassitai-0.2.0/tests/newengine/test_transport.py +44 -0
  58. wassitai-0.2.0/tests/unit/core/test_formatter.py +435 -0
  59. wassitai-0.2.0/tests/unit/core/test_json_schema_validation.py +382 -0
  60. wassitai-0.2.0/tests/unit/core/test_model_parser.py +467 -0
  61. wassitai-0.2.0/tests/unit/core/test_strict_format.py +244 -0
  62. wassitai-0.2.0/tests/unit/core/test_strict_validation.py +427 -0
  63. wassitai-0.2.0/tests/unit/llms/test_providers.py +461 -0
  64. wassitai-0.2.0/tests/utils/__init__.py +6 -0
  65. wassitai-0.2.0/tests/utils/test_helpers.py +358 -0
  66. wassitai-0.2.0/wassitai/__init__.py +50 -0
  67. wassitai-0.2.0/wassitai/_version.py +1 -0
  68. wassitai-0.2.0/wassitai/api/__init__.py +3 -0
  69. wassitai-0.2.0/wassitai/api/app.py +61 -0
  70. wassitai-0.2.0/wassitai/api/deps.py +49 -0
  71. wassitai-0.2.0/wassitai/api/errors.py +53 -0
  72. wassitai-0.2.0/wassitai/api/models.py +42 -0
  73. wassitai-0.2.0/wassitai/api/routes.py +93 -0
  74. wassitai-0.2.0/wassitai/api/static/app.js +405 -0
  75. wassitai-0.2.0/wassitai/api/static/favicon.svg +9 -0
  76. wassitai-0.2.0/wassitai/api/static/guide.html +357 -0
  77. wassitai-0.2.0/wassitai/api/static/help.js +88 -0
  78. wassitai-0.2.0/wassitai/api/static/index.html +379 -0
  79. wassitai-0.2.0/wassitai/api/static/styles.css +874 -0
  80. wassitai-0.2.0/wassitai/api/static/theme.js +30 -0
  81. wassitai-0.2.0/wassitai/api/static/vendor/alpine.min.js +5 -0
  82. wassitai-0.2.0/wassitai/api/static/vendor/fonts/instrument-serif-italic-400.woff2 +0 -0
  83. wassitai-0.2.0/wassitai/api/static/vendor/fonts/instrument-serif-normal-400.woff2 +0 -0
  84. wassitai-0.2.0/wassitai/api/static/vendor/fonts/inter-normal-400.woff2 +0 -0
  85. wassitai-0.2.0/wassitai/api/static/vendor/fonts/jetbrains-mono-normal-400.woff2 +0 -0
  86. wassitai-0.2.0/wassitai/api/static/vendor/fonts.css +60 -0
  87. wassitai-0.2.0/wassitai/client.py +151 -0
  88. wassitai-0.2.0/wassitai/config.py +16 -0
  89. wassitai-0.2.0/wassitai/core/formatter.py +76 -0
  90. wassitai-0.2.0/wassitai/core/formatter_utils.py +78 -0
  91. wassitai-0.2.0/wassitai/core/model_parser.py +139 -0
  92. wassitai-0.2.0/wassitai/core/model_validator.py +46 -0
  93. wassitai-0.2.0/wassitai/core/payload_validator.py +117 -0
  94. wassitai-0.2.0/wassitai/core/prompt_builder.py +95 -0
  95. wassitai-0.2.0/wassitai/errors.py +90 -0
  96. wassitai-0.2.0/wassitai/llm_parser.py +63 -0
  97. wassitai-0.2.0/wassitai/llms/__init__.py +22 -0
  98. wassitai-0.2.0/wassitai/llms/base.py +12 -0
  99. wassitai-0.2.0/wassitai/llms/const.py +8 -0
  100. wassitai-0.2.0/wassitai/llms/providers/deepseek_provider.py +21 -0
  101. wassitai-0.2.0/wassitai/llms/providers/gemini_provider.py +24 -0
  102. wassitai-0.2.0/wassitai/llms/providers/groq_provider.py +28 -0
  103. wassitai-0.2.0/wassitai/llms/providers/huggingface_provider.py +56 -0
  104. wassitai-0.2.0/wassitai/llms/providers/openai_provider.py +21 -0
  105. wassitai-0.2.0/wassitai/llms/providers/openrouter_provider.py +39 -0
  106. wassitai-0.2.0/wassitai/pipeline/__init__.py +37 -0
  107. wassitai-0.2.0/wassitai/pipeline/base.py +18 -0
  108. wassitai-0.2.0/wassitai/pipeline/context.py +25 -0
  109. wassitai-0.2.0/wassitai/pipeline/stages/__init__.py +0 -0
  110. wassitai-0.2.0/wassitai/pipeline/stages/coerce.py +11 -0
  111. wassitai-0.2.0/wassitai/pipeline/stages/deserialize.py +19 -0
  112. wassitai-0.2.0/wassitai/pipeline/stages/extract.py +33 -0
  113. wassitai-0.2.0/wassitai/pipeline/stages/normalize.py +12 -0
  114. wassitai-0.2.0/wassitai/pipeline/stages/syntax_repair.py +17 -0
  115. wassitai-0.2.0/wassitai/pipeline/stages/validate.py +23 -0
  116. wassitai-0.2.0/wassitai/providers/__init__.py +68 -0
  117. wassitai-0.2.0/wassitai/providers/base.py +73 -0
  118. wassitai-0.2.0/wassitai/providers/openai_compatible.py +107 -0
  119. wassitai-0.2.0/wassitai/providers/transport.py +60 -0
  120. wassitai-0.2.0/wassitai/render.py +28 -0
  121. wassitai-0.2.0/wassitai/result.py +40 -0
  122. wassitai-0.2.0/wassitai/schema/__init__.py +43 -0
  123. wassitai-0.2.0/wassitai/schema/base.py +12 -0
  124. wassitai-0.2.0/wassitai/schema/dict_adapter.py +80 -0
  125. wassitai-0.2.0/wassitai/schema/json_string_adapter.py +37 -0
  126. wassitai-0.2.0/wassitai/schema/pydantic_adapter.py +51 -0
  127. wassitai-0.2.0/wassitai/schema/spec.py +12 -0
  128. wassitai-0.2.0/wassitai/strategies/__init__.py +34 -0
  129. wassitai-0.2.0/wassitai/strategies/base.py +15 -0
  130. wassitai-0.2.0/wassitai/strategies/native.py +23 -0
  131. wassitai-0.2.0/wassitai/strategies/prompt_repair.py +26 -0
  132. wassitai-0.2.0/wassitai/strategies/tool_calling.py +27 -0
  133. wassitai-0.2.0/wassitai.egg-info/PKG-INFO +224 -0
  134. wassitai-0.2.0/wassitai.egg-info/SOURCES.txt +135 -0
  135. wassitai-0.2.0/wassitai.egg-info/dependency_links.txt +1 -0
  136. wassitai-0.2.0/wassitai.egg-info/requires.txt +17 -0
  137. wassitai-0.2.0/wassitai.egg-info/top_level.txt +2 -0
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. This project adheres to
4
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+
6
+ ## 0.2.0
7
+
8
+ First release published to PyPI, as **`wassitai`**.
9
+
10
+ ### Renamed
11
+
12
+ - **The distribution and import package are now `wassitai`.** Install with `pip install wassitai`
13
+ and `import wassitai`. The previous name, `llm_parser`, was never published to PyPI.
14
+ - `llm_parser` remains importable as a **deprecated alias** and emits a `DeprecationWarning`. It
15
+ forwards every attribute and submodule to `wassitai` — `from llm_parser import Client`,
16
+ `from llm_parser.llm_parser import LLMParser` and `llm_parser.core.formatter` all resolve to the
17
+ same objects as their `wassitai.*` counterparts, so pickles, `isinstance` checks and module-level
18
+ state are shared, not duplicated. The alias is removed in 2.0.
19
+ - **Breaking: the REST API moved from the top-level `api` package to `wassitai.api`.** Run it with
20
+ `uvicorn wassitai.api.app:app` and import from `wassitai.api.*`. Shipping a top-level `api` module
21
+ would have squatted a very generic import name in every environment that installed this package.
22
+
23
+ ### Added
24
+
25
+ - The new engine: `Client` / `parse` / `try_parse` / `render`, `Result` with per-stage diagnostics and
26
+ a confidence score, and the four extension interfaces (`Provider`, `Strategy`, `SchemaAdapter`,
27
+ `Stage`).
28
+ - REST API and browser playground (`pip install "wassitai[api]"`).
29
+ - Packaging metadata for publication: long description, license, project URLs and classifiers, plus a
30
+ `LICENSE` file (MIT, as the README already declared).
31
+
32
+ ### Deprecated
33
+
34
+ - `LLMParser.ask` — use `Client.generate` / `parse`. Still works, still supports Gemini and
35
+ HuggingFace, removed in 2.0.
36
+ - The `wassitai.llms.providers.*` classes — new providers are presets or `Provider` implementations
37
+ under `wassitai/providers/`.
38
+
39
+ ### Upgrading from an `llm_parser` checkout
40
+
41
+ ```bash
42
+ pip uninstall llm_parser # pip will not replace it: the distribution name changed
43
+ pip install wassitai
44
+ ```
45
+
46
+ Nothing else is required — existing `import llm_parser` code keeps working until 2.0, with a warning.
wassitai-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Abdelaziz Kella
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ # The sdist is what distro packagers and `pip install --no-binary` build from, so
2
+ # it carries the things the wheel has no need for: the changelog, and the test
3
+ # suite that proves the tarball is what it claims to be. `packages.find` still
4
+ # excludes `tests*` from the *installed* set -- shipping them here does not put a
5
+ # top-level `tests` package into anyone's site-packages.
6
+ include CHANGELOG.md
7
+ recursive-include tests *.py
8
+ include tests/README.md
9
+ include pytest.ini
@@ -0,0 +1,224 @@
1
+ Metadata-Version: 2.4
2
+ Name: wassitai
3
+ Version: 0.2.0
4
+ Summary: Universal structured-generation & LLM-output parsing framework
5
+ Author-email: Abdelaziz Kella <kaa.kella.abdelaziz@gmail.com>, Abdelaziz Kella <a.kella@univ-chlef.dz>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kaaaziz/wassitai
8
+ Project-URL: Repository, https://github.com/kaaaziz/wassitai
9
+ Project-URL: Issues, https://github.com/kaaaziz/wassitai/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: pydantic>=2
25
+ Requires-Dist: httpx>=0.24
26
+ Requires-Dist: json_repair>=0.25
27
+ Requires-Dist: xmltodict>=0.13
28
+ Provides-Extra: api
29
+ Requires-Dist: fastapi>=0.110; extra == "api"
30
+ Requires-Dist: uvicorn[standard]>=0.29; extra == "api"
31
+ Requires-Dist: python-multipart>=0.0.9; extra == "api"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7; extra == "dev"
34
+ Requires-Dist: pytest-cov; extra == "dev"
35
+ Requires-Dist: hypothesis>=6; extra == "dev"
36
+ Requires-Dist: respx>=0.21; extra == "dev"
37
+ Requires-Dist: ruff<0.17,>=0.16; extra == "dev"
38
+ Requires-Dist: mypy; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # Wassitai
42
+
43
+ Ask an LLM a question, get back a **validated Python object** — not a string you have to parse.
44
+
45
+ Wassitai sends your prompt with a schema, picks the best structured-output method the provider
46
+ supports, then extracts, repairs, validates and coerces the reply through a deterministic pipeline.
47
+ If the result still doesn't fit the schema, it re-asks the model. Ships as the importable package
48
+ `wassitai`, with a REST API and a browser playground on top.
49
+
50
+ ```bash
51
+ pip install wassitai # library
52
+ pip install "wassitai[api]" # + REST API and playground
53
+ ```
54
+
55
+ Code written against the pre-0.2 name still works: `llm_parser` remains importable as a deprecated
56
+ alias for `wassitai` until 2.0. If you have an old editable install of the `llm_parser` *distribution*,
57
+ run `pip uninstall llm_parser` first — pip won't replace it, and its source tree would keep shadowing
58
+ the alias.
59
+
60
+ ## Features
61
+
62
+ - 🤖 **Any OpenAI-compatible provider** — Groq, OpenAI, DeepSeek and OpenRouter as presets;
63
+ Together, xAI, Ollama, vLLM or a local server with just `base_url=` and `model=`
64
+ - 🧱 **Five ways to write a schema** — Pydantic model, `{"field": "type"}` dict, JSON Schema, or a
65
+ JSON or XML schema string
66
+ - 🔁 **Object, JSON or XML out** — the same validated result, rendered however you need it
67
+ - 📊 **Lists** — ask for `list[Invoice]`, get a list back
68
+ - 🧪 **Repairs before it fails** — lifts JSON out of markdown fences and prose, fixes trailing
69
+ commas, smart quotes and Python-repr output, then re-asks the model if it still doesn't fit
70
+ - 🔒 **Strict or flexible validation** — exact types for production, coercion while prototyping
71
+ - 💡 **Diagnostics you can act on** — every stage reports what it did, a confidence score falls as
72
+ repairs accumulate, and errors are a typed tree with fixes in the message
73
+ - 🎯 **Pydantic all the way through** — your model is the schema, the validator and the return type
74
+ - 🌐 **A playground, not just a library** — browser console plus a beginner's guide
75
+ - ✅ **329 tests** — unit, integration and end-to-end, with no network calls
76
+
77
+ ## Python
78
+
79
+ ```python
80
+ from wassitai import Client
81
+ from pydantic import BaseModel
82
+
83
+ class Invoice(BaseModel):
84
+ number: str
85
+ total: float
86
+
87
+ client = Client.from_provider("groq", api_key="…", model="llama-3.3-70b-versatile")
88
+ invoice = client.generate("Extract the invoice from: …", schema=Invoice) # -> Invoice
89
+
90
+ # A list, by asking for a list schema:
91
+ invoices = client.generate("Extract all invoices: …", schema=list[Invoice])
92
+
93
+ # Never raises — for batch pipelines:
94
+ res = client.try_generate("…", schema=Invoice) # -> Result[Invoice]
95
+ if res.ok:
96
+ print(res.value, res.confidence)
97
+
98
+ # Text you already have, no provider call:
99
+ from wassitai import parse, render
100
+ invoice = parse('{"number": "A1", "total": 9.5}', Invoice)
101
+ text = render(invoice, "json")
102
+ ```
103
+
104
+ A schema can be a Pydantic model, a plain `{"field": "type"}` dict, or a JSON Schema.
105
+ `strict=True` enforces exact types; the default coerces close-enough values (`"3"` → `3`).
106
+ `max_retries` caps how often the model is re-asked (default 1).
107
+
108
+ ## REST API & playground
109
+
110
+ ```bash
111
+ uvicorn wassitai.api.app:app --reload
112
+ ```
113
+
114
+ Playground at `http://localhost:8000/`, beginner's guide at `/static/guide.html`,
115
+ OpenAPI reference at `/docs`.
116
+
117
+ | Method | Path | Does |
118
+ |---|---|---|
119
+ | `POST` | `/v1/generate` | prompt + schema → validated object |
120
+ | `POST` | `/v1/parse` | text + schema → validated object (no provider call) |
121
+ | `GET` | `/v1/providers` | providers and their capabilities |
122
+ | `GET` | `/health` | liveness |
123
+
124
+ ```bash
125
+ curl -X POST http://localhost:8000/v1/generate \
126
+ -H "X-Provider-Key: $GROQ_API_KEY" \
127
+ -H "Content-Type: application/json" \
128
+ -d '{"provider":"groq","prompt":"Extract the invoice: …",
129
+ "schema":{"number":"str","total":"float"}}'
130
+ ```
131
+
132
+ Provider keys travel per-request in the `X-Provider-Key` header (or come from `<PROVIDER>_API_KEY`
133
+ on the server) and are never stored, logged, or pre-filled into the browser. Errors map to status
134
+ codes: `422` validation · `401` auth · `429` rate limit · `502` provider · `504` timeout.
135
+
136
+ ## How it works
137
+
138
+ `generate()` → schema adapter → strategy → provider → **pipeline** → validated object.
139
+
140
+ - **Strategy** — resolved per provider in order `native` (JSON-schema mode) → `tool_calling` →
141
+ `prompt_repair`, which is the universal floor and needs nothing from the provider.
142
+ - **Pipeline** — six ordered stages: `normalize → extract → syntax_repair → deserialize →
143
+ validate → coerce`. Each records a diagnostic, and the confidence score falls as repairs and
144
+ re-asks accumulate.
145
+ - **Extending** — `Provider`, `Strategy`, `SchemaAdapter` and `Stage` are the four interfaces;
146
+ most providers need only a preset in `wassitai/providers/__init__.py`.
147
+
148
+ Errors form one tree under `LLMParserError`: `ConfigurationError` (your bug), `ProviderError`
149
+ (the network/provider's), and `ParseError` (the model's).
150
+
151
+ ## Providers
152
+
153
+ Built-in presets speak the OpenAI wire format: **groq**, **openai**, **deepseek**, **openrouter**.
154
+ Any other OpenAI-compatible endpoint (Together, xAI, Ollama, vLLM, local) works by passing
155
+ `base_url=` and `model=`. Native adapters for Anthropic, Gemini and Bedrock are planned behind the
156
+ same `Provider` interface.
157
+
158
+ ## Why Wassitai
159
+
160
+ | | Wassitai | Typical structured-output library |
161
+ |---|---|---|
162
+ | **Providers** | 4 presets, plus any OpenAI-compatible endpoint via `base_url=` | An adapter to write per provider |
163
+ | **Output** | A validated object, or the same data as JSON or XML text | JSON only |
164
+ | **Validation** | Strict *and* flexible, chosen per call | One fixed mode |
165
+ | **On a bad answer** | Repairs it, then re-asks the model | Raises and stops |
166
+ | **When it fails** | Per-stage diagnostics and a confidence score | A single validation error |
167
+ | **Ships a UI** | Playground + beginner's guide | Library only |
168
+
169
+ ### Key differentiators
170
+
171
+ 1. **Dual validation modes** — coerce close-enough values while prototyping, enforce exact types in
172
+ production, without changing your schema.
173
+ 2. **It repairs before it gives up** — malformed JSON is fixed, and a reply that still doesn't fit
174
+ the schema is sent back to the model rather than thrown at you.
175
+ 3. **It tells the truth about what happened** — every stage records a diagnostic, and confidence
176
+ drops as repairs and re-asks accumulate, so a "valid" object you shouldn't trust is visible.
177
+ 4. **Any OpenAI-compatible endpoint** — Together, xAI, Ollama, vLLM or a local server need a URL
178
+ and a model name, not an adapter.
179
+ 5. **JSON *and* XML** — the same validated object renders to either.
180
+ 6. **A playground, not just a library** — a browser console that exercises the real REST API, with
181
+ a guide written for people who have never called an LLM.
182
+
183
+ ### Use cases
184
+
185
+ - **Development** — flexible validation for rapid prototyping
186
+ - **Production** — strict validation for data integrity
187
+ - **Research** — compare providers and strategies on the same prompt and schema
188
+ - **Education** — plain-language guide and honest error messages
189
+ - **Experimentation** — the playground, with no code to write
190
+
191
+ ## From a checkout
192
+
193
+ ```bash
194
+ pip install -e ".[api,dev]" # editable install with the API and test deps
195
+ pytest # 329 tests, no network calls
196
+ pytest -m unit # or: integration, e2e, provider
197
+ pytest --cov=wassitai --cov-report=html
198
+ ```
199
+
200
+ CI runs the same suite on Python 3.9–3.13, plus `ruff` over the current engine, the REST API and the
201
+ `llm_parser` alias (the pre-0.2 `core/` and `llms/` modules are grandfathered).
202
+
203
+ ## Legacy API (removed in 2.0)
204
+
205
+ `LLMParser.ask` still works and still supports Gemini and HuggingFace, but is deprecated.
206
+
207
+ | Old | New |
208
+ |---|---|
209
+ | `LLMParser("groq", api_key=k).ask(q, schema=S, fmt="json")` | `Client.from_provider("groq", api_key=k).generate(q, schema=S)` |
210
+ | `...ask(q, schema=S, many=True)` | `...generate(q, schema=list[S])` |
211
+ | `strict_validation=True` | `generate(..., strict=True)` |
212
+ | need the JSON string back | `render(obj, "json")` |
213
+
214
+ The Streamlit UI at `ui/app.py` (`streamlit run ui/app.py`) targets this legacy path; the
215
+ playground above is the current one.
216
+
217
+ ## License
218
+
219
+ MIT.
220
+
221
+ ## Contact
222
+
223
+ Issues and discussions on [GitHub](https://github.com/kaaaziz/wassitai) ·
224
+ kaa.kella.abdelaziz@gmail.com
@@ -0,0 +1,184 @@
1
+ # Wassitai
2
+
3
+ Ask an LLM a question, get back a **validated Python object** — not a string you have to parse.
4
+
5
+ Wassitai sends your prompt with a schema, picks the best structured-output method the provider
6
+ supports, then extracts, repairs, validates and coerces the reply through a deterministic pipeline.
7
+ If the result still doesn't fit the schema, it re-asks the model. Ships as the importable package
8
+ `wassitai`, with a REST API and a browser playground on top.
9
+
10
+ ```bash
11
+ pip install wassitai # library
12
+ pip install "wassitai[api]" # + REST API and playground
13
+ ```
14
+
15
+ Code written against the pre-0.2 name still works: `llm_parser` remains importable as a deprecated
16
+ alias for `wassitai` until 2.0. If you have an old editable install of the `llm_parser` *distribution*,
17
+ run `pip uninstall llm_parser` first — pip won't replace it, and its source tree would keep shadowing
18
+ the alias.
19
+
20
+ ## Features
21
+
22
+ - 🤖 **Any OpenAI-compatible provider** — Groq, OpenAI, DeepSeek and OpenRouter as presets;
23
+ Together, xAI, Ollama, vLLM or a local server with just `base_url=` and `model=`
24
+ - 🧱 **Five ways to write a schema** — Pydantic model, `{"field": "type"}` dict, JSON Schema, or a
25
+ JSON or XML schema string
26
+ - 🔁 **Object, JSON or XML out** — the same validated result, rendered however you need it
27
+ - 📊 **Lists** — ask for `list[Invoice]`, get a list back
28
+ - 🧪 **Repairs before it fails** — lifts JSON out of markdown fences and prose, fixes trailing
29
+ commas, smart quotes and Python-repr output, then re-asks the model if it still doesn't fit
30
+ - 🔒 **Strict or flexible validation** — exact types for production, coercion while prototyping
31
+ - 💡 **Diagnostics you can act on** — every stage reports what it did, a confidence score falls as
32
+ repairs accumulate, and errors are a typed tree with fixes in the message
33
+ - 🎯 **Pydantic all the way through** — your model is the schema, the validator and the return type
34
+ - 🌐 **A playground, not just a library** — browser console plus a beginner's guide
35
+ - ✅ **329 tests** — unit, integration and end-to-end, with no network calls
36
+
37
+ ## Python
38
+
39
+ ```python
40
+ from wassitai import Client
41
+ from pydantic import BaseModel
42
+
43
+ class Invoice(BaseModel):
44
+ number: str
45
+ total: float
46
+
47
+ client = Client.from_provider("groq", api_key="…", model="llama-3.3-70b-versatile")
48
+ invoice = client.generate("Extract the invoice from: …", schema=Invoice) # -> Invoice
49
+
50
+ # A list, by asking for a list schema:
51
+ invoices = client.generate("Extract all invoices: …", schema=list[Invoice])
52
+
53
+ # Never raises — for batch pipelines:
54
+ res = client.try_generate("…", schema=Invoice) # -> Result[Invoice]
55
+ if res.ok:
56
+ print(res.value, res.confidence)
57
+
58
+ # Text you already have, no provider call:
59
+ from wassitai import parse, render
60
+ invoice = parse('{"number": "A1", "total": 9.5}', Invoice)
61
+ text = render(invoice, "json")
62
+ ```
63
+
64
+ A schema can be a Pydantic model, a plain `{"field": "type"}` dict, or a JSON Schema.
65
+ `strict=True` enforces exact types; the default coerces close-enough values (`"3"` → `3`).
66
+ `max_retries` caps how often the model is re-asked (default 1).
67
+
68
+ ## REST API & playground
69
+
70
+ ```bash
71
+ uvicorn wassitai.api.app:app --reload
72
+ ```
73
+
74
+ Playground at `http://localhost:8000/`, beginner's guide at `/static/guide.html`,
75
+ OpenAPI reference at `/docs`.
76
+
77
+ | Method | Path | Does |
78
+ |---|---|---|
79
+ | `POST` | `/v1/generate` | prompt + schema → validated object |
80
+ | `POST` | `/v1/parse` | text + schema → validated object (no provider call) |
81
+ | `GET` | `/v1/providers` | providers and their capabilities |
82
+ | `GET` | `/health` | liveness |
83
+
84
+ ```bash
85
+ curl -X POST http://localhost:8000/v1/generate \
86
+ -H "X-Provider-Key: $GROQ_API_KEY" \
87
+ -H "Content-Type: application/json" \
88
+ -d '{"provider":"groq","prompt":"Extract the invoice: …",
89
+ "schema":{"number":"str","total":"float"}}'
90
+ ```
91
+
92
+ Provider keys travel per-request in the `X-Provider-Key` header (or come from `<PROVIDER>_API_KEY`
93
+ on the server) and are never stored, logged, or pre-filled into the browser. Errors map to status
94
+ codes: `422` validation · `401` auth · `429` rate limit · `502` provider · `504` timeout.
95
+
96
+ ## How it works
97
+
98
+ `generate()` → schema adapter → strategy → provider → **pipeline** → validated object.
99
+
100
+ - **Strategy** — resolved per provider in order `native` (JSON-schema mode) → `tool_calling` →
101
+ `prompt_repair`, which is the universal floor and needs nothing from the provider.
102
+ - **Pipeline** — six ordered stages: `normalize → extract → syntax_repair → deserialize →
103
+ validate → coerce`. Each records a diagnostic, and the confidence score falls as repairs and
104
+ re-asks accumulate.
105
+ - **Extending** — `Provider`, `Strategy`, `SchemaAdapter` and `Stage` are the four interfaces;
106
+ most providers need only a preset in `wassitai/providers/__init__.py`.
107
+
108
+ Errors form one tree under `LLMParserError`: `ConfigurationError` (your bug), `ProviderError`
109
+ (the network/provider's), and `ParseError` (the model's).
110
+
111
+ ## Providers
112
+
113
+ Built-in presets speak the OpenAI wire format: **groq**, **openai**, **deepseek**, **openrouter**.
114
+ Any other OpenAI-compatible endpoint (Together, xAI, Ollama, vLLM, local) works by passing
115
+ `base_url=` and `model=`. Native adapters for Anthropic, Gemini and Bedrock are planned behind the
116
+ same `Provider` interface.
117
+
118
+ ## Why Wassitai
119
+
120
+ | | Wassitai | Typical structured-output library |
121
+ |---|---|---|
122
+ | **Providers** | 4 presets, plus any OpenAI-compatible endpoint via `base_url=` | An adapter to write per provider |
123
+ | **Output** | A validated object, or the same data as JSON or XML text | JSON only |
124
+ | **Validation** | Strict *and* flexible, chosen per call | One fixed mode |
125
+ | **On a bad answer** | Repairs it, then re-asks the model | Raises and stops |
126
+ | **When it fails** | Per-stage diagnostics and a confidence score | A single validation error |
127
+ | **Ships a UI** | Playground + beginner's guide | Library only |
128
+
129
+ ### Key differentiators
130
+
131
+ 1. **Dual validation modes** — coerce close-enough values while prototyping, enforce exact types in
132
+ production, without changing your schema.
133
+ 2. **It repairs before it gives up** — malformed JSON is fixed, and a reply that still doesn't fit
134
+ the schema is sent back to the model rather than thrown at you.
135
+ 3. **It tells the truth about what happened** — every stage records a diagnostic, and confidence
136
+ drops as repairs and re-asks accumulate, so a "valid" object you shouldn't trust is visible.
137
+ 4. **Any OpenAI-compatible endpoint** — Together, xAI, Ollama, vLLM or a local server need a URL
138
+ and a model name, not an adapter.
139
+ 5. **JSON *and* XML** — the same validated object renders to either.
140
+ 6. **A playground, not just a library** — a browser console that exercises the real REST API, with
141
+ a guide written for people who have never called an LLM.
142
+
143
+ ### Use cases
144
+
145
+ - **Development** — flexible validation for rapid prototyping
146
+ - **Production** — strict validation for data integrity
147
+ - **Research** — compare providers and strategies on the same prompt and schema
148
+ - **Education** — plain-language guide and honest error messages
149
+ - **Experimentation** — the playground, with no code to write
150
+
151
+ ## From a checkout
152
+
153
+ ```bash
154
+ pip install -e ".[api,dev]" # editable install with the API and test deps
155
+ pytest # 329 tests, no network calls
156
+ pytest -m unit # or: integration, e2e, provider
157
+ pytest --cov=wassitai --cov-report=html
158
+ ```
159
+
160
+ CI runs the same suite on Python 3.9–3.13, plus `ruff` over the current engine, the REST API and the
161
+ `llm_parser` alias (the pre-0.2 `core/` and `llms/` modules are grandfathered).
162
+
163
+ ## Legacy API (removed in 2.0)
164
+
165
+ `LLMParser.ask` still works and still supports Gemini and HuggingFace, but is deprecated.
166
+
167
+ | Old | New |
168
+ |---|---|
169
+ | `LLMParser("groq", api_key=k).ask(q, schema=S, fmt="json")` | `Client.from_provider("groq", api_key=k).generate(q, schema=S)` |
170
+ | `...ask(q, schema=S, many=True)` | `...generate(q, schema=list[S])` |
171
+ | `strict_validation=True` | `generate(..., strict=True)` |
172
+ | need the JSON string back | `render(obj, "json")` |
173
+
174
+ The Streamlit UI at `ui/app.py` (`streamlit run ui/app.py`) targets this legacy path; the
175
+ playground above is the current one.
176
+
177
+ ## License
178
+
179
+ MIT.
180
+
181
+ ## Contact
182
+
183
+ Issues and discussions on [GitHub](https://github.com/kaaaziz/wassitai) ·
184
+ kaa.kella.abdelaziz@gmail.com
@@ -0,0 +1,116 @@
1
+ """Deprecated import alias for :mod:`wassitai`.
2
+
3
+ The distribution was renamed ``llm_parser`` -> ``wassitai`` in 0.2. This package
4
+ exists only so code written against the old name keeps working: it forwards
5
+ every attribute *and* every submodule to :mod:`wassitai`, so both
6
+
7
+ from llm_parser import Client, parse
8
+ from llm_parser.llm_parser import LLMParser
9
+ from llm_parser.core import formatter
10
+
11
+ resolve to the very same objects the ``wassitai.*`` paths give you -- not copies.
12
+ It will be removed in 2.0; import from ``wassitai`` instead.
13
+
14
+ Two deliberate limits, neither of which any caller in this repo relies on:
15
+ ``pkgutil.iter_modules(llm_parser.__path__)`` enumerates nothing (this directory
16
+ really is empty), and ``python -m llm_parser.<mod>`` does not work. Use the
17
+ ``wassitai`` names for either.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import importlib
23
+ import importlib.abc
24
+ import importlib.util
25
+ import sys
26
+ import warnings
27
+
28
+ import wassitai as _wassitai
29
+
30
+ _OLD = "llm_parser"
31
+ _NEW = "wassitai"
32
+
33
+
34
+ class _AliasLoader(importlib.abc.Loader):
35
+ """Loader that hands back an already-imported ``wassitai`` submodule."""
36
+
37
+ def __init__(self, real_name: str) -> None:
38
+ self._real_name = real_name
39
+ self._real_spec = None
40
+
41
+ def create_module(self, spec):
42
+ real = importlib.import_module(self._real_name)
43
+ self._real_spec = real.__spec__
44
+ return real
45
+
46
+ def exec_module(self, module) -> None:
47
+ # The real module was executed under its own name; re-running it would
48
+ # duplicate its state (registries, module-level singletons) under a
49
+ # second identity, which is exactly what this alias exists to prevent.
50
+ #
51
+ # Undo the one thing the import machinery did to it on the way here:
52
+ # `_init_module_attrs` overwrites `__spec__` unconditionally, so
53
+ # importing `llm_parser.core.formatter` would otherwise rename the live
54
+ # `wassitai.core.formatter` and blank its `submodule_search_locations` --
55
+ # breaking `reload()` and `importlib.resources` for callers that never
56
+ # touched the old name. The alias forwards one way only.
57
+ if self._real_spec is not None:
58
+ module.__spec__ = self._real_spec
59
+
60
+
61
+ class _AliasFinder(importlib.abc.MetaPathFinder):
62
+ """Maps ``llm_parser.<sub>`` onto ``wassitai.<sub>``."""
63
+
64
+ # Duck-typed marker rather than isinstance: re-importing this module (tests
65
+ # do, to observe the warning) rebinds the class, so an isinstance check
66
+ # against the fresh class would miss the finder already on sys.meta_path and
67
+ # stack up a duplicate on every reload.
68
+ _wassitai_alias_finder = True
69
+
70
+ def find_spec(self, fullname, path=None, target=None):
71
+ if not fullname.startswith(_OLD + "."):
72
+ return None
73
+ real_name = _NEW + fullname[len(_OLD) :]
74
+ try:
75
+ real = importlib.import_module(real_name)
76
+ except ModuleNotFoundError as exc:
77
+ if exc.name != real_name:
78
+ # The module exists but its *own* imports failed -- a missing
79
+ # optional dependency, say. Swallowing that would report the
80
+ # alias as absent and destroy the real cause.
81
+ raise
82
+ return None # genuinely absent: let the machinery raise as usual
83
+ return importlib.util.spec_from_loader(
84
+ fullname,
85
+ _AliasLoader(real_name),
86
+ is_package=hasattr(real, "__path__"),
87
+ )
88
+
89
+
90
+ # Ahead of the path-based finder, which would otherwise fail to find submodules
91
+ # that no longer live in this (now almost empty) directory.
92
+ if not any(getattr(f, "_wassitai_alias_finder", False) for f in sys.meta_path):
93
+ sys.meta_path.insert(0, _AliasFinder())
94
+
95
+ warnings.warn(
96
+ "The 'llm_parser' package was renamed to 'wassitai'; install it with "
97
+ "`pip install wassitai` and import `wassitai`. The 'llm_parser' alias will "
98
+ "be removed in 2.0.",
99
+ DeprecationWarning,
100
+ stacklevel=2,
101
+ )
102
+
103
+ __all__ = list(_wassitai.__all__)
104
+
105
+
106
+ def __getattr__(name: str):
107
+ # Delegated rather than star-imported so the alias never drifts from the
108
+ # real package's surface as that surface grows.
109
+ try:
110
+ return getattr(_wassitai, name)
111
+ except AttributeError:
112
+ raise AttributeError(f"module {_OLD!r} has no attribute {name!r}") from None
113
+
114
+
115
+ def __dir__():
116
+ return sorted(set(__all__) | set(globals()))