dtxt 0.1.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.
dtxt-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pillyshi
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.
dtxt-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: dtxt
3
+ Version: 0.1.0
4
+ Summary: Schema-centric bidirectional conversion between text and structured data
5
+ Author: pillyshi
6
+ Author-email: pillyshi <pillyshi21@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: pydantic>=2.0
18
+ Requires-Dist: jsonschema>=4.0
19
+ Requires-Dist: dtxt[openai,anthropic,llamacpp] ; extra == 'all'
20
+ Requires-Dist: anthropic>=0.40 ; extra == 'anthropic'
21
+ Requires-Dist: llama-cpp-python>=0.3 ; extra == 'llamacpp'
22
+ Requires-Dist: openai>=1.0 ; extra == 'openai'
23
+ Requires-Python: >=3.10
24
+ Project-URL: Homepage, https://github.com/pillyshi/dtxt
25
+ Project-URL: Repository, https://github.com/pillyshi/dtxt
26
+ Project-URL: Issues, https://github.com/pillyshi/dtxt/issues
27
+ Provides-Extra: all
28
+ Provides-Extra: anthropic
29
+ Provides-Extra: llamacpp
30
+ Provides-Extra: openai
31
+ Description-Content-Type: text/markdown
32
+
33
+ # dtxt
34
+
35
+ Schema-centric bidirectional conversion between text and structured data.
36
+
37
+ `dtxt` is not related to [`dtx`](https://pypi.org/project/dtx/) (an AI red-teaming tool).
38
+
39
+ ## Core features
40
+
41
+ 1. **Schema inference** (`infer_schema`): derive a schema from a collection of texts
42
+ 2. **T2D** (`parse`): convert text into a schema-conformant object
43
+ 3. **D2T** (`render`): convert an object into text
44
+ 4. **Round-trip verification** (`check_roundtrip`): check that
45
+ `parse(render(obj)) ≈ obj` for a given schema and backend
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install dtxt # core only
51
+ pip install dtxt[anthropic] # + Anthropic backend
52
+ pip install dtxt[openai] # + OpenAI backend
53
+ pip install dtxt[llamacpp] # + local GGUF models via llama.cpp
54
+ pip install dtxt[all] # everything
55
+ ```
56
+
57
+ The core package depends only on `pydantic` and `jsonschema`. Backends are
58
+ optional extras, imported lazily.
59
+
60
+ ## Usage
61
+
62
+ ```python
63
+ import dtxt
64
+ from dtxt import Schema
65
+ from dtxt.backends import MockBackend
66
+
67
+ schema = Schema({
68
+ "type": "object",
69
+ "properties": {
70
+ "name": {"type": "string", "x-dtxt-description": "the person's full name"},
71
+ "age": {"type": "integer"},
72
+ },
73
+ "required": ["name", "age"],
74
+ })
75
+
76
+ # Backends can be set globally per function...
77
+ dtxt.configure(parse=MockBackend(), render=MockBackend())
78
+
79
+ # ...or overridden per call via backend=.
80
+ obj = dtxt.parse("Alice is 30 years old.", schema, backend=MockBackend())
81
+ text = dtxt.render({"name": "Alice", "age": 30}, schema)
82
+
83
+ result = dtxt.check_roundtrip({"name": "Alice", "age": 30}, schema)
84
+ result.ok # True if parse(render(obj)) == obj on every schema field
85
+ ```
86
+
87
+ Swap `MockBackend` for a real one:
88
+
89
+ ```python
90
+ dtxt.configure(
91
+ infer=dtxt.backends.Anthropic("claude-sonnet-4-6"),
92
+ parse=dtxt.backends.LlamaCpp("model.gguf", n_ctx=8192),
93
+ render=dtxt.backends.Anthropic("claude-sonnet-4-6"),
94
+ )
95
+ ```
96
+
97
+ `Anthropic` uses forced tool use to get structured output; `OpenAI` uses
98
+ `response_format={"type": "json_schema", ...}`; `LlamaCpp` constrains
99
+ decoding at the grammar level via GBNF. None of them guarantee full schema
100
+ conformance on their own:
101
+
102
+ - Anthropic/OpenAI guarantee valid JSON syntax, not every schema keyword.
103
+ - `LlamaCpp` strips constructs GBNF can't reliably express (`format`,
104
+ `pattern`, deeply nested objects/arrays) from the grammar-facing schema;
105
+ the original schema is still checked afterwards.
106
+
107
+ So all three go through dtxt's retry + validation loop the same way.
108
+ `parse_many` runs concurrently via asyncio for Anthropic/OpenAI, bounded by
109
+ `max_concurrency` (default 8) to avoid tripping rate limits; `LlamaCpp`
110
+ processes it sequentially in-process so its prompt cache stays warm. A
111
+ partial batch failure raises one `ParseError` naming how many texts failed
112
+ and the first failing index, rather than aborting on the first error.
113
+
114
+ Style is controllable at both the schema and call level:
115
+
116
+ ```python
117
+ schema = Schema({
118
+ "type": "object",
119
+ "properties": {"name": {"type": "string"}},
120
+ "required": ["name"],
121
+ "x-dtxt-style": "formal, third person", # schema-wide default
122
+ })
123
+ dtxt.render(obj, schema) # uses "formal, third person"
124
+ dtxt.render(obj, schema, style="casual, upbeat") # overrides it for this call
125
+ ```
126
+
127
+ ## Status
128
+
129
+ Early development (`0.0.x`). M1-M5 of the milestone plan are implemented:
130
+ `Schema`, `parse` / `parse_many` (asyncio-parallel + bounded concurrency
131
+ for API backends), `render` (with schema-level and per-call style
132
+ control), `infer_schema` (sampling + merge, `min_coverage`),
133
+ `check_roundtrip`, `configure`, a mock backend for testing, and the
134
+ Anthropic / OpenAI / llama.cpp backends. Not yet done: publishing to PyPI
135
+ as `0.1.0` -- see `CLAUDE.md` for the milestone plan.
136
+
137
+ ## Development
138
+
139
+ ```bash
140
+ uv sync --dev
141
+ uv run pytest
142
+ uv run ruff check . && uv run ruff format --check .
143
+ uv run mypy src/
144
+ ```
dtxt-0.1.0/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # dtxt
2
+
3
+ Schema-centric bidirectional conversion between text and structured data.
4
+
5
+ `dtxt` is not related to [`dtx`](https://pypi.org/project/dtx/) (an AI red-teaming tool).
6
+
7
+ ## Core features
8
+
9
+ 1. **Schema inference** (`infer_schema`): derive a schema from a collection of texts
10
+ 2. **T2D** (`parse`): convert text into a schema-conformant object
11
+ 3. **D2T** (`render`): convert an object into text
12
+ 4. **Round-trip verification** (`check_roundtrip`): check that
13
+ `parse(render(obj)) ≈ obj` for a given schema and backend
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install dtxt # core only
19
+ pip install dtxt[anthropic] # + Anthropic backend
20
+ pip install dtxt[openai] # + OpenAI backend
21
+ pip install dtxt[llamacpp] # + local GGUF models via llama.cpp
22
+ pip install dtxt[all] # everything
23
+ ```
24
+
25
+ The core package depends only on `pydantic` and `jsonschema`. Backends are
26
+ optional extras, imported lazily.
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ import dtxt
32
+ from dtxt import Schema
33
+ from dtxt.backends import MockBackend
34
+
35
+ schema = Schema({
36
+ "type": "object",
37
+ "properties": {
38
+ "name": {"type": "string", "x-dtxt-description": "the person's full name"},
39
+ "age": {"type": "integer"},
40
+ },
41
+ "required": ["name", "age"],
42
+ })
43
+
44
+ # Backends can be set globally per function...
45
+ dtxt.configure(parse=MockBackend(), render=MockBackend())
46
+
47
+ # ...or overridden per call via backend=.
48
+ obj = dtxt.parse("Alice is 30 years old.", schema, backend=MockBackend())
49
+ text = dtxt.render({"name": "Alice", "age": 30}, schema)
50
+
51
+ result = dtxt.check_roundtrip({"name": "Alice", "age": 30}, schema)
52
+ result.ok # True if parse(render(obj)) == obj on every schema field
53
+ ```
54
+
55
+ Swap `MockBackend` for a real one:
56
+
57
+ ```python
58
+ dtxt.configure(
59
+ infer=dtxt.backends.Anthropic("claude-sonnet-4-6"),
60
+ parse=dtxt.backends.LlamaCpp("model.gguf", n_ctx=8192),
61
+ render=dtxt.backends.Anthropic("claude-sonnet-4-6"),
62
+ )
63
+ ```
64
+
65
+ `Anthropic` uses forced tool use to get structured output; `OpenAI` uses
66
+ `response_format={"type": "json_schema", ...}`; `LlamaCpp` constrains
67
+ decoding at the grammar level via GBNF. None of them guarantee full schema
68
+ conformance on their own:
69
+
70
+ - Anthropic/OpenAI guarantee valid JSON syntax, not every schema keyword.
71
+ - `LlamaCpp` strips constructs GBNF can't reliably express (`format`,
72
+ `pattern`, deeply nested objects/arrays) from the grammar-facing schema;
73
+ the original schema is still checked afterwards.
74
+
75
+ So all three go through dtxt's retry + validation loop the same way.
76
+ `parse_many` runs concurrently via asyncio for Anthropic/OpenAI, bounded by
77
+ `max_concurrency` (default 8) to avoid tripping rate limits; `LlamaCpp`
78
+ processes it sequentially in-process so its prompt cache stays warm. A
79
+ partial batch failure raises one `ParseError` naming how many texts failed
80
+ and the first failing index, rather than aborting on the first error.
81
+
82
+ Style is controllable at both the schema and call level:
83
+
84
+ ```python
85
+ schema = Schema({
86
+ "type": "object",
87
+ "properties": {"name": {"type": "string"}},
88
+ "required": ["name"],
89
+ "x-dtxt-style": "formal, third person", # schema-wide default
90
+ })
91
+ dtxt.render(obj, schema) # uses "formal, third person"
92
+ dtxt.render(obj, schema, style="casual, upbeat") # overrides it for this call
93
+ ```
94
+
95
+ ## Status
96
+
97
+ Early development (`0.0.x`). M1-M5 of the milestone plan are implemented:
98
+ `Schema`, `parse` / `parse_many` (asyncio-parallel + bounded concurrency
99
+ for API backends), `render` (with schema-level and per-call style
100
+ control), `infer_schema` (sampling + merge, `min_coverage`),
101
+ `check_roundtrip`, `configure`, a mock backend for testing, and the
102
+ Anthropic / OpenAI / llama.cpp backends. Not yet done: publishing to PyPI
103
+ as `0.1.0` -- see `CLAUDE.md` for the milestone plan.
104
+
105
+ ## Development
106
+
107
+ ```bash
108
+ uv sync --dev
109
+ uv run pytest
110
+ uv run ruff check . && uv run ruff format --check .
111
+ uv run mypy src/
112
+ ```
@@ -0,0 +1,72 @@
1
+ [project]
2
+ name = "dtxt"
3
+ version = "0.1.0"
4
+ description = "Schema-centric bidirectional conversion between text and structured data"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "pillyshi", email = "pillyshi21@gmail.com" }
10
+ ]
11
+ requires-python = ">=3.10"
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = [
23
+ "pydantic>=2.0",
24
+ "jsonschema>=4.0",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/pillyshi/dtxt"
29
+ Repository = "https://github.com/pillyshi/dtxt"
30
+ Issues = "https://github.com/pillyshi/dtxt/issues"
31
+
32
+ [project.optional-dependencies]
33
+ openai = ["openai>=1.0"]
34
+ anthropic = ["anthropic>=0.40"]
35
+ llamacpp = ["llama-cpp-python>=0.3"]
36
+ all = ["dtxt[openai,anthropic,llamacpp]"]
37
+
38
+ [build-system]
39
+ requires = ["uv_build>=0.11.7,<0.12.0"]
40
+ build-backend = "uv_build"
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "mypy>=1.11",
45
+ "pytest>=8.0",
46
+ "ruff>=0.6",
47
+ "types-jsonschema>=4.0",
48
+ ]
49
+
50
+ [tool.ruff]
51
+ line-length = 100
52
+ target-version = "py310"
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "I", "UP", "B"]
56
+
57
+ [tool.mypy]
58
+ strict = true
59
+ packages = ["dtxt"]
60
+ mypy_path = "src"
61
+ explicit_package_bases = true
62
+
63
+ [[tool.mypy.overrides]]
64
+ module = ["anthropic.*", "openai.*", "llama_cpp.*"]
65
+ ignore_missing_imports = true
66
+
67
+ [tool.pytest.ini_options]
68
+ testpaths = ["tests"]
69
+ markers = [
70
+ "integration: tests that call a real LLM backend (excluded by default)",
71
+ ]
72
+ addopts = "-m 'not integration'"
@@ -0,0 +1,23 @@
1
+ """dtxt: schema-centric bidirectional conversion between text and structured data."""
2
+
3
+ from . import backends
4
+ from ._config import configure
5
+ from .d2t import render
6
+ from .infer import InferError, infer_schema
7
+ from .roundtrip import RoundtripResult, check_roundtrip
8
+ from .schema import Schema
9
+ from .t2d import ParseError, parse, parse_many
10
+
11
+ __all__ = [
12
+ "InferError",
13
+ "ParseError",
14
+ "RoundtripResult",
15
+ "Schema",
16
+ "backends",
17
+ "check_roundtrip",
18
+ "configure",
19
+ "infer_schema",
20
+ "parse",
21
+ "parse_many",
22
+ "render",
23
+ ]
@@ -0,0 +1,55 @@
1
+ """Global backend configuration, one slot per public function.
2
+
3
+ ``configure()`` sets the default; each function's own ``backend=``
4
+ argument overrides it for that single call.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+ from .backends.base import Backend
12
+
13
+
14
+ @dataclass
15
+ class _Config:
16
+ infer: Backend | None = None
17
+ parse: Backend | None = None
18
+ render: Backend | None = None
19
+
20
+
21
+ _config = _Config()
22
+
23
+
24
+ def configure(
25
+ *,
26
+ infer: Backend | None = None,
27
+ parse: Backend | None = None,
28
+ render: Backend | None = None,
29
+ ) -> None:
30
+ """Set the default backend used by ``infer_schema`` / ``parse`` / ``render``."""
31
+ if infer is not None:
32
+ _config.infer = infer
33
+ if parse is not None:
34
+ _config.parse = parse
35
+ if render is not None:
36
+ _config.render = render
37
+
38
+
39
+ def resolve_backend(function: str, override: Backend | None) -> Backend:
40
+ if override is not None:
41
+ return override
42
+ configured: Backend | None = getattr(_config, function)
43
+ if configured is None:
44
+ raise RuntimeError(
45
+ f"No backend configured for '{function}'. "
46
+ f"Call dtxt.configure({function}=...) or pass backend=... explicitly."
47
+ )
48
+ return configured
49
+
50
+
51
+ def reset() -> None:
52
+ """Clear all configured backends. Mainly useful between tests."""
53
+ _config.infer = None
54
+ _config.parse = None
55
+ _config.render = None
@@ -0,0 +1,18 @@
1
+ """Small helpers shared across the T2D / D2T / infer pipelines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+
9
+ def extract_json(raw: str) -> Any:
10
+ """Parse a JSON value out of a model response, tolerating code fences."""
11
+ text = raw.strip()
12
+ if text.startswith("```"):
13
+ text = text.strip("`")
14
+ if "\n" in text:
15
+ first_line, rest = text.split("\n", 1)
16
+ if first_line.strip().isalpha():
17
+ text = rest
18
+ return json.loads(text)
@@ -0,0 +1,33 @@
1
+ """Backend implementations for dtxt.
2
+
3
+ Only the mock backend is imported eagerly. API/local backends
4
+ (``Anthropic``, ``OpenAI``, ``LlamaCpp``) live in optional extras; their
5
+ modules -- and the underlying SDK packages -- are imported lazily on first
6
+ attribute access, so importing ``dtxt`` never requires them to be
7
+ installed.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import importlib
13
+ from typing import Any
14
+
15
+ from .base import Backend
16
+ from .mock import MockBackend
17
+
18
+ __all__ = ["Backend", "MockBackend", "Anthropic", "OpenAI", "LlamaCpp"]
19
+
20
+ _LAZY: dict[str, tuple[str, str]] = {
21
+ "Anthropic": (".anthropic", "Anthropic"),
22
+ "OpenAI": (".openai", "OpenAI"),
23
+ "LlamaCpp": (".llamacpp", "LlamaCpp"),
24
+ }
25
+
26
+
27
+ def __getattr__(name: str) -> Any:
28
+ try:
29
+ module_name, attr_name = _LAZY[name]
30
+ except KeyError:
31
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
32
+ module = importlib.import_module(module_name, __name__)
33
+ return getattr(module, attr_name)
@@ -0,0 +1,114 @@
1
+ """Anthropic API backend: structured output via forced tool use.
2
+
3
+ The ``anthropic`` package is an optional extra (``dtxt[anthropic]``) and is
4
+ imported lazily, only when a client actually needs to be built.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Any
11
+
12
+ from .base import TOOL_CALLING
13
+
14
+ _INSTALL_HINT = "pip install dtxt[anthropic]"
15
+ _TOOL_NAME = "emit_result"
16
+
17
+
18
+ def _import_anthropic() -> Any:
19
+ try:
20
+ import anthropic
21
+ except ImportError as exc:
22
+ raise ImportError(
23
+ "the 'anthropic' package is required to use dtxt.backends.Anthropic. "
24
+ f"Install it with: {_INSTALL_HINT}"
25
+ ) from exc
26
+ return anthropic
27
+
28
+
29
+ def _build_request_kwargs(
30
+ model: str, prompt: str, schema: dict[str, Any] | None, max_tokens: int
31
+ ) -> dict[str, Any]:
32
+ kwargs: dict[str, Any] = {
33
+ "model": model,
34
+ "max_tokens": max_tokens,
35
+ "messages": [{"role": "user", "content": prompt}],
36
+ }
37
+ if schema is not None:
38
+ kwargs["tools"] = [
39
+ {
40
+ "name": _TOOL_NAME,
41
+ "description": "Emit the result as structured data matching the schema.",
42
+ "input_schema": schema,
43
+ }
44
+ ]
45
+ kwargs["tool_choice"] = {"type": "tool", "name": _TOOL_NAME}
46
+ return kwargs
47
+
48
+
49
+ def _extract_result(message: Any, schema: dict[str, Any] | None) -> str:
50
+ if schema is not None:
51
+ for block in message.content:
52
+ if getattr(block, "type", None) == "tool_use" and getattr(block, "name", None) == (
53
+ _TOOL_NAME
54
+ ):
55
+ return json.dumps(block.input)
56
+ raise RuntimeError("Anthropic response did not include the expected tool_use block")
57
+ return "".join(
58
+ block.text for block in message.content if getattr(block, "type", None) == "text"
59
+ )
60
+
61
+
62
+ class Anthropic:
63
+ """Backend for the Anthropic Messages API.
64
+
65
+ Structured output is obtained by wrapping the JSON Schema as a single
66
+ tool and forcing ``tool_choice`` onto it, so ``tool_use.input`` is the
67
+ extracted object. This is not treated as ``constrained_decoding``: the
68
+ API does not guarantee full schema conformance the way grammar-based
69
+ decoding does, so dtxt still runs it through the retry + validation
70
+ loop.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ model: str,
76
+ *,
77
+ api_key: str | None = None,
78
+ max_tokens: int = 4096,
79
+ client: Any | None = None,
80
+ async_client: Any | None = None,
81
+ ) -> None:
82
+ self._model = model
83
+ self._api_key = api_key
84
+ self._max_tokens = max_tokens
85
+ self._client = client
86
+ self._async_client = async_client
87
+
88
+ def _get_client(self) -> Any:
89
+ if self._client is None:
90
+ anthropic = _import_anthropic()
91
+ kwargs: dict[str, Any] = {"api_key": self._api_key} if self._api_key else {}
92
+ self._client = anthropic.Anthropic(**kwargs)
93
+ return self._client
94
+
95
+ def _get_async_client(self) -> Any:
96
+ if self._async_client is None:
97
+ anthropic = _import_anthropic()
98
+ kwargs: dict[str, Any] = {"api_key": self._api_key} if self._api_key else {}
99
+ self._async_client = anthropic.AsyncAnthropic(**kwargs)
100
+ return self._async_client
101
+
102
+ @property
103
+ def capabilities(self) -> set[str]:
104
+ return {TOOL_CALLING}
105
+
106
+ def generate(self, prompt: str, *, schema: dict[str, Any] | None = None) -> str:
107
+ kwargs = _build_request_kwargs(self._model, prompt, schema, self._max_tokens)
108
+ message = self._get_client().messages.create(**kwargs)
109
+ return _extract_result(message, schema)
110
+
111
+ async def agenerate(self, prompt: str, *, schema: dict[str, Any] | None = None) -> str:
112
+ kwargs = _build_request_kwargs(self._model, prompt, schema, self._max_tokens)
113
+ message = await self._get_async_client().messages.create(**kwargs)
114
+ return _extract_result(message, schema)
@@ -0,0 +1,27 @@
1
+ """Backend abstraction: the interface every dtxt backend implements."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Protocol, runtime_checkable
6
+
7
+ CONSTRAINED_DECODING = "constrained_decoding"
8
+ JSON_MODE = "json_mode"
9
+ TOOL_CALLING = "tool_calling"
10
+
11
+
12
+ @runtime_checkable
13
+ class Backend(Protocol):
14
+ """Anything that can turn a prompt into text, optionally schema-guided.
15
+
16
+ ``capabilities`` tells the caller which execution path to take:
17
+
18
+ - ``constrained_decoding``: the backend enforces the JSON schema at the
19
+ grammar level, so callers may skip syntactic validation and only run
20
+ semantic checks.
21
+ - otherwise: callers must retry and validate the output themselves.
22
+ """
23
+
24
+ def generate(self, prompt: str, *, schema: dict[str, Any] | None = None) -> str: ...
25
+
26
+ @property
27
+ def capabilities(self) -> set[str]: ...