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.
Files changed (79) hide show
  1. prompture/__init__.py +264 -23
  2. prompture/_version.py +34 -0
  3. prompture/agent.py +924 -0
  4. prompture/agent_types.py +156 -0
  5. prompture/aio/__init__.py +74 -0
  6. prompture/async_agent.py +880 -0
  7. prompture/async_conversation.py +789 -0
  8. prompture/async_core.py +803 -0
  9. prompture/async_driver.py +193 -0
  10. prompture/async_groups.py +551 -0
  11. prompture/cache.py +469 -0
  12. prompture/callbacks.py +55 -0
  13. prompture/cli.py +63 -4
  14. prompture/conversation.py +826 -0
  15. prompture/core.py +894 -263
  16. prompture/cost_mixin.py +51 -0
  17. prompture/discovery.py +187 -0
  18. prompture/driver.py +206 -5
  19. prompture/drivers/__init__.py +175 -67
  20. prompture/drivers/airllm_driver.py +109 -0
  21. prompture/drivers/async_airllm_driver.py +26 -0
  22. prompture/drivers/async_azure_driver.py +123 -0
  23. prompture/drivers/async_claude_driver.py +113 -0
  24. prompture/drivers/async_google_driver.py +316 -0
  25. prompture/drivers/async_grok_driver.py +97 -0
  26. prompture/drivers/async_groq_driver.py +90 -0
  27. prompture/drivers/async_hugging_driver.py +61 -0
  28. prompture/drivers/async_lmstudio_driver.py +148 -0
  29. prompture/drivers/async_local_http_driver.py +44 -0
  30. prompture/drivers/async_ollama_driver.py +135 -0
  31. prompture/drivers/async_openai_driver.py +102 -0
  32. prompture/drivers/async_openrouter_driver.py +102 -0
  33. prompture/drivers/async_registry.py +133 -0
  34. prompture/drivers/azure_driver.py +42 -9
  35. prompture/drivers/claude_driver.py +257 -34
  36. prompture/drivers/google_driver.py +295 -42
  37. prompture/drivers/grok_driver.py +35 -32
  38. prompture/drivers/groq_driver.py +33 -26
  39. prompture/drivers/hugging_driver.py +6 -6
  40. prompture/drivers/lmstudio_driver.py +97 -19
  41. prompture/drivers/local_http_driver.py +6 -6
  42. prompture/drivers/ollama_driver.py +168 -23
  43. prompture/drivers/openai_driver.py +184 -9
  44. prompture/drivers/openrouter_driver.py +37 -25
  45. prompture/drivers/registry.py +306 -0
  46. prompture/drivers/vision_helpers.py +153 -0
  47. prompture/field_definitions.py +106 -96
  48. prompture/group_types.py +147 -0
  49. prompture/groups.py +530 -0
  50. prompture/image.py +180 -0
  51. prompture/logging.py +80 -0
  52. prompture/model_rates.py +217 -0
  53. prompture/persistence.py +254 -0
  54. prompture/persona.py +482 -0
  55. prompture/runner.py +49 -47
  56. prompture/scaffold/__init__.py +1 -0
  57. prompture/scaffold/generator.py +84 -0
  58. prompture/scaffold/templates/Dockerfile.j2 +12 -0
  59. prompture/scaffold/templates/README.md.j2 +41 -0
  60. prompture/scaffold/templates/config.py.j2 +21 -0
  61. prompture/scaffold/templates/env.example.j2 +8 -0
  62. prompture/scaffold/templates/main.py.j2 +86 -0
  63. prompture/scaffold/templates/models.py.j2 +40 -0
  64. prompture/scaffold/templates/requirements.txt.j2 +5 -0
  65. prompture/serialization.py +218 -0
  66. prompture/server.py +183 -0
  67. prompture/session.py +117 -0
  68. prompture/settings.py +19 -1
  69. prompture/tools.py +219 -267
  70. prompture/tools_schema.py +254 -0
  71. prompture/validator.py +3 -3
  72. prompture-0.0.38.dev2.dist-info/METADATA +369 -0
  73. prompture-0.0.38.dev2.dist-info/RECORD +77 -0
  74. {prompture-0.0.29.dev8.dist-info → prompture-0.0.38.dev2.dist-info}/WHEEL +1 -1
  75. prompture-0.0.29.dev8.dist-info/METADATA +0 -368
  76. prompture-0.0.29.dev8.dist-info/RECORD +0 -27
  77. {prompture-0.0.29.dev8.dist-info → prompture-0.0.38.dev2.dist-info}/entry_points.txt +0 -0
  78. {prompture-0.0.29.dev8.dist-info → prompture-0.0.38.dev2.dist-info}/licenses/LICENSE +0 -0
  79. {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, Dict
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: Dict[str,Any]) -> Dict[str,Any]:
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)