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 +21 -0
- dtxt-0.1.0/PKG-INFO +144 -0
- dtxt-0.1.0/README.md +112 -0
- dtxt-0.1.0/pyproject.toml +72 -0
- dtxt-0.1.0/src/dtxt/__init__.py +23 -0
- dtxt-0.1.0/src/dtxt/_config.py +55 -0
- dtxt-0.1.0/src/dtxt/_util.py +18 -0
- dtxt-0.1.0/src/dtxt/backends/__init__.py +33 -0
- dtxt-0.1.0/src/dtxt/backends/anthropic.py +114 -0
- dtxt-0.1.0/src/dtxt/backends/base.py +27 -0
- dtxt-0.1.0/src/dtxt/backends/llamacpp.py +126 -0
- dtxt-0.1.0/src/dtxt/backends/mock.py +58 -0
- dtxt-0.1.0/src/dtxt/backends/openai.py +96 -0
- dtxt-0.1.0/src/dtxt/d2t.py +53 -0
- dtxt-0.1.0/src/dtxt/infer.py +96 -0
- dtxt-0.1.0/src/dtxt/prompts.py +101 -0
- dtxt-0.1.0/src/dtxt/py.typed +0 -0
- dtxt-0.1.0/src/dtxt/roundtrip.py +54 -0
- dtxt-0.1.0/src/dtxt/schema.py +116 -0
- dtxt-0.1.0/src/dtxt/t2d.py +166 -0
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]: ...
|