wassitai 0.2.0__py3-none-any.whl
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.
- llm_parser/__init__.py +116 -0
- wassitai/__init__.py +50 -0
- wassitai/_version.py +1 -0
- wassitai/api/__init__.py +3 -0
- wassitai/api/app.py +61 -0
- wassitai/api/deps.py +49 -0
- wassitai/api/errors.py +53 -0
- wassitai/api/models.py +42 -0
- wassitai/api/routes.py +93 -0
- wassitai/api/static/app.js +405 -0
- wassitai/api/static/favicon.svg +9 -0
- wassitai/api/static/guide.html +357 -0
- wassitai/api/static/help.js +88 -0
- wassitai/api/static/index.html +379 -0
- wassitai/api/static/styles.css +874 -0
- wassitai/api/static/theme.js +30 -0
- wassitai/api/static/vendor/alpine.min.js +5 -0
- wassitai/api/static/vendor/fonts/instrument-serif-italic-400.woff2 +0 -0
- wassitai/api/static/vendor/fonts/instrument-serif-normal-400.woff2 +0 -0
- wassitai/api/static/vendor/fonts/inter-normal-400.woff2 +0 -0
- wassitai/api/static/vendor/fonts/jetbrains-mono-normal-400.woff2 +0 -0
- wassitai/api/static/vendor/fonts.css +60 -0
- wassitai/client.py +151 -0
- wassitai/config.py +16 -0
- wassitai/core/formatter.py +76 -0
- wassitai/core/formatter_utils.py +78 -0
- wassitai/core/model_parser.py +139 -0
- wassitai/core/model_validator.py +46 -0
- wassitai/core/payload_validator.py +117 -0
- wassitai/core/prompt_builder.py +95 -0
- wassitai/errors.py +90 -0
- wassitai/llm_parser.py +63 -0
- wassitai/llms/__init__.py +22 -0
- wassitai/llms/base.py +12 -0
- wassitai/llms/const.py +8 -0
- wassitai/llms/providers/deepseek_provider.py +21 -0
- wassitai/llms/providers/gemini_provider.py +24 -0
- wassitai/llms/providers/groq_provider.py +28 -0
- wassitai/llms/providers/huggingface_provider.py +56 -0
- wassitai/llms/providers/openai_provider.py +21 -0
- wassitai/llms/providers/openrouter_provider.py +39 -0
- wassitai/pipeline/__init__.py +37 -0
- wassitai/pipeline/base.py +18 -0
- wassitai/pipeline/context.py +25 -0
- wassitai/pipeline/stages/__init__.py +0 -0
- wassitai/pipeline/stages/coerce.py +11 -0
- wassitai/pipeline/stages/deserialize.py +19 -0
- wassitai/pipeline/stages/extract.py +33 -0
- wassitai/pipeline/stages/normalize.py +12 -0
- wassitai/pipeline/stages/syntax_repair.py +17 -0
- wassitai/pipeline/stages/validate.py +23 -0
- wassitai/providers/__init__.py +68 -0
- wassitai/providers/base.py +73 -0
- wassitai/providers/openai_compatible.py +107 -0
- wassitai/providers/transport.py +60 -0
- wassitai/render.py +28 -0
- wassitai/result.py +40 -0
- wassitai/schema/__init__.py +43 -0
- wassitai/schema/base.py +12 -0
- wassitai/schema/dict_adapter.py +80 -0
- wassitai/schema/json_string_adapter.py +37 -0
- wassitai/schema/pydantic_adapter.py +51 -0
- wassitai/schema/spec.py +12 -0
- wassitai/strategies/__init__.py +34 -0
- wassitai/strategies/base.py +15 -0
- wassitai/strategies/native.py +23 -0
- wassitai/strategies/prompt_repair.py +26 -0
- wassitai/strategies/tool_calling.py +27 -0
- wassitai-0.2.0.dist-info/METADATA +224 -0
- wassitai-0.2.0.dist-info/RECORD +73 -0
- wassitai-0.2.0.dist-info/WHEEL +5 -0
- wassitai-0.2.0.dist-info/licenses/LICENSE +21 -0
- wassitai-0.2.0.dist-info/top_level.txt +2 -0
llm_parser/__init__.py
ADDED
|
@@ -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()))
|
wassitai/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .client import Client, parse, try_parse
|
|
5
|
+
from .config import Config
|
|
6
|
+
from .errors import (
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
ConfigurationError,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExtractionError,
|
|
11
|
+
LLMParserError,
|
|
12
|
+
ParseError,
|
|
13
|
+
ProviderError,
|
|
14
|
+
ProviderHTTPError,
|
|
15
|
+
ProviderResponseError,
|
|
16
|
+
ProviderTimeoutError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
RepairError,
|
|
19
|
+
SchemaError,
|
|
20
|
+
SchemaValidationError,
|
|
21
|
+
UnknownProviderError,
|
|
22
|
+
)
|
|
23
|
+
from .render import render
|
|
24
|
+
from .result import Result, Usage
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"__version__",
|
|
28
|
+
"Client",
|
|
29
|
+
"parse",
|
|
30
|
+
"try_parse",
|
|
31
|
+
"Config",
|
|
32
|
+
"Result",
|
|
33
|
+
"Usage",
|
|
34
|
+
"render",
|
|
35
|
+
"Diagnostic",
|
|
36
|
+
"LLMParserError",
|
|
37
|
+
"ConfigurationError",
|
|
38
|
+
"UnknownProviderError",
|
|
39
|
+
"SchemaError",
|
|
40
|
+
"ProviderError",
|
|
41
|
+
"AuthenticationError",
|
|
42
|
+
"RateLimitError",
|
|
43
|
+
"ProviderTimeoutError",
|
|
44
|
+
"ProviderHTTPError",
|
|
45
|
+
"ProviderResponseError",
|
|
46
|
+
"ParseError",
|
|
47
|
+
"ExtractionError",
|
|
48
|
+
"RepairError",
|
|
49
|
+
"SchemaValidationError",
|
|
50
|
+
]
|
wassitai/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
wassitai/api/__init__.py
ADDED
wassitai/api/app.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
7
|
+
from fastapi.responses import FileResponse
|
|
8
|
+
from fastapi.staticfiles import StaticFiles
|
|
9
|
+
|
|
10
|
+
from .._version import __version__
|
|
11
|
+
from .errors import register_exception_handlers
|
|
12
|
+
from .routes import router
|
|
13
|
+
|
|
14
|
+
_STATIC = Path(__file__).parent / "static"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _RevalidatingStaticFiles(StaticFiles):
|
|
18
|
+
"""Serve static assets with ``Cache-Control: no-cache``.
|
|
19
|
+
|
|
20
|
+
Plain ``StaticFiles`` sends no ``Cache-Control``, so browsers apply heuristic
|
|
21
|
+
freshness and can keep running a stale ``app.js``/``styles.css`` after an edit
|
|
22
|
+
without ever re-requesting it. ``no-cache`` means "cache, but always
|
|
23
|
+
revalidate": the browser issues a conditional request and gets a cheap 304
|
|
24
|
+
when nothing changed, or the fresh file the moment it does — so the playground
|
|
25
|
+
never boots old JS against a new page.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
async def get_response(self, path: str, scope): # type: ignore[override]
|
|
29
|
+
response = await super().get_response(path, scope)
|
|
30
|
+
response.headers.setdefault("Cache-Control", "no-cache")
|
|
31
|
+
return response
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_app() -> FastAPI:
|
|
35
|
+
app = FastAPI(
|
|
36
|
+
title="Wassitai API",
|
|
37
|
+
# One version for the whole distribution: a hardcoded literal here drifts
|
|
38
|
+
# from the package the moment either is bumped, and it is what `/docs` and
|
|
39
|
+
# the OpenAPI schema advertise.
|
|
40
|
+
version=__version__,
|
|
41
|
+
description="Structured generation & LLM-output parsing over HTTP.",
|
|
42
|
+
)
|
|
43
|
+
app.add_middleware(
|
|
44
|
+
CORSMiddleware,
|
|
45
|
+
allow_origins=["*"],
|
|
46
|
+
allow_methods=["*"],
|
|
47
|
+
allow_headers=["*"],
|
|
48
|
+
)
|
|
49
|
+
app.include_router(router)
|
|
50
|
+
register_exception_handlers(app)
|
|
51
|
+
|
|
52
|
+
app.mount("/static", _RevalidatingStaticFiles(directory=str(_STATIC)), name="static")
|
|
53
|
+
|
|
54
|
+
@app.get("/", include_in_schema=False)
|
|
55
|
+
def index() -> FileResponse:
|
|
56
|
+
return FileResponse(str(_STATIC / "index.html"), headers={"Cache-Control": "no-cache"})
|
|
57
|
+
|
|
58
|
+
return app
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
app = create_app()
|
wassitai/api/deps.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from wassitai import Client
|
|
7
|
+
from wassitai.config import Config
|
|
8
|
+
from wassitai.errors import AuthenticationError
|
|
9
|
+
from wassitai.strategies.native import NativeStructuredOutputStrategy
|
|
10
|
+
from wassitai.strategies.prompt_repair import PromptAndRepairStrategy
|
|
11
|
+
from wassitai.strategies.tool_calling import ToolCallingStrategy
|
|
12
|
+
|
|
13
|
+
_STRATEGIES = {
|
|
14
|
+
"native": NativeStructuredOutputStrategy,
|
|
15
|
+
"tool_calling": ToolCallingStrategy,
|
|
16
|
+
"prompt_repair": PromptAndRepairStrategy,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_key(provider: str, header_key: Optional[str]) -> str:
|
|
21
|
+
if header_key:
|
|
22
|
+
return header_key
|
|
23
|
+
env = os.getenv(f"{provider.upper()}_API_KEY")
|
|
24
|
+
if env:
|
|
25
|
+
return env
|
|
26
|
+
raise AuthenticationError(
|
|
27
|
+
f"No API key for '{provider}'. Supply the X-Provider-Key header "
|
|
28
|
+
f"or set {provider.upper()}_API_KEY on the server."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_client(
|
|
33
|
+
provider: str,
|
|
34
|
+
key: str,
|
|
35
|
+
model: Optional[str] = None,
|
|
36
|
+
max_retries: Optional[int] = None,
|
|
37
|
+
strict: bool = False,
|
|
38
|
+
strategy: Optional[str] = None,
|
|
39
|
+
) -> Client:
|
|
40
|
+
strat = _STRATEGIES[strategy]() if strategy in _STRATEGIES else None
|
|
41
|
+
cfg = Config(
|
|
42
|
+
strict=strict,
|
|
43
|
+
max_retries=1 if max_retries is None else max_retries,
|
|
44
|
+
strategy=strat,
|
|
45
|
+
)
|
|
46
|
+
kw = {"api_key": key, "config": cfg}
|
|
47
|
+
if model:
|
|
48
|
+
kw["model"] = model
|
|
49
|
+
return Client.from_provider(provider, **kw)
|
wassitai/api/errors.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import Request
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
|
|
6
|
+
from wassitai.errors import (
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
LLMParserError,
|
|
9
|
+
ParseError,
|
|
10
|
+
ProviderHTTPError,
|
|
11
|
+
ProviderResponseError,
|
|
12
|
+
ProviderTimeoutError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
SchemaError,
|
|
15
|
+
SchemaValidationError,
|
|
16
|
+
UnknownProviderError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Order matters: most specific first.
|
|
20
|
+
_STATUS_TABLE = [
|
|
21
|
+
(AuthenticationError, 401),
|
|
22
|
+
(RateLimitError, 429),
|
|
23
|
+
(ProviderTimeoutError, 504),
|
|
24
|
+
(ProviderHTTPError, 502),
|
|
25
|
+
(ProviderResponseError, 502),
|
|
26
|
+
(UnknownProviderError, 400),
|
|
27
|
+
(SchemaError, 422),
|
|
28
|
+
(SchemaValidationError, 422),
|
|
29
|
+
(ParseError, 422),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def status_for(exc: LLMParserError) -> int:
|
|
34
|
+
for cls, code in _STATUS_TABLE:
|
|
35
|
+
if isinstance(exc, cls):
|
|
36
|
+
return code
|
|
37
|
+
return 500
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def error_body(exc: LLMParserError) -> dict:
|
|
41
|
+
body: dict = {"error": str(exc), "error_type": type(exc).__name__}
|
|
42
|
+
if isinstance(exc, ParseError):
|
|
43
|
+
body["diagnostics"] = [
|
|
44
|
+
{"stage": d.stage, "level": d.level, "message": d.message} for d in exc.diagnostics
|
|
45
|
+
]
|
|
46
|
+
body["confidence"] = exc.confidence
|
|
47
|
+
return body
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def register_exception_handlers(app) -> None:
|
|
51
|
+
@app.exception_handler(LLMParserError)
|
|
52
|
+
async def _handle(request: Request, exc: LLMParserError):
|
|
53
|
+
return JSONResponse(status_code=status_for(exc), content=error_body(exc))
|
wassitai/api/models.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, Union
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GenerateRequest(BaseModel):
|
|
9
|
+
provider: str
|
|
10
|
+
prompt: str
|
|
11
|
+
schema_: Union[dict, str] = Field(alias="schema")
|
|
12
|
+
model: Optional[str] = None
|
|
13
|
+
strict: bool = False
|
|
14
|
+
max_retries: Optional[int] = None
|
|
15
|
+
strategy: Optional[str] = None
|
|
16
|
+
render: Optional[str] = None # "json" | "xml" | None (return the object)
|
|
17
|
+
|
|
18
|
+
model_config = {"populate_by_name": True}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ParseRequest(BaseModel):
|
|
22
|
+
text: str
|
|
23
|
+
schema_: Union[dict, str] = Field(alias="schema")
|
|
24
|
+
strict: bool = False
|
|
25
|
+
render: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
model_config = {"populate_by_name": True}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GenerateResponse(BaseModel):
|
|
31
|
+
ok: bool
|
|
32
|
+
value: Any = None
|
|
33
|
+
rendered: Optional[str] = None
|
|
34
|
+
confidence: float = 1.0
|
|
35
|
+
diagnostics: list[dict[str, Any]] = []
|
|
36
|
+
usage: Optional[dict[str, int]] = None
|
|
37
|
+
error: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ProviderInfo(BaseModel):
|
|
41
|
+
name: str
|
|
42
|
+
capabilities: dict[str, Any]
|
wassitai/api/routes.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Header
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from wassitai import __version__, try_parse
|
|
9
|
+
from wassitai import render as render_obj
|
|
10
|
+
from wassitai.providers import _PRESETS
|
|
11
|
+
|
|
12
|
+
from .deps import build_client, resolve_key
|
|
13
|
+
from .models import GenerateRequest, GenerateResponse, ParseRequest
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _jsonable(value: object) -> object:
|
|
19
|
+
if isinstance(value, BaseModel):
|
|
20
|
+
return value.model_dump()
|
|
21
|
+
if isinstance(value, list):
|
|
22
|
+
return [_jsonable(v) for v in value]
|
|
23
|
+
return value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _diagnostics(result) -> list:
|
|
27
|
+
return [
|
|
28
|
+
{"stage": d.stage, "level": d.level, "message": d.message} for d in result.diagnostics
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.get("/health")
|
|
33
|
+
def health() -> dict:
|
|
34
|
+
return {"status": "ok", "version": __version__}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.post("/v1/parse", response_model=GenerateResponse)
|
|
38
|
+
def parse_route(req: ParseRequest) -> GenerateResponse:
|
|
39
|
+
result = try_parse(req.text, req.schema_, strict=req.strict)
|
|
40
|
+
value = result.unwrap() # raises the typed error; mapped to a status by the handler
|
|
41
|
+
rendered = render_obj(value, fmt=req.render) if req.render else None
|
|
42
|
+
return GenerateResponse(
|
|
43
|
+
ok=True,
|
|
44
|
+
value=_jsonable(value),
|
|
45
|
+
rendered=rendered,
|
|
46
|
+
confidence=result.confidence,
|
|
47
|
+
diagnostics=_diagnostics(result),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.post("/v1/generate", response_model=GenerateResponse)
|
|
52
|
+
def generate_route(
|
|
53
|
+
req: GenerateRequest,
|
|
54
|
+
x_provider_key: Optional[str] = Header(default=None),
|
|
55
|
+
) -> GenerateResponse:
|
|
56
|
+
key = resolve_key(req.provider, x_provider_key) # -> 401 if missing
|
|
57
|
+
client = build_client(
|
|
58
|
+
req.provider,
|
|
59
|
+
key,
|
|
60
|
+
model=req.model,
|
|
61
|
+
max_retries=req.max_retries,
|
|
62
|
+
strict=req.strict,
|
|
63
|
+
strategy=req.strategy,
|
|
64
|
+
)
|
|
65
|
+
result = client.try_generate(req.prompt, schema=req.schema_)
|
|
66
|
+
value = result.unwrap() # raises the typed error; mapped to a status by the handler
|
|
67
|
+
rendered = render_obj(value, fmt=req.render) if req.render else None
|
|
68
|
+
return GenerateResponse(
|
|
69
|
+
ok=True,
|
|
70
|
+
value=_jsonable(value),
|
|
71
|
+
rendered=rendered,
|
|
72
|
+
confidence=result.confidence,
|
|
73
|
+
diagnostics=_diagnostics(result),
|
|
74
|
+
usage=(result.usage.__dict__ if result.usage else None),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@router.get("/v1/providers")
|
|
79
|
+
def list_providers() -> dict:
|
|
80
|
+
return {
|
|
81
|
+
"providers": [
|
|
82
|
+
{
|
|
83
|
+
"name": name,
|
|
84
|
+
"capabilities": {
|
|
85
|
+
"openai_compatible": True,
|
|
86
|
+
"tool_calling": True,
|
|
87
|
+
"json_mode": True,
|
|
88
|
+
},
|
|
89
|
+
"default_model": preset[1],
|
|
90
|
+
}
|
|
91
|
+
for name, preset in _PRESETS.items()
|
|
92
|
+
]
|
|
93
|
+
}
|