prompture 0.0.29.dev8__py3-none-any.whl → 0.0.38.dev2__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.
- prompture/__init__.py +264 -23
- prompture/_version.py +34 -0
- prompture/agent.py +924 -0
- prompture/agent_types.py +156 -0
- prompture/aio/__init__.py +74 -0
- prompture/async_agent.py +880 -0
- prompture/async_conversation.py +789 -0
- prompture/async_core.py +803 -0
- prompture/async_driver.py +193 -0
- prompture/async_groups.py +551 -0
- prompture/cache.py +469 -0
- prompture/callbacks.py +55 -0
- prompture/cli.py +63 -4
- prompture/conversation.py +826 -0
- prompture/core.py +894 -263
- prompture/cost_mixin.py +51 -0
- prompture/discovery.py +187 -0
- prompture/driver.py +206 -5
- prompture/drivers/__init__.py +175 -67
- prompture/drivers/airllm_driver.py +109 -0
- prompture/drivers/async_airllm_driver.py +26 -0
- prompture/drivers/async_azure_driver.py +123 -0
- prompture/drivers/async_claude_driver.py +113 -0
- prompture/drivers/async_google_driver.py +316 -0
- prompture/drivers/async_grok_driver.py +97 -0
- prompture/drivers/async_groq_driver.py +90 -0
- prompture/drivers/async_hugging_driver.py +61 -0
- prompture/drivers/async_lmstudio_driver.py +148 -0
- prompture/drivers/async_local_http_driver.py +44 -0
- prompture/drivers/async_ollama_driver.py +135 -0
- prompture/drivers/async_openai_driver.py +102 -0
- prompture/drivers/async_openrouter_driver.py +102 -0
- prompture/drivers/async_registry.py +133 -0
- prompture/drivers/azure_driver.py +42 -9
- prompture/drivers/claude_driver.py +257 -34
- prompture/drivers/google_driver.py +295 -42
- prompture/drivers/grok_driver.py +35 -32
- prompture/drivers/groq_driver.py +33 -26
- prompture/drivers/hugging_driver.py +6 -6
- prompture/drivers/lmstudio_driver.py +97 -19
- prompture/drivers/local_http_driver.py +6 -6
- prompture/drivers/ollama_driver.py +168 -23
- prompture/drivers/openai_driver.py +184 -9
- prompture/drivers/openrouter_driver.py +37 -25
- prompture/drivers/registry.py +306 -0
- prompture/drivers/vision_helpers.py +153 -0
- prompture/field_definitions.py +106 -96
- prompture/group_types.py +147 -0
- prompture/groups.py +530 -0
- prompture/image.py +180 -0
- prompture/logging.py +80 -0
- prompture/model_rates.py +217 -0
- prompture/persistence.py +254 -0
- prompture/persona.py +482 -0
- prompture/runner.py +49 -47
- prompture/scaffold/__init__.py +1 -0
- prompture/scaffold/generator.py +84 -0
- prompture/scaffold/templates/Dockerfile.j2 +12 -0
- prompture/scaffold/templates/README.md.j2 +41 -0
- prompture/scaffold/templates/config.py.j2 +21 -0
- prompture/scaffold/templates/env.example.j2 +8 -0
- prompture/scaffold/templates/main.py.j2 +86 -0
- prompture/scaffold/templates/models.py.j2 +40 -0
- prompture/scaffold/templates/requirements.txt.j2 +5 -0
- prompture/serialization.py +218 -0
- prompture/server.py +183 -0
- prompture/session.py +117 -0
- prompture/settings.py +19 -1
- prompture/tools.py +219 -267
- prompture/tools_schema.py +254 -0
- prompture/validator.py +3 -3
- prompture-0.0.38.dev2.dist-info/METADATA +369 -0
- prompture-0.0.38.dev2.dist-info/RECORD +77 -0
- {prompture-0.0.29.dev8.dist-info → prompture-0.0.38.dev2.dist-info}/WHEEL +1 -1
- prompture-0.0.29.dev8.dist-info/METADATA +0 -368
- prompture-0.0.29.dev8.dist-info/RECORD +0 -27
- {prompture-0.0.29.dev8.dist-info → prompture-0.0.38.dev2.dist-info}/entry_points.txt +0 -0
- {prompture-0.0.29.dev8.dist-info → prompture-0.0.38.dev2.dist-info}/licenses/LICENSE +0 -0
- {prompture-0.0.29.dev8.dist-info → prompture-0.0.38.dev2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Function calling / tool use support for Prompture.
|
|
2
|
+
|
|
3
|
+
Provides :class:`ToolDefinition` for describing callable tools,
|
|
4
|
+
:class:`ToolRegistry` for managing a collection of tools, and
|
|
5
|
+
:func:`tool_from_function` to auto-generate tool schemas from type hints.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
from prompture import ToolRegistry
|
|
10
|
+
|
|
11
|
+
registry = ToolRegistry()
|
|
12
|
+
|
|
13
|
+
@registry.tool
|
|
14
|
+
def get_weather(city: str, units: str = "celsius") -> str:
|
|
15
|
+
\"\"\"Get the current weather for a city.\"\"\"
|
|
16
|
+
return f"Weather in {city}: 22 {units}"
|
|
17
|
+
|
|
18
|
+
# Or register explicitly
|
|
19
|
+
registry.register(get_weather)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import inspect
|
|
25
|
+
import logging
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from typing import Any, Callable, get_type_hints
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("prompture.tools_schema")
|
|
30
|
+
|
|
31
|
+
# Mapping from Python types to JSON Schema types
|
|
32
|
+
_TYPE_MAP: dict[type, str] = {
|
|
33
|
+
str: "string",
|
|
34
|
+
int: "integer",
|
|
35
|
+
float: "number",
|
|
36
|
+
bool: "boolean",
|
|
37
|
+
list: "array",
|
|
38
|
+
dict: "object",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _python_type_to_json_schema(annotation: Any) -> dict[str, Any]:
|
|
43
|
+
"""Convert a Python type annotation to a JSON Schema snippet."""
|
|
44
|
+
if annotation is inspect.Parameter.empty or annotation is None:
|
|
45
|
+
return {"type": "string"}
|
|
46
|
+
|
|
47
|
+
# Handle Optional[X] (Union[X, None])
|
|
48
|
+
origin = getattr(annotation, "__origin__", None)
|
|
49
|
+
args = getattr(annotation, "__args__", ())
|
|
50
|
+
|
|
51
|
+
if origin is type(None):
|
|
52
|
+
return {"type": "string"}
|
|
53
|
+
|
|
54
|
+
# Union types (Optional)
|
|
55
|
+
if origin is not None and hasattr(origin, "__name__") and origin.__name__ == "Union":
|
|
56
|
+
non_none = [a for a in args if a is not type(None)]
|
|
57
|
+
if len(non_none) == 1:
|
|
58
|
+
return _python_type_to_json_schema(non_none[0])
|
|
59
|
+
|
|
60
|
+
# list[X]
|
|
61
|
+
if origin is list and args:
|
|
62
|
+
return {"type": "array", "items": _python_type_to_json_schema(args[0])}
|
|
63
|
+
|
|
64
|
+
# dict[str, X]
|
|
65
|
+
if origin is dict:
|
|
66
|
+
return {"type": "object"}
|
|
67
|
+
|
|
68
|
+
# Simple types
|
|
69
|
+
json_type = _TYPE_MAP.get(annotation, "string")
|
|
70
|
+
return {"type": json_type}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class ToolDefinition:
|
|
75
|
+
"""Describes a single callable tool the LLM can invoke.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
name: Unique tool identifier.
|
|
79
|
+
description: Human-readable description shown to the LLM.
|
|
80
|
+
parameters: JSON Schema describing the function parameters.
|
|
81
|
+
function: The Python callable to execute.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
name: str
|
|
85
|
+
description: str
|
|
86
|
+
parameters: dict[str, Any]
|
|
87
|
+
function: Callable[..., Any]
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
# Serialisation helpers
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def to_openai_format(self) -> dict[str, Any]:
|
|
94
|
+
"""Serialise to OpenAI ``tools`` array element format."""
|
|
95
|
+
return {
|
|
96
|
+
"type": "function",
|
|
97
|
+
"function": {
|
|
98
|
+
"name": self.name,
|
|
99
|
+
"description": self.description,
|
|
100
|
+
"parameters": self.parameters,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def to_anthropic_format(self) -> dict[str, Any]:
|
|
105
|
+
"""Serialise to Anthropic ``tools`` array element format."""
|
|
106
|
+
return {
|
|
107
|
+
"name": self.name,
|
|
108
|
+
"description": self.description,
|
|
109
|
+
"input_schema": self.parameters,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def tool_from_function(fn: Callable[..., Any], *, name: str | None = None, description: str | None = None) -> ToolDefinition:
|
|
114
|
+
"""Build a :class:`ToolDefinition` by inspecting *fn*'s signature and docstring.
|
|
115
|
+
|
|
116
|
+
Parameters:
|
|
117
|
+
fn: The callable to wrap.
|
|
118
|
+
name: Override the tool name (defaults to ``fn.__name__``).
|
|
119
|
+
description: Override the description (defaults to the first line of the docstring).
|
|
120
|
+
"""
|
|
121
|
+
tool_name = name or fn.__name__
|
|
122
|
+
tool_desc = description or (inspect.getdoc(fn) or "").split("\n")[0] or f"Call {tool_name}"
|
|
123
|
+
|
|
124
|
+
sig = inspect.signature(fn)
|
|
125
|
+
try:
|
|
126
|
+
hints = get_type_hints(fn)
|
|
127
|
+
except Exception:
|
|
128
|
+
hints = {}
|
|
129
|
+
|
|
130
|
+
properties: dict[str, Any] = {}
|
|
131
|
+
required: list[str] = []
|
|
132
|
+
|
|
133
|
+
for param_name, param in sig.parameters.items():
|
|
134
|
+
if param_name == "self":
|
|
135
|
+
continue
|
|
136
|
+
annotation = hints.get(param_name, param.annotation)
|
|
137
|
+
prop = _python_type_to_json_schema(annotation)
|
|
138
|
+
|
|
139
|
+
# Use parameter name as description fallback
|
|
140
|
+
prop.setdefault("description", f"Parameter: {param_name}")
|
|
141
|
+
|
|
142
|
+
properties[param_name] = prop
|
|
143
|
+
|
|
144
|
+
if param.default is inspect.Parameter.empty:
|
|
145
|
+
required.append(param_name)
|
|
146
|
+
|
|
147
|
+
parameters: dict[str, Any] = {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"properties": properties,
|
|
150
|
+
}
|
|
151
|
+
if required:
|
|
152
|
+
parameters["required"] = required
|
|
153
|
+
|
|
154
|
+
return ToolDefinition(
|
|
155
|
+
name=tool_name,
|
|
156
|
+
description=tool_desc,
|
|
157
|
+
parameters=parameters,
|
|
158
|
+
function=fn,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class ToolRegistry:
|
|
164
|
+
"""A collection of :class:`ToolDefinition` instances.
|
|
165
|
+
|
|
166
|
+
Supports decorator-based and explicit registration::
|
|
167
|
+
|
|
168
|
+
registry = ToolRegistry()
|
|
169
|
+
|
|
170
|
+
@registry.tool
|
|
171
|
+
def my_func(x: int) -> str:
|
|
172
|
+
...
|
|
173
|
+
|
|
174
|
+
registry.register(another_func)
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
_tools: dict[str, ToolDefinition] = field(default_factory=dict)
|
|
178
|
+
|
|
179
|
+
# ------------------------------------------------------------------
|
|
180
|
+
# Registration
|
|
181
|
+
# ------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def register(
|
|
184
|
+
self,
|
|
185
|
+
fn: Callable[..., Any],
|
|
186
|
+
*,
|
|
187
|
+
name: str | None = None,
|
|
188
|
+
description: str | None = None,
|
|
189
|
+
) -> ToolDefinition:
|
|
190
|
+
"""Register *fn* as a tool and return the :class:`ToolDefinition`."""
|
|
191
|
+
td = tool_from_function(fn, name=name, description=description)
|
|
192
|
+
self._tools[td.name] = td
|
|
193
|
+
return td
|
|
194
|
+
|
|
195
|
+
def tool(self, fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
196
|
+
"""Decorator to register a function as a tool.
|
|
197
|
+
|
|
198
|
+
Returns the original function unchanged so it remains callable.
|
|
199
|
+
"""
|
|
200
|
+
self.register(fn)
|
|
201
|
+
return fn
|
|
202
|
+
|
|
203
|
+
def add(self, tool_def: ToolDefinition) -> None:
|
|
204
|
+
"""Add a pre-built :class:`ToolDefinition`."""
|
|
205
|
+
self._tools[tool_def.name] = tool_def
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------------
|
|
208
|
+
# Lookup
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
def get(self, name: str) -> ToolDefinition | None:
|
|
212
|
+
return self._tools.get(name)
|
|
213
|
+
|
|
214
|
+
def __contains__(self, name: str) -> bool:
|
|
215
|
+
return name in self._tools
|
|
216
|
+
|
|
217
|
+
def __len__(self) -> int:
|
|
218
|
+
return len(self._tools)
|
|
219
|
+
|
|
220
|
+
def __bool__(self) -> bool:
|
|
221
|
+
return bool(self._tools)
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def names(self) -> list[str]:
|
|
225
|
+
return list(self._tools.keys())
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def definitions(self) -> list[ToolDefinition]:
|
|
229
|
+
return list(self._tools.values())
|
|
230
|
+
|
|
231
|
+
# ------------------------------------------------------------------
|
|
232
|
+
# Serialisation
|
|
233
|
+
# ------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
def to_openai_format(self) -> list[dict[str, Any]]:
|
|
236
|
+
return [td.to_openai_format() for td in self._tools.values()]
|
|
237
|
+
|
|
238
|
+
def to_anthropic_format(self) -> list[dict[str, Any]]:
|
|
239
|
+
return [td.to_anthropic_format() for td in self._tools.values()]
|
|
240
|
+
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
# Execution
|
|
243
|
+
# ------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
def execute(self, name: str, arguments: dict[str, Any]) -> Any:
|
|
246
|
+
"""Execute a registered tool by name with the given arguments.
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
KeyError: If no tool with *name* is registered.
|
|
250
|
+
"""
|
|
251
|
+
td = self._tools.get(name)
|
|
252
|
+
if td is None:
|
|
253
|
+
raise KeyError(f"Tool not registered: {name!r}")
|
|
254
|
+
return td.function(**arguments)
|
prompture/validator.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Any
|
|
3
3
|
|
|
4
4
|
try:
|
|
5
5
|
import jsonschema
|
|
@@ -7,7 +7,7 @@ except Exception:
|
|
|
7
7
|
jsonschema = None
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def validate_against_schema(instance_json: str, schema:
|
|
10
|
+
def validate_against_schema(instance_json: str, schema: dict[str, Any]) -> dict[str, Any]:
|
|
11
11
|
"""Valida el JSON (string) contra un JSON Schema.
|
|
12
12
|
Devuelve dict con ok: bool y detalles.
|
|
13
13
|
"""
|
|
@@ -28,4 +28,4 @@ def validate_against_schema(instance_json: str, schema: Dict[str,Any]) -> Dict[s
|
|
|
28
28
|
jsonschema.validate(instance=data, schema=schema)
|
|
29
29
|
return {"ok": True, "data": data}
|
|
30
30
|
except jsonschema.ValidationError as e:
|
|
31
|
-
return {"ok": False, "error": str(e), "data": data}
|
|
31
|
+
return {"ok": False, "error": str(e), "data": data}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prompture
|
|
3
|
+
Version: 0.0.38.dev2
|
|
4
|
+
Summary: Ask LLMs to return structured JSON and run cross-model tests. API-first.
|
|
5
|
+
Author-email: Juan Denis <juan@vene.co>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jhd3197/prompture
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: anthropic>=0.8.0
|
|
14
|
+
Requires-Dist: click>=8.0
|
|
15
|
+
Requires-Dist: google-generativeai>=0.3.0
|
|
16
|
+
Requires-Dist: groq>=0.4.0
|
|
17
|
+
Requires-Dist: httpx>=0.25.0
|
|
18
|
+
Requires-Dist: jsonschema>=4.0
|
|
19
|
+
Requires-Dist: openai>=1.0.0
|
|
20
|
+
Requires-Dist: pandas>=1.3.0
|
|
21
|
+
Requires-Dist: pydantic>=1.10
|
|
22
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
23
|
+
Requires-Dist: python-dotenv>=0.19.0
|
|
24
|
+
Requires-Dist: python-toon>=0.1.0
|
|
25
|
+
Requires-Dist: requests>=2.28
|
|
26
|
+
Requires-Dist: python-dateutil>=2.9.0
|
|
27
|
+
Requires-Dist: tukuy>=0.0.6
|
|
28
|
+
Requires-Dist: pyyaml>=6.0
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "test"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.8.0; extra == "dev"
|
|
36
|
+
Provides-Extra: airllm
|
|
37
|
+
Requires-Dist: airllm>=2.8.0; extra == "airllm"
|
|
38
|
+
Provides-Extra: redis
|
|
39
|
+
Requires-Dist: redis>=4.0; extra == "redis"
|
|
40
|
+
Provides-Extra: serve
|
|
41
|
+
Requires-Dist: fastapi>=0.100; extra == "serve"
|
|
42
|
+
Requires-Dist: uvicorn[standard]>=0.20; extra == "serve"
|
|
43
|
+
Requires-Dist: sse-starlette>=1.6; extra == "serve"
|
|
44
|
+
Provides-Extra: scaffold
|
|
45
|
+
Requires-Dist: jinja2>=3.0; extra == "scaffold"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<h1 align="center">Prompture</h1>
|
|
50
|
+
<p align="center">Structured JSON extraction from any LLM. Schema-enforced, Pydantic-native, multi-provider.</p>
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
<a href="https://pypi.org/project/prompture/"><img src="https://badge.fury.io/py/prompture.svg" alt="PyPI version"></a>
|
|
55
|
+
<a href="https://pypi.org/project/prompture/"><img src="https://img.shields.io/pypi/pyversions/prompture.svg" alt="Python versions"></a>
|
|
56
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
57
|
+
<a href="https://pepy.tech/project/prompture"><img src="https://static.pepy.tech/badge/prompture" alt="Downloads"></a>
|
|
58
|
+
<a href="https://github.com/jhd3197/prompture"><img src="https://img.shields.io/github/stars/jhd3197/prompture?style=social" alt="GitHub stars"></a>
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
**Prompture** is a Python library that turns LLM responses into validated, structured data. Define a schema or Pydantic model, point it at any provider, and get typed output back — with token tracking, cost calculation, and automatic JSON repair built in.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from pydantic import BaseModel
|
|
67
|
+
from prompture import extract_with_model
|
|
68
|
+
|
|
69
|
+
class Person(BaseModel):
|
|
70
|
+
name: str
|
|
71
|
+
age: int
|
|
72
|
+
profession: str
|
|
73
|
+
|
|
74
|
+
person = extract_with_model(Person, "Maria is 32, a developer in NYC.", model_name="openai/gpt-4")
|
|
75
|
+
print(person.name) # Maria
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Key Features
|
|
79
|
+
|
|
80
|
+
- **Structured output** — JSON schema enforcement and direct Pydantic model population
|
|
81
|
+
- **12 providers** — OpenAI, Claude, Google, Groq, Grok, Azure, Ollama, LM Studio, OpenRouter, HuggingFace, AirLLM, and generic HTTP
|
|
82
|
+
- **TOON input conversion** — 45-60% token savings when sending structured data via [Token-Oriented Object Notation](https://github.com/jhd3197/python-toon)
|
|
83
|
+
- **Stepwise extraction** — Per-field prompts with smart type coercion (shorthand numbers, multilingual booleans, dates)
|
|
84
|
+
- **Field registry** — 50+ predefined extraction fields with template variables and Pydantic integration
|
|
85
|
+
- **Conversations** — Stateful multi-turn sessions with sync and async support
|
|
86
|
+
- **Tool use** — Function calling and streaming across supported providers
|
|
87
|
+
- **Caching** — Built-in response cache with memory, SQLite, and Redis backends
|
|
88
|
+
- **Plugin system** — Register custom drivers via entry points
|
|
89
|
+
- **Usage tracking** — Token counts and cost calculation on every call
|
|
90
|
+
- **Auto-repair** — Optional second LLM pass to fix malformed JSON
|
|
91
|
+
- **Batch testing** — Spec-driven suites to compare models side by side
|
|
92
|
+
|
|
93
|
+
## Installation
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install prompture
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Optional extras:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install prompture[redis] # Redis cache backend
|
|
103
|
+
pip install prompture[serve] # FastAPI server mode
|
|
104
|
+
pip install prompture[airllm] # AirLLM local inference
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Configuration
|
|
108
|
+
|
|
109
|
+
Set API keys for the providers you use. Prompture reads from environment variables or a `.env` file:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
OPENAI_API_KEY=sk-...
|
|
113
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
114
|
+
GOOGLE_API_KEY=...
|
|
115
|
+
GROQ_API_KEY=...
|
|
116
|
+
GROK_API_KEY=...
|
|
117
|
+
OPENROUTER_API_KEY=...
|
|
118
|
+
AZURE_OPENAI_ENDPOINT=...
|
|
119
|
+
AZURE_OPENAI_API_KEY=...
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Local providers (Ollama, LM Studio) work out of the box with no keys required.
|
|
123
|
+
|
|
124
|
+
## Providers
|
|
125
|
+
|
|
126
|
+
Model strings use `"provider/model"` format. The provider prefix routes to the correct driver automatically.
|
|
127
|
+
|
|
128
|
+
| Provider | Example Model | Cost |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `openai` | `openai/gpt-4` | Automatic |
|
|
131
|
+
| `claude` | `claude/claude-3` | Automatic |
|
|
132
|
+
| `google` | `google/gemini-1.5-pro` | Automatic |
|
|
133
|
+
| `groq` | `groq/llama2-70b-4096` | Automatic |
|
|
134
|
+
| `grok` | `grok/grok-4-fast-reasoning` | Automatic |
|
|
135
|
+
| `azure` | `azure/deployed-name` | Automatic |
|
|
136
|
+
| `openrouter` | `openrouter/anthropic/claude-2` | Automatic |
|
|
137
|
+
| `ollama` | `ollama/llama3.1:8b` | Free (local) |
|
|
138
|
+
| `lmstudio` | `lmstudio/local-model` | Free (local) |
|
|
139
|
+
| `huggingface` | `hf/model-name` | Free (local) |
|
|
140
|
+
| `http` | `http/self-hosted` | Free |
|
|
141
|
+
|
|
142
|
+
## Usage
|
|
143
|
+
|
|
144
|
+
### One-Shot Pydantic Extraction
|
|
145
|
+
|
|
146
|
+
Single LLM call, returns a validated Pydantic instance:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from typing import List, Optional
|
|
150
|
+
from pydantic import BaseModel
|
|
151
|
+
from prompture import extract_with_model
|
|
152
|
+
|
|
153
|
+
class Person(BaseModel):
|
|
154
|
+
name: str
|
|
155
|
+
age: int
|
|
156
|
+
profession: str
|
|
157
|
+
city: str
|
|
158
|
+
hobbies: List[str]
|
|
159
|
+
education: Optional[str] = None
|
|
160
|
+
|
|
161
|
+
person = extract_with_model(
|
|
162
|
+
Person,
|
|
163
|
+
"Maria is 32, a software developer in New York. She loves hiking and photography.",
|
|
164
|
+
model_name="openai/gpt-4"
|
|
165
|
+
)
|
|
166
|
+
print(person.model_dump())
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Stepwise Extraction
|
|
170
|
+
|
|
171
|
+
One LLM call per field. Higher accuracy, per-field error recovery:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from prompture import stepwise_extract_with_model
|
|
175
|
+
|
|
176
|
+
result = stepwise_extract_with_model(
|
|
177
|
+
Person,
|
|
178
|
+
"Maria is 32, a software developer in New York. She loves hiking and photography.",
|
|
179
|
+
model_name="openai/gpt-4"
|
|
180
|
+
)
|
|
181
|
+
print(result["model"].model_dump())
|
|
182
|
+
print(result["usage"]) # per-field and total token usage
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
| Aspect | `extract_with_model` | `stepwise_extract_with_model` |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| LLM calls | 1 | N (one per field) |
|
|
188
|
+
| Speed / cost | Faster, cheaper | Slower, higher |
|
|
189
|
+
| Accuracy | Good global coherence | Higher per-field accuracy |
|
|
190
|
+
| Error handling | All-or-nothing | Per-field recovery |
|
|
191
|
+
|
|
192
|
+
### JSON Schema Extraction
|
|
193
|
+
|
|
194
|
+
For raw JSON output with full control:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from prompture import ask_for_json
|
|
198
|
+
|
|
199
|
+
schema = {
|
|
200
|
+
"type": "object",
|
|
201
|
+
"required": ["name", "age"],
|
|
202
|
+
"properties": {
|
|
203
|
+
"name": {"type": "string"},
|
|
204
|
+
"age": {"type": "integer"}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
result = ask_for_json(
|
|
209
|
+
content_prompt="Extract the person's info from: John is 28 and lives in Miami.",
|
|
210
|
+
json_schema=schema,
|
|
211
|
+
model_name="openai/gpt-4"
|
|
212
|
+
)
|
|
213
|
+
print(result["json_object"]) # {"name": "John", "age": 28}
|
|
214
|
+
print(result["usage"]) # token counts and cost
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### TOON Input — Token Savings
|
|
218
|
+
|
|
219
|
+
Analyze structured data with automatic TOON conversion for 45-60% fewer tokens:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from prompture import extract_from_data
|
|
223
|
+
|
|
224
|
+
products = [
|
|
225
|
+
{"id": 1, "name": "Laptop", "price": 999.99, "rating": 4.5},
|
|
226
|
+
{"id": 2, "name": "Book", "price": 19.99, "rating": 4.2},
|
|
227
|
+
{"id": 3, "name": "Headphones", "price": 149.99, "rating": 4.7},
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
result = extract_from_data(
|
|
231
|
+
data=products,
|
|
232
|
+
question="What is the average price and highest rated product?",
|
|
233
|
+
json_schema={
|
|
234
|
+
"type": "object",
|
|
235
|
+
"properties": {
|
|
236
|
+
"average_price": {"type": "number"},
|
|
237
|
+
"highest_rated": {"type": "string"}
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
model_name="openai/gpt-4"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
print(result["json_object"])
|
|
244
|
+
# {"average_price": 389.99, "highest_rated": "Headphones"}
|
|
245
|
+
|
|
246
|
+
print(f"Token savings: {result['token_savings']['percentage_saved']}%")
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Works with Pandas DataFrames via `extract_from_pandas()`.
|
|
250
|
+
|
|
251
|
+
### Field Definitions
|
|
252
|
+
|
|
253
|
+
Use the built-in field registry for consistent extraction across models:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from pydantic import BaseModel
|
|
257
|
+
from prompture import field_from_registry, stepwise_extract_with_model
|
|
258
|
+
|
|
259
|
+
class Person(BaseModel):
|
|
260
|
+
name: str = field_from_registry("name")
|
|
261
|
+
age: int = field_from_registry("age")
|
|
262
|
+
email: str = field_from_registry("email")
|
|
263
|
+
occupation: str = field_from_registry("occupation")
|
|
264
|
+
|
|
265
|
+
result = stepwise_extract_with_model(
|
|
266
|
+
Person,
|
|
267
|
+
"John Smith, 25, software engineer at TechCorp, john@example.com",
|
|
268
|
+
model_name="openai/gpt-4"
|
|
269
|
+
)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Register custom fields with template variables:
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from prompture import register_field
|
|
276
|
+
|
|
277
|
+
register_field("document_date", {
|
|
278
|
+
"type": "str",
|
|
279
|
+
"description": "Document creation date",
|
|
280
|
+
"instructions": "Use {{current_date}} if not specified",
|
|
281
|
+
"default": "{{current_date}}",
|
|
282
|
+
"nullable": False
|
|
283
|
+
})
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Conversations
|
|
287
|
+
|
|
288
|
+
Stateful multi-turn sessions:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from prompture import Conversation
|
|
292
|
+
|
|
293
|
+
conv = Conversation(model_name="openai/gpt-4")
|
|
294
|
+
conv.add_message("system", "You are a helpful assistant.")
|
|
295
|
+
response = conv.send("What is the capital of France?")
|
|
296
|
+
follow_up = conv.send("What about Germany?") # retains context
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Model Discovery
|
|
300
|
+
|
|
301
|
+
Auto-detect available models from configured providers:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
from prompture import get_available_models
|
|
305
|
+
|
|
306
|
+
models = get_available_models()
|
|
307
|
+
for model in models:
|
|
308
|
+
print(model) # "openai/gpt-4", "ollama/llama3:latest", ...
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Logging and Debugging
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
import logging
|
|
315
|
+
from prompture import configure_logging
|
|
316
|
+
|
|
317
|
+
configure_logging(logging.DEBUG)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Response Shape
|
|
321
|
+
|
|
322
|
+
All extraction functions return a consistent structure:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
{
|
|
326
|
+
"json_string": str, # raw JSON text
|
|
327
|
+
"json_object": dict, # parsed result
|
|
328
|
+
"usage": {
|
|
329
|
+
"prompt_tokens": int,
|
|
330
|
+
"completion_tokens": int,
|
|
331
|
+
"total_tokens": int,
|
|
332
|
+
"cost": float,
|
|
333
|
+
"model_name": str
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## CLI
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
prompture run <spec-file>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Run spec-driven extraction suites for cross-model comparison.
|
|
345
|
+
|
|
346
|
+
## Development
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
# Install with dev dependencies
|
|
350
|
+
pip install -e ".[test,dev]"
|
|
351
|
+
|
|
352
|
+
# Run tests
|
|
353
|
+
pytest
|
|
354
|
+
|
|
355
|
+
# Run integration tests (requires live LLM access)
|
|
356
|
+
pytest --run-integration
|
|
357
|
+
|
|
358
|
+
# Lint and format
|
|
359
|
+
ruff check .
|
|
360
|
+
ruff format .
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Contributing
|
|
364
|
+
|
|
365
|
+
PRs welcome. Please add tests for new functionality and examples under `examples/` for new drivers or patterns.
|
|
366
|
+
|
|
367
|
+
## License
|
|
368
|
+
|
|
369
|
+
[MIT](https://opensource.org/licenses/MIT)
|