prompture 0.0.47.dev1__tar.gz → 0.0.47.dev2__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.
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/PKG-INFO +35 -2
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/README.md +34 -1
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/_version.py +2 -2
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/async_conversation.py +71 -2
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/conversation.py +71 -2
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_moonshot_driver.py +12 -3
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/moonshot_driver.py +12 -3
- prompture-0.0.47.dev2/prompture/simulated_tools.py +115 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/tools_schema.py +22 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture.egg-info/PKG-INFO +35 -2
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture.egg-info/SOURCES.txt +1 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.claude/skills/add-driver/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.claude/skills/add-driver/references/driver-template.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.claude/skills/add-example/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.claude/skills/add-field/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.claude/skills/add-persona/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.claude/skills/add-test/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.claude/skills/add-tool/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.claude/skills/run-tests/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.claude/skills/scaffold-extraction/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.claude/skills/update-pricing/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.env.copy +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.github/FUNDING.yml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.github/scripts/update_docs_version.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.github/scripts/update_wrapper_version.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.github/workflows/dev.yml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.github/workflows/documentation.yml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/.github/workflows/publish.yml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/CLAUDE.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/LICENSE +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/MANIFEST.in +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/ROADMAP.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/_static/custom.css +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/_templates/footer.html +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/api/core.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/api/drivers.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/api/field_definitions.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/api/index.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/api/runner.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/api/tools.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/api/validator.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/conf.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/contributing.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/examples.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/field_definitions_reference.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/index.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/installation.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/quickstart.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/docs/source/toon_input_guide.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/packages/README.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/packages/llm_to_json/README.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/packages/llm_to_json/llm_to_json/__init__.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/packages/llm_to_json/pyproject.toml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/packages/llm_to_json/test.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/packages/llm_to_toon/README.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/packages/llm_to_toon/llm_to_toon/__init__.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/packages/llm_to_toon/pyproject.toml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/packages/llm_to_toon/test.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/__init__.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/agent.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/agent_types.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/aio/__init__.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/async_agent.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/async_core.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/async_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/async_groups.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/cache.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/callbacks.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/cli.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/core.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/cost_mixin.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/discovery.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/__init__.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/airllm_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_airllm_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_azure_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_claude_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_google_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_grok_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_groq_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_hugging_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_lmstudio_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_local_http_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_modelscope_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_ollama_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_openai_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_openrouter_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_registry.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_zai_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/azure_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/claude_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/google_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/grok_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/groq_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/hugging_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/lmstudio_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/local_http_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/modelscope_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/ollama_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/openai_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/openrouter_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/registry.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/vision_helpers.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/zai_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/field_definitions.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/group_types.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/groups.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/image.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/ledger.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/logging.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/model_rates.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/persistence.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/persona.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/runner.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/scaffold/__init__.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/scaffold/generator.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/scaffold/templates/Dockerfile.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/scaffold/templates/README.md.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/scaffold/templates/config.py.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/scaffold/templates/env.example.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/scaffold/templates/main.py.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/scaffold/templates/models.py.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/scaffold/templates/requirements.txt.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/serialization.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/server.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/session.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/settings.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/tools.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/validator.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture.egg-info/dependency_links.txt +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture.egg-info/entry_points.txt +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture.egg-info/requires.txt +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture.egg-info/top_level.txt +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/pyproject.toml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/requirements.txt +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/setup.cfg +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/test.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/test_version_diagnosis.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: prompture
|
|
3
|
-
Version: 0.0.47.
|
|
3
|
+
Version: 0.0.47.dev2
|
|
4
4
|
Summary: Ask LLMs to return structured JSON and run cross-model tests. API-first.
|
|
5
5
|
Author-email: Juan Denis <juan@vene.co>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -83,7 +83,7 @@ print(person.name) # Maria
|
|
|
83
83
|
- **Stepwise extraction** — Per-field prompts with smart type coercion (shorthand numbers, multilingual booleans, dates)
|
|
84
84
|
- **Field registry** — 50+ predefined extraction fields with template variables and Pydantic integration
|
|
85
85
|
- **Conversations** — Stateful multi-turn sessions with sync and async support
|
|
86
|
-
- **Tool use** — Function calling and streaming across supported providers
|
|
86
|
+
- **Tool use** — Function calling and streaming across supported providers, with automatic prompt-based simulation for models without native tool support
|
|
87
87
|
- **Caching** — Built-in response cache with memory, SQLite, and Redis backends
|
|
88
88
|
- **Plugin system** — Register custom drivers via entry points
|
|
89
89
|
- **Usage tracking** — Token counts and cost calculation on every call
|
|
@@ -296,6 +296,39 @@ response = conv.send("What is the capital of France?")
|
|
|
296
296
|
follow_up = conv.send("What about Germany?") # retains context
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
+
### Tool Use
|
|
300
|
+
|
|
301
|
+
Register Python functions as tools the LLM can call during a conversation:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
from prompture import Conversation, ToolRegistry
|
|
305
|
+
|
|
306
|
+
registry = ToolRegistry()
|
|
307
|
+
|
|
308
|
+
@registry.tool
|
|
309
|
+
def get_weather(city: str, units: str = "celsius") -> str:
|
|
310
|
+
"""Get the current weather for a city."""
|
|
311
|
+
return f"Weather in {city}: 22 {units}"
|
|
312
|
+
|
|
313
|
+
conv = Conversation("openai/gpt-4", tools=registry)
|
|
314
|
+
result = conv.ask("What's the weather in London?")
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
For models without native function calling (Ollama, LM Studio, etc.), Prompture automatically simulates tool use by describing tools in the prompt and parsing structured JSON responses:
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
# Auto-detect: uses native tool calling if available, simulation otherwise
|
|
321
|
+
conv = Conversation("ollama/llama3.1:8b", tools=registry, simulated_tools="auto")
|
|
322
|
+
|
|
323
|
+
# Force simulation even on capable models
|
|
324
|
+
conv = Conversation("openai/gpt-4", tools=registry, simulated_tools=True)
|
|
325
|
+
|
|
326
|
+
# Disable tool use entirely
|
|
327
|
+
conv = Conversation("openai/gpt-4", tools=registry, simulated_tools=False)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The simulation loop describes tools in the system prompt, asks the model to respond with JSON (`tool_call` or `final_answer`), executes tools, and feeds results back — all transparent to the caller.
|
|
331
|
+
|
|
299
332
|
### Model Discovery
|
|
300
333
|
|
|
301
334
|
Auto-detect available models from configured providers:
|
|
@@ -36,7 +36,7 @@ print(person.name) # Maria
|
|
|
36
36
|
- **Stepwise extraction** — Per-field prompts with smart type coercion (shorthand numbers, multilingual booleans, dates)
|
|
37
37
|
- **Field registry** — 50+ predefined extraction fields with template variables and Pydantic integration
|
|
38
38
|
- **Conversations** — Stateful multi-turn sessions with sync and async support
|
|
39
|
-
- **Tool use** — Function calling and streaming across supported providers
|
|
39
|
+
- **Tool use** — Function calling and streaming across supported providers, with automatic prompt-based simulation for models without native tool support
|
|
40
40
|
- **Caching** — Built-in response cache with memory, SQLite, and Redis backends
|
|
41
41
|
- **Plugin system** — Register custom drivers via entry points
|
|
42
42
|
- **Usage tracking** — Token counts and cost calculation on every call
|
|
@@ -249,6 +249,39 @@ response = conv.send("What is the capital of France?")
|
|
|
249
249
|
follow_up = conv.send("What about Germany?") # retains context
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
+
### Tool Use
|
|
253
|
+
|
|
254
|
+
Register Python functions as tools the LLM can call during a conversation:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from prompture import Conversation, ToolRegistry
|
|
258
|
+
|
|
259
|
+
registry = ToolRegistry()
|
|
260
|
+
|
|
261
|
+
@registry.tool
|
|
262
|
+
def get_weather(city: str, units: str = "celsius") -> str:
|
|
263
|
+
"""Get the current weather for a city."""
|
|
264
|
+
return f"Weather in {city}: 22 {units}"
|
|
265
|
+
|
|
266
|
+
conv = Conversation("openai/gpt-4", tools=registry)
|
|
267
|
+
result = conv.ask("What's the weather in London?")
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
For models without native function calling (Ollama, LM Studio, etc.), Prompture automatically simulates tool use by describing tools in the prompt and parsing structured JSON responses:
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
# Auto-detect: uses native tool calling if available, simulation otherwise
|
|
274
|
+
conv = Conversation("ollama/llama3.1:8b", tools=registry, simulated_tools="auto")
|
|
275
|
+
|
|
276
|
+
# Force simulation even on capable models
|
|
277
|
+
conv = Conversation("openai/gpt-4", tools=registry, simulated_tools=True)
|
|
278
|
+
|
|
279
|
+
# Disable tool use entirely
|
|
280
|
+
conv = Conversation("openai/gpt-4", tools=registry, simulated_tools=False)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The simulation loop describes tools in the system prompt, asks the model to respond with JSON (`tool_call` or `final_answer`), executes tools, and feeds results back — all transparent to the caller.
|
|
284
|
+
|
|
252
285
|
### Model Discovery
|
|
253
286
|
|
|
254
287
|
Auto-detect available models from configured providers:
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.0.47.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 0, 47, '
|
|
31
|
+
__version__ = version = '0.0.47.dev2'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 47, 'dev2')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -55,6 +55,7 @@ class AsyncConversation:
|
|
|
55
55
|
callbacks: DriverCallbacks | None = None,
|
|
56
56
|
tools: ToolRegistry | None = None,
|
|
57
57
|
max_tool_rounds: int = 10,
|
|
58
|
+
simulated_tools: bool | Literal["auto"] = "auto",
|
|
58
59
|
conversation_id: str | None = None,
|
|
59
60
|
auto_save: str | Path | None = None,
|
|
60
61
|
tags: list[str] | None = None,
|
|
@@ -106,6 +107,7 @@ class AsyncConversation:
|
|
|
106
107
|
}
|
|
107
108
|
self._tools = tools or ToolRegistry()
|
|
108
109
|
self._max_tool_rounds = max_tool_rounds
|
|
110
|
+
self._simulated_tools = simulated_tools
|
|
109
111
|
|
|
110
112
|
# Persistence
|
|
111
113
|
self._conversation_id = conversation_id or str(uuid.uuid4())
|
|
@@ -324,8 +326,13 @@ class AsyncConversation:
|
|
|
324
326
|
If tools are registered and the driver supports tool use,
|
|
325
327
|
dispatches to the async tool execution loop.
|
|
326
328
|
"""
|
|
327
|
-
|
|
328
|
-
|
|
329
|
+
# Route to appropriate tool handling
|
|
330
|
+
if self._tools:
|
|
331
|
+
use_native = getattr(self._driver, "supports_tool_use", False)
|
|
332
|
+
if self._simulated_tools is True or (self._simulated_tools == "auto" and not use_native):
|
|
333
|
+
return await self._ask_with_simulated_tools(content, options, images=images)
|
|
334
|
+
elif use_native and self._simulated_tools is not True:
|
|
335
|
+
return await self._ask_with_tools(content, options, images=images)
|
|
329
336
|
|
|
330
337
|
merged = {**self._options, **(options or {})}
|
|
331
338
|
messages = self._build_messages(content, images=images)
|
|
@@ -377,6 +384,11 @@ class AsyncConversation:
|
|
|
377
384
|
}
|
|
378
385
|
for tc in tool_calls
|
|
379
386
|
]
|
|
387
|
+
# Preserve reasoning_content for providers that require it
|
|
388
|
+
# on subsequent requests (e.g. Moonshot reasoning models).
|
|
389
|
+
if resp.get("reasoning_content") is not None:
|
|
390
|
+
assistant_msg["reasoning_content"] = resp["reasoning_content"]
|
|
391
|
+
|
|
380
392
|
self._messages.append(assistant_msg)
|
|
381
393
|
msgs.append(assistant_msg)
|
|
382
394
|
|
|
@@ -397,6 +409,63 @@ class AsyncConversation:
|
|
|
397
409
|
|
|
398
410
|
raise RuntimeError(f"Tool execution loop exceeded {self._max_tool_rounds} rounds")
|
|
399
411
|
|
|
412
|
+
async def _ask_with_simulated_tools(
|
|
413
|
+
self,
|
|
414
|
+
content: str,
|
|
415
|
+
options: dict[str, Any] | None = None,
|
|
416
|
+
images: list[ImageInput] | None = None,
|
|
417
|
+
) -> str:
|
|
418
|
+
"""Async prompt-based tool calling for drivers without native tool use."""
|
|
419
|
+
from .simulated_tools import build_tool_prompt, format_tool_result, parse_simulated_response
|
|
420
|
+
|
|
421
|
+
merged = {**self._options, **(options or {})}
|
|
422
|
+
tool_prompt = build_tool_prompt(self._tools)
|
|
423
|
+
|
|
424
|
+
# Augment system prompt with tool descriptions
|
|
425
|
+
augmented_system = tool_prompt
|
|
426
|
+
if self._system_prompt:
|
|
427
|
+
augmented_system = f"{self._system_prompt}\n\n{tool_prompt}"
|
|
428
|
+
|
|
429
|
+
# Record user message in history
|
|
430
|
+
user_content = self._build_content_with_images(content, images)
|
|
431
|
+
self._messages.append({"role": "user", "content": user_content})
|
|
432
|
+
|
|
433
|
+
for _round in range(self._max_tool_rounds):
|
|
434
|
+
# Build messages with the augmented system prompt
|
|
435
|
+
msgs: list[dict[str, Any]] = []
|
|
436
|
+
msgs.append({"role": "system", "content": augmented_system})
|
|
437
|
+
msgs.extend(self._messages)
|
|
438
|
+
|
|
439
|
+
resp = await self._driver.generate_messages_with_hooks(msgs, merged)
|
|
440
|
+
text = resp.get("text", "")
|
|
441
|
+
meta = resp.get("meta", {})
|
|
442
|
+
self._accumulate_usage(meta)
|
|
443
|
+
|
|
444
|
+
parsed = parse_simulated_response(text, self._tools)
|
|
445
|
+
|
|
446
|
+
if parsed["type"] == "final_answer":
|
|
447
|
+
answer = parsed["content"]
|
|
448
|
+
self._messages.append({"role": "assistant", "content": answer})
|
|
449
|
+
return answer
|
|
450
|
+
|
|
451
|
+
# Tool call
|
|
452
|
+
tool_name = parsed["name"]
|
|
453
|
+
tool_args = parsed["arguments"]
|
|
454
|
+
|
|
455
|
+
# Record assistant's tool call as an assistant message
|
|
456
|
+
self._messages.append({"role": "assistant", "content": text})
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
result = self._tools.execute(tool_name, tool_args)
|
|
460
|
+
result_msg = format_tool_result(tool_name, result)
|
|
461
|
+
except Exception as exc:
|
|
462
|
+
result_msg = format_tool_result(tool_name, f"Error: {exc}")
|
|
463
|
+
|
|
464
|
+
# Record tool result as a user message
|
|
465
|
+
self._messages.append({"role": "user", "content": result_msg})
|
|
466
|
+
|
|
467
|
+
raise RuntimeError(f"Simulated tool execution loop exceeded {self._max_tool_rounds} rounds")
|
|
468
|
+
|
|
400
469
|
def _build_messages_raw(self) -> list[dict[str, Any]]:
|
|
401
470
|
"""Build messages array from system prompt + full history (including tool messages)."""
|
|
402
471
|
msgs: list[dict[str, Any]] = []
|
|
@@ -56,6 +56,7 @@ class Conversation:
|
|
|
56
56
|
callbacks: DriverCallbacks | None = None,
|
|
57
57
|
tools: ToolRegistry | None = None,
|
|
58
58
|
max_tool_rounds: int = 10,
|
|
59
|
+
simulated_tools: bool | Literal["auto"] = "auto",
|
|
59
60
|
conversation_id: str | None = None,
|
|
60
61
|
auto_save: str | Path | None = None,
|
|
61
62
|
tags: list[str] | None = None,
|
|
@@ -109,6 +110,7 @@ class Conversation:
|
|
|
109
110
|
}
|
|
110
111
|
self._tools = tools or ToolRegistry()
|
|
111
112
|
self._max_tool_rounds = max_tool_rounds
|
|
113
|
+
self._simulated_tools = simulated_tools
|
|
112
114
|
|
|
113
115
|
# Persistence
|
|
114
116
|
self._conversation_id = conversation_id or str(uuid.uuid4())
|
|
@@ -338,8 +340,13 @@ class Conversation:
|
|
|
338
340
|
images: Optional list of images to include (bytes, path, URL,
|
|
339
341
|
base64 string, or :class:`ImageContent`).
|
|
340
342
|
"""
|
|
341
|
-
|
|
342
|
-
|
|
343
|
+
# Route to appropriate tool handling
|
|
344
|
+
if self._tools:
|
|
345
|
+
use_native = getattr(self._driver, "supports_tool_use", False)
|
|
346
|
+
if self._simulated_tools is True or (self._simulated_tools == "auto" and not use_native):
|
|
347
|
+
return self._ask_with_simulated_tools(content, options, images=images)
|
|
348
|
+
elif use_native and self._simulated_tools is not True:
|
|
349
|
+
return self._ask_with_tools(content, options, images=images)
|
|
343
350
|
|
|
344
351
|
merged = {**self._options, **(options or {})}
|
|
345
352
|
messages = self._build_messages(content, images=images)
|
|
@@ -395,6 +402,11 @@ class Conversation:
|
|
|
395
402
|
}
|
|
396
403
|
for tc in tool_calls
|
|
397
404
|
]
|
|
405
|
+
# Preserve reasoning_content for providers that require it
|
|
406
|
+
# on subsequent requests (e.g. Moonshot reasoning models).
|
|
407
|
+
if resp.get("reasoning_content") is not None:
|
|
408
|
+
assistant_msg["reasoning_content"] = resp["reasoning_content"]
|
|
409
|
+
|
|
398
410
|
self._messages.append(assistant_msg)
|
|
399
411
|
msgs.append(assistant_msg)
|
|
400
412
|
|
|
@@ -416,6 +428,63 @@ class Conversation:
|
|
|
416
428
|
|
|
417
429
|
raise RuntimeError(f"Tool execution loop exceeded {self._max_tool_rounds} rounds")
|
|
418
430
|
|
|
431
|
+
def _ask_with_simulated_tools(
|
|
432
|
+
self,
|
|
433
|
+
content: str,
|
|
434
|
+
options: dict[str, Any] | None = None,
|
|
435
|
+
images: list[ImageInput] | None = None,
|
|
436
|
+
) -> str:
|
|
437
|
+
"""Prompt-based tool calling for drivers without native tool use."""
|
|
438
|
+
from .simulated_tools import build_tool_prompt, format_tool_result, parse_simulated_response
|
|
439
|
+
|
|
440
|
+
merged = {**self._options, **(options or {})}
|
|
441
|
+
tool_prompt = build_tool_prompt(self._tools)
|
|
442
|
+
|
|
443
|
+
# Augment system prompt with tool descriptions
|
|
444
|
+
augmented_system = tool_prompt
|
|
445
|
+
if self._system_prompt:
|
|
446
|
+
augmented_system = f"{self._system_prompt}\n\n{tool_prompt}"
|
|
447
|
+
|
|
448
|
+
# Record user message in history
|
|
449
|
+
user_content = self._build_content_with_images(content, images)
|
|
450
|
+
self._messages.append({"role": "user", "content": user_content})
|
|
451
|
+
|
|
452
|
+
for _round in range(self._max_tool_rounds):
|
|
453
|
+
# Build messages with the augmented system prompt
|
|
454
|
+
msgs: list[dict[str, Any]] = []
|
|
455
|
+
msgs.append({"role": "system", "content": augmented_system})
|
|
456
|
+
msgs.extend(self._messages)
|
|
457
|
+
|
|
458
|
+
resp = self._driver.generate_messages_with_hooks(msgs, merged)
|
|
459
|
+
text = resp.get("text", "")
|
|
460
|
+
meta = resp.get("meta", {})
|
|
461
|
+
self._accumulate_usage(meta)
|
|
462
|
+
|
|
463
|
+
parsed = parse_simulated_response(text, self._tools)
|
|
464
|
+
|
|
465
|
+
if parsed["type"] == "final_answer":
|
|
466
|
+
answer = parsed["content"]
|
|
467
|
+
self._messages.append({"role": "assistant", "content": answer})
|
|
468
|
+
return answer
|
|
469
|
+
|
|
470
|
+
# Tool call
|
|
471
|
+
tool_name = parsed["name"]
|
|
472
|
+
tool_args = parsed["arguments"]
|
|
473
|
+
|
|
474
|
+
# Record assistant's tool call as an assistant message
|
|
475
|
+
self._messages.append({"role": "assistant", "content": text})
|
|
476
|
+
|
|
477
|
+
try:
|
|
478
|
+
result = self._tools.execute(tool_name, tool_args)
|
|
479
|
+
result_msg = format_tool_result(tool_name, result)
|
|
480
|
+
except Exception as exc:
|
|
481
|
+
result_msg = format_tool_result(tool_name, f"Error: {exc}")
|
|
482
|
+
|
|
483
|
+
# Record tool result as a user message (all drivers understand user/assistant)
|
|
484
|
+
self._messages.append({"role": "user", "content": result_msg})
|
|
485
|
+
|
|
486
|
+
raise RuntimeError(f"Simulated tool execution loop exceeded {self._max_tool_rounds} rounds")
|
|
487
|
+
|
|
419
488
|
def _build_messages_raw(self) -> list[dict[str, Any]]:
|
|
420
489
|
"""Build messages array from system prompt + full history (including tool messages)."""
|
|
421
490
|
msgs: list[dict[str, Any]] = []
|
|
@@ -271,11 +271,12 @@ class AsyncMoonshotDriver(CostMixin, AsyncDriver):
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
choice = resp["choices"][0]
|
|
274
|
-
|
|
274
|
+
message = choice["message"]
|
|
275
|
+
text = message.get("content") or ""
|
|
275
276
|
stop_reason = choice.get("finish_reason")
|
|
276
277
|
|
|
277
278
|
tool_calls_out: list[dict[str, Any]] = []
|
|
278
|
-
for tc in
|
|
279
|
+
for tc in message.get("tool_calls", []):
|
|
279
280
|
try:
|
|
280
281
|
args = json.loads(tc["function"]["arguments"])
|
|
281
282
|
except (json.JSONDecodeError, TypeError):
|
|
@@ -288,13 +289,21 @@ class AsyncMoonshotDriver(CostMixin, AsyncDriver):
|
|
|
288
289
|
}
|
|
289
290
|
)
|
|
290
291
|
|
|
291
|
-
|
|
292
|
+
result: dict[str, Any] = {
|
|
292
293
|
"text": text,
|
|
293
294
|
"meta": meta,
|
|
294
295
|
"tool_calls": tool_calls_out,
|
|
295
296
|
"stop_reason": stop_reason,
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
# Preserve reasoning_content for reasoning models so the
|
|
300
|
+
# conversation loop can include it when sending the assistant
|
|
301
|
+
# message back (Moonshot requires it on subsequent requests).
|
|
302
|
+
if message.get("reasoning_content") is not None:
|
|
303
|
+
result["reasoning_content"] = message["reasoning_content"]
|
|
304
|
+
|
|
305
|
+
return result
|
|
306
|
+
|
|
298
307
|
# ------------------------------------------------------------------
|
|
299
308
|
# Streaming
|
|
300
309
|
# ------------------------------------------------------------------
|
|
@@ -364,11 +364,12 @@ class MoonshotDriver(CostMixin, Driver):
|
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
choice = resp["choices"][0]
|
|
367
|
-
|
|
367
|
+
message = choice["message"]
|
|
368
|
+
text = message.get("content") or ""
|
|
368
369
|
stop_reason = choice.get("finish_reason")
|
|
369
370
|
|
|
370
371
|
tool_calls_out: list[dict[str, Any]] = []
|
|
371
|
-
for tc in
|
|
372
|
+
for tc in message.get("tool_calls", []):
|
|
372
373
|
try:
|
|
373
374
|
args = json.loads(tc["function"]["arguments"])
|
|
374
375
|
except (json.JSONDecodeError, TypeError):
|
|
@@ -381,13 +382,21 @@ class MoonshotDriver(CostMixin, Driver):
|
|
|
381
382
|
}
|
|
382
383
|
)
|
|
383
384
|
|
|
384
|
-
|
|
385
|
+
result: dict[str, Any] = {
|
|
385
386
|
"text": text,
|
|
386
387
|
"meta": meta,
|
|
387
388
|
"tool_calls": tool_calls_out,
|
|
388
389
|
"stop_reason": stop_reason,
|
|
389
390
|
}
|
|
390
391
|
|
|
392
|
+
# Preserve reasoning_content for reasoning models so the
|
|
393
|
+
# conversation loop can include it when sending the assistant
|
|
394
|
+
# message back (Moonshot requires it on subsequent requests).
|
|
395
|
+
if message.get("reasoning_content") is not None:
|
|
396
|
+
result["reasoning_content"] = message["reasoning_content"]
|
|
397
|
+
|
|
398
|
+
return result
|
|
399
|
+
|
|
391
400
|
# ------------------------------------------------------------------
|
|
392
401
|
# Streaming
|
|
393
402
|
# ------------------------------------------------------------------
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Prompt-based tool calling for drivers without native tool use support.
|
|
2
|
+
|
|
3
|
+
When a driver lacks ``supports_tool_use`` the conversation classes can
|
|
4
|
+
fall back to *simulated* tool calling: the available tools are described
|
|
5
|
+
in the system prompt, the model is asked to respond with a structured
|
|
6
|
+
JSON object (either a tool call or a final answer), and Prompture
|
|
7
|
+
parses + dispatches accordingly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .tools import clean_json_text
|
|
17
|
+
from .tools_schema import ToolRegistry
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("prompture.simulated_tools")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_tool_prompt(tools: ToolRegistry) -> str:
|
|
23
|
+
"""Build a plain-text prompt section describing all registered tools.
|
|
24
|
+
|
|
25
|
+
The returned string should be appended to the system prompt so the
|
|
26
|
+
model knows which tools are available and how to call them.
|
|
27
|
+
"""
|
|
28
|
+
lines = [
|
|
29
|
+
"You have access to the following tools:",
|
|
30
|
+
"",
|
|
31
|
+
tools.to_prompt_format(),
|
|
32
|
+
"",
|
|
33
|
+
"To use a tool, respond with ONLY a JSON object in this exact format:",
|
|
34
|
+
'{"type": "tool_call", "name": "<tool_name>", "arguments": {<args>}}',
|
|
35
|
+
"",
|
|
36
|
+
"When you have the final answer (after using tools or if no tool is needed), "
|
|
37
|
+
"respond with ONLY a JSON object in this format:",
|
|
38
|
+
'{"type": "final_answer", "content": "<your answer>"}',
|
|
39
|
+
"",
|
|
40
|
+
"IMPORTANT: Your entire response must be a single JSON object. "
|
|
41
|
+
"Do not include any other text, markdown, or explanation outside the JSON.",
|
|
42
|
+
]
|
|
43
|
+
return "\n".join(lines)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_simulated_response(text: str, tools: ToolRegistry) -> dict[str, Any]:
|
|
47
|
+
"""Parse the model's response into a tool call or final answer dict.
|
|
48
|
+
|
|
49
|
+
Returns one of:
|
|
50
|
+
- ``{"type": "tool_call", "name": str, "arguments": dict}``
|
|
51
|
+
- ``{"type": "final_answer", "content": str}``
|
|
52
|
+
"""
|
|
53
|
+
cleaned = clean_json_text(text).strip()
|
|
54
|
+
|
|
55
|
+
# Try JSON parse
|
|
56
|
+
try:
|
|
57
|
+
obj = json.loads(cleaned)
|
|
58
|
+
except (json.JSONDecodeError, ValueError):
|
|
59
|
+
# Non-JSON text → treat as final answer
|
|
60
|
+
logger.debug("Response is not valid JSON, treating as final answer")
|
|
61
|
+
return {"type": "final_answer", "content": text.strip()}
|
|
62
|
+
|
|
63
|
+
if not isinstance(obj, dict):
|
|
64
|
+
return {"type": "final_answer", "content": text.strip()}
|
|
65
|
+
|
|
66
|
+
# Explicit type discriminator
|
|
67
|
+
resp_type = obj.get("type")
|
|
68
|
+
|
|
69
|
+
if resp_type == "tool_call":
|
|
70
|
+
return {
|
|
71
|
+
"type": "tool_call",
|
|
72
|
+
"name": obj.get("name", ""),
|
|
73
|
+
"arguments": obj.get("arguments", {}),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if resp_type == "final_answer":
|
|
77
|
+
return {
|
|
78
|
+
"type": "final_answer",
|
|
79
|
+
"content": obj.get("content", ""),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Infer type from keys when "type" is missing
|
|
83
|
+
if "name" in obj and "arguments" in obj:
|
|
84
|
+
logger.debug("Inferred tool_call from keys (no 'type' field)")
|
|
85
|
+
return {
|
|
86
|
+
"type": "tool_call",
|
|
87
|
+
"name": obj["name"],
|
|
88
|
+
"arguments": obj.get("arguments", {}),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if "content" in obj:
|
|
92
|
+
logger.debug("Inferred final_answer from keys (no 'type' field)")
|
|
93
|
+
return {
|
|
94
|
+
"type": "final_answer",
|
|
95
|
+
"content": obj["content"],
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Unrecognised JSON structure → final answer with the raw text
|
|
99
|
+
return {"type": "final_answer", "content": text.strip()}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def format_tool_result(tool_name: str, result: Any) -> str:
|
|
103
|
+
"""Format a tool execution result as a user message for the next round."""
|
|
104
|
+
if isinstance(result, str):
|
|
105
|
+
result_str = result
|
|
106
|
+
else:
|
|
107
|
+
try:
|
|
108
|
+
result_str = json.dumps(result)
|
|
109
|
+
except (TypeError, ValueError):
|
|
110
|
+
result_str = str(result)
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
f"Tool '{tool_name}' returned:\n{result_str}\n\n"
|
|
114
|
+
"Continue using the JSON format. Either call another tool or provide your final answer."
|
|
115
|
+
)
|
|
@@ -109,6 +109,24 @@ class ToolDefinition:
|
|
|
109
109
|
"input_schema": self.parameters,
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
def to_prompt_format(self) -> str:
|
|
113
|
+
"""Plain-text description suitable for prompt-based tool calling."""
|
|
114
|
+
lines = [f"Tool: {self.name}", f" Description: {self.description}", " Parameters:"]
|
|
115
|
+
props = self.parameters.get("properties", {})
|
|
116
|
+
required = set(self.parameters.get("required", []))
|
|
117
|
+
if not props:
|
|
118
|
+
lines.append(" (none)")
|
|
119
|
+
else:
|
|
120
|
+
for pname, pschema in props.items():
|
|
121
|
+
ptype = pschema.get("type", "string")
|
|
122
|
+
req_label = "required" if pname in required else "optional"
|
|
123
|
+
desc = pschema.get("description", "")
|
|
124
|
+
line = f" - {pname} ({ptype}, {req_label})"
|
|
125
|
+
if desc:
|
|
126
|
+
line += f": {desc}"
|
|
127
|
+
lines.append(line)
|
|
128
|
+
return "\n".join(lines)
|
|
129
|
+
|
|
112
130
|
|
|
113
131
|
def tool_from_function(
|
|
114
132
|
fn: Callable[..., Any], *, name: str | None = None, description: str | None = None
|
|
@@ -244,6 +262,10 @@ class ToolRegistry:
|
|
|
244
262
|
def to_anthropic_format(self) -> list[dict[str, Any]]:
|
|
245
263
|
return [td.to_anthropic_format() for td in self._tools.values()]
|
|
246
264
|
|
|
265
|
+
def to_prompt_format(self) -> str:
|
|
266
|
+
"""Join all tool descriptions into a single plain-text block."""
|
|
267
|
+
return "\n\n".join(td.to_prompt_format() for td in self._tools.values())
|
|
268
|
+
|
|
247
269
|
# ------------------------------------------------------------------
|
|
248
270
|
# Execution
|
|
249
271
|
# ------------------------------------------------------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: prompture
|
|
3
|
-
Version: 0.0.47.
|
|
3
|
+
Version: 0.0.47.dev2
|
|
4
4
|
Summary: Ask LLMs to return structured JSON and run cross-model tests. API-first.
|
|
5
5
|
Author-email: Juan Denis <juan@vene.co>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -83,7 +83,7 @@ print(person.name) # Maria
|
|
|
83
83
|
- **Stepwise extraction** — Per-field prompts with smart type coercion (shorthand numbers, multilingual booleans, dates)
|
|
84
84
|
- **Field registry** — 50+ predefined extraction fields with template variables and Pydantic integration
|
|
85
85
|
- **Conversations** — Stateful multi-turn sessions with sync and async support
|
|
86
|
-
- **Tool use** — Function calling and streaming across supported providers
|
|
86
|
+
- **Tool use** — Function calling and streaming across supported providers, with automatic prompt-based simulation for models without native tool support
|
|
87
87
|
- **Caching** — Built-in response cache with memory, SQLite, and Redis backends
|
|
88
88
|
- **Plugin system** — Register custom drivers via entry points
|
|
89
89
|
- **Usage tracking** — Token counts and cost calculation on every call
|
|
@@ -296,6 +296,39 @@ response = conv.send("What is the capital of France?")
|
|
|
296
296
|
follow_up = conv.send("What about Germany?") # retains context
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
+
### Tool Use
|
|
300
|
+
|
|
301
|
+
Register Python functions as tools the LLM can call during a conversation:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
from prompture import Conversation, ToolRegistry
|
|
305
|
+
|
|
306
|
+
registry = ToolRegistry()
|
|
307
|
+
|
|
308
|
+
@registry.tool
|
|
309
|
+
def get_weather(city: str, units: str = "celsius") -> str:
|
|
310
|
+
"""Get the current weather for a city."""
|
|
311
|
+
return f"Weather in {city}: 22 {units}"
|
|
312
|
+
|
|
313
|
+
conv = Conversation("openai/gpt-4", tools=registry)
|
|
314
|
+
result = conv.ask("What's the weather in London?")
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
For models without native function calling (Ollama, LM Studio, etc.), Prompture automatically simulates tool use by describing tools in the prompt and parsing structured JSON responses:
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
# Auto-detect: uses native tool calling if available, simulation otherwise
|
|
321
|
+
conv = Conversation("ollama/llama3.1:8b", tools=registry, simulated_tools="auto")
|
|
322
|
+
|
|
323
|
+
# Force simulation even on capable models
|
|
324
|
+
conv = Conversation("openai/gpt-4", tools=registry, simulated_tools=True)
|
|
325
|
+
|
|
326
|
+
# Disable tool use entirely
|
|
327
|
+
conv = Conversation("openai/gpt-4", tools=registry, simulated_tools=False)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The simulation loop describes tools in the system prompt, asks the model to respond with JSON (`tool_call` or `final_answer`), executes tools, and feeds results back — all transparent to the caller.
|
|
331
|
+
|
|
299
332
|
### Model Discovery
|
|
300
333
|
|
|
301
334
|
Auto-detect available models from configured providers:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/packages/llm_to_json/llm_to_json/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/packages/llm_to_toon/llm_to_toon/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_local_http_driver.py
RENAMED
|
File without changes
|
{prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_modelscope_driver.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/drivers/async_openrouter_driver.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{prompture-0.0.47.dev1 → prompture-0.0.47.dev2}/prompture/scaffold/templates/requirements.txt.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|