prompture 0.0.47.dev1__tar.gz → 0.0.47.dev3__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.dev3}/PKG-INFO +35 -2
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/README.md +34 -1
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/_version.py +2 -2
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/async_conversation.py +87 -2
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/conversation.py +87 -2
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_grok_driver.py +23 -9
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_groq_driver.py +23 -9
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_lmstudio_driver.py +10 -2
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_moonshot_driver.py +32 -12
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_openrouter_driver.py +43 -17
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/grok_driver.py +23 -9
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/groq_driver.py +23 -9
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/lmstudio_driver.py +11 -2
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/moonshot_driver.py +32 -12
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/openrouter_driver.py +34 -10
- prompture-0.0.47.dev3/prompture/simulated_tools.py +115 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/tools_schema.py +22 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture.egg-info/PKG-INFO +35 -2
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture.egg-info/SOURCES.txt +1 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.claude/skills/add-driver/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.claude/skills/add-driver/references/driver-template.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.claude/skills/add-example/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.claude/skills/add-field/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.claude/skills/add-persona/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.claude/skills/add-test/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.claude/skills/add-tool/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.claude/skills/run-tests/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.claude/skills/scaffold-extraction/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.claude/skills/update-pricing/SKILL.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.env.copy +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.github/FUNDING.yml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.github/scripts/update_docs_version.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.github/scripts/update_wrapper_version.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.github/workflows/dev.yml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.github/workflows/documentation.yml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/.github/workflows/publish.yml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/CLAUDE.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/LICENSE +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/MANIFEST.in +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/ROADMAP.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/_static/custom.css +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/_templates/footer.html +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/api/core.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/api/drivers.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/api/field_definitions.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/api/index.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/api/runner.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/api/tools.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/api/validator.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/conf.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/contributing.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/examples.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/field_definitions_reference.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/index.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/installation.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/quickstart.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/docs/source/toon_input_guide.rst +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/packages/README.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/packages/llm_to_json/README.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/packages/llm_to_json/llm_to_json/__init__.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/packages/llm_to_json/pyproject.toml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/packages/llm_to_json/test.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/packages/llm_to_toon/README.md +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/packages/llm_to_toon/llm_to_toon/__init__.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/packages/llm_to_toon/pyproject.toml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/packages/llm_to_toon/test.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/__init__.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/agent.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/agent_types.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/aio/__init__.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/async_agent.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/async_core.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/async_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/async_groups.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/cache.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/callbacks.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/cli.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/core.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/cost_mixin.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/discovery.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/__init__.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/airllm_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_airllm_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_azure_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_claude_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_google_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_hugging_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_local_http_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_modelscope_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_ollama_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_openai_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_registry.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/async_zai_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/azure_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/claude_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/google_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/hugging_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/local_http_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/modelscope_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/ollama_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/openai_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/registry.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/vision_helpers.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/drivers/zai_driver.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/field_definitions.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/group_types.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/groups.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/image.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/ledger.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/logging.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/model_rates.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/persistence.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/persona.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/runner.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/scaffold/__init__.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/scaffold/generator.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/scaffold/templates/Dockerfile.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/scaffold/templates/README.md.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/scaffold/templates/config.py.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/scaffold/templates/env.example.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/scaffold/templates/main.py.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/scaffold/templates/models.py.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/scaffold/templates/requirements.txt.j2 +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/serialization.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/server.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/session.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/settings.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/tools.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture/validator.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture.egg-info/dependency_links.txt +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture.egg-info/entry_points.txt +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture.egg-info/requires.txt +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/prompture.egg-info/top_level.txt +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/pyproject.toml +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/requirements.txt +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/setup.cfg +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/test.py +0 -0
- {prompture-0.0.47.dev1 → prompture-0.0.47.dev3}/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.dev3
|
|
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.dev3'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 47, 'dev3')
|
|
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,10 @@ 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
|
|
111
|
+
|
|
112
|
+
# Reasoning content from last response
|
|
113
|
+
self._last_reasoning: str | None = None
|
|
109
114
|
|
|
110
115
|
# Persistence
|
|
111
116
|
self._conversation_id = conversation_id or str(uuid.uuid4())
|
|
@@ -119,6 +124,11 @@ class AsyncConversation:
|
|
|
119
124
|
# Public helpers
|
|
120
125
|
# ------------------------------------------------------------------
|
|
121
126
|
|
|
127
|
+
@property
|
|
128
|
+
def last_reasoning(self) -> str | None:
|
|
129
|
+
"""The reasoning/thinking content from the last LLM response, if any."""
|
|
130
|
+
return self._last_reasoning
|
|
131
|
+
|
|
122
132
|
@property
|
|
123
133
|
def messages(self) -> list[dict[str, Any]]:
|
|
124
134
|
"""Read-only view of the conversation history."""
|
|
@@ -324,8 +334,15 @@ class AsyncConversation:
|
|
|
324
334
|
If tools are registered and the driver supports tool use,
|
|
325
335
|
dispatches to the async tool execution loop.
|
|
326
336
|
"""
|
|
327
|
-
|
|
328
|
-
|
|
337
|
+
self._last_reasoning = None
|
|
338
|
+
|
|
339
|
+
# Route to appropriate tool handling
|
|
340
|
+
if self._tools:
|
|
341
|
+
use_native = getattr(self._driver, "supports_tool_use", False)
|
|
342
|
+
if self._simulated_tools is True or (self._simulated_tools == "auto" and not use_native):
|
|
343
|
+
return await self._ask_with_simulated_tools(content, options, images=images)
|
|
344
|
+
elif use_native and self._simulated_tools is not True:
|
|
345
|
+
return await self._ask_with_tools(content, options, images=images)
|
|
329
346
|
|
|
330
347
|
merged = {**self._options, **(options or {})}
|
|
331
348
|
messages = self._build_messages(content, images=images)
|
|
@@ -333,6 +350,7 @@ class AsyncConversation:
|
|
|
333
350
|
|
|
334
351
|
text = resp.get("text", "")
|
|
335
352
|
meta = resp.get("meta", {})
|
|
353
|
+
self._last_reasoning = resp.get("reasoning_content")
|
|
336
354
|
|
|
337
355
|
user_content = self._build_content_with_images(content, images)
|
|
338
356
|
self._messages.append({"role": "user", "content": user_content})
|
|
@@ -365,6 +383,7 @@ class AsyncConversation:
|
|
|
365
383
|
text = resp.get("text", "")
|
|
366
384
|
|
|
367
385
|
if not tool_calls:
|
|
386
|
+
self._last_reasoning = resp.get("reasoning_content")
|
|
368
387
|
self._messages.append({"role": "assistant", "content": text})
|
|
369
388
|
return text
|
|
370
389
|
|
|
@@ -377,6 +396,11 @@ class AsyncConversation:
|
|
|
377
396
|
}
|
|
378
397
|
for tc in tool_calls
|
|
379
398
|
]
|
|
399
|
+
# Preserve reasoning_content for providers that require it
|
|
400
|
+
# on subsequent requests (e.g. Moonshot reasoning models).
|
|
401
|
+
if resp.get("reasoning_content") is not None:
|
|
402
|
+
assistant_msg["reasoning_content"] = resp["reasoning_content"]
|
|
403
|
+
|
|
380
404
|
self._messages.append(assistant_msg)
|
|
381
405
|
msgs.append(assistant_msg)
|
|
382
406
|
|
|
@@ -397,6 +421,63 @@ class AsyncConversation:
|
|
|
397
421
|
|
|
398
422
|
raise RuntimeError(f"Tool execution loop exceeded {self._max_tool_rounds} rounds")
|
|
399
423
|
|
|
424
|
+
async def _ask_with_simulated_tools(
|
|
425
|
+
self,
|
|
426
|
+
content: str,
|
|
427
|
+
options: dict[str, Any] | None = None,
|
|
428
|
+
images: list[ImageInput] | None = None,
|
|
429
|
+
) -> str:
|
|
430
|
+
"""Async prompt-based tool calling for drivers without native tool use."""
|
|
431
|
+
from .simulated_tools import build_tool_prompt, format_tool_result, parse_simulated_response
|
|
432
|
+
|
|
433
|
+
merged = {**self._options, **(options or {})}
|
|
434
|
+
tool_prompt = build_tool_prompt(self._tools)
|
|
435
|
+
|
|
436
|
+
# Augment system prompt with tool descriptions
|
|
437
|
+
augmented_system = tool_prompt
|
|
438
|
+
if self._system_prompt:
|
|
439
|
+
augmented_system = f"{self._system_prompt}\n\n{tool_prompt}"
|
|
440
|
+
|
|
441
|
+
# Record user message in history
|
|
442
|
+
user_content = self._build_content_with_images(content, images)
|
|
443
|
+
self._messages.append({"role": "user", "content": user_content})
|
|
444
|
+
|
|
445
|
+
for _round in range(self._max_tool_rounds):
|
|
446
|
+
# Build messages with the augmented system prompt
|
|
447
|
+
msgs: list[dict[str, Any]] = []
|
|
448
|
+
msgs.append({"role": "system", "content": augmented_system})
|
|
449
|
+
msgs.extend(self._messages)
|
|
450
|
+
|
|
451
|
+
resp = await self._driver.generate_messages_with_hooks(msgs, merged)
|
|
452
|
+
text = resp.get("text", "")
|
|
453
|
+
meta = resp.get("meta", {})
|
|
454
|
+
self._accumulate_usage(meta)
|
|
455
|
+
|
|
456
|
+
parsed = parse_simulated_response(text, self._tools)
|
|
457
|
+
|
|
458
|
+
if parsed["type"] == "final_answer":
|
|
459
|
+
answer = parsed["content"]
|
|
460
|
+
self._messages.append({"role": "assistant", "content": answer})
|
|
461
|
+
return answer
|
|
462
|
+
|
|
463
|
+
# Tool call
|
|
464
|
+
tool_name = parsed["name"]
|
|
465
|
+
tool_args = parsed["arguments"]
|
|
466
|
+
|
|
467
|
+
# Record assistant's tool call as an assistant message
|
|
468
|
+
self._messages.append({"role": "assistant", "content": text})
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
result = self._tools.execute(tool_name, tool_args)
|
|
472
|
+
result_msg = format_tool_result(tool_name, result)
|
|
473
|
+
except Exception as exc:
|
|
474
|
+
result_msg = format_tool_result(tool_name, f"Error: {exc}")
|
|
475
|
+
|
|
476
|
+
# Record tool result as a user message
|
|
477
|
+
self._messages.append({"role": "user", "content": result_msg})
|
|
478
|
+
|
|
479
|
+
raise RuntimeError(f"Simulated tool execution loop exceeded {self._max_tool_rounds} rounds")
|
|
480
|
+
|
|
400
481
|
def _build_messages_raw(self) -> list[dict[str, Any]]:
|
|
401
482
|
"""Build messages array from system prompt + full history (including tool messages)."""
|
|
402
483
|
msgs: list[dict[str, Any]] = []
|
|
@@ -457,6 +538,8 @@ class AsyncConversation:
|
|
|
457
538
|
images: list[ImageInput] | None = None,
|
|
458
539
|
) -> dict[str, Any]:
|
|
459
540
|
"""Send a message with schema enforcement and get structured JSON back (async)."""
|
|
541
|
+
self._last_reasoning = None
|
|
542
|
+
|
|
460
543
|
merged = {**self._options, **(options or {})}
|
|
461
544
|
|
|
462
545
|
schema_string = json.dumps(json_schema, indent=2)
|
|
@@ -494,6 +577,7 @@ class AsyncConversation:
|
|
|
494
577
|
|
|
495
578
|
text = resp.get("text", "")
|
|
496
579
|
meta = resp.get("meta", {})
|
|
580
|
+
self._last_reasoning = resp.get("reasoning_content")
|
|
497
581
|
|
|
498
582
|
user_content = self._build_content_with_images(content, images)
|
|
499
583
|
self._messages.append({"role": "user", "content": user_content})
|
|
@@ -528,6 +612,7 @@ class AsyncConversation:
|
|
|
528
612
|
"json_object": json_obj,
|
|
529
613
|
"usage": usage,
|
|
530
614
|
"output_format": output_format,
|
|
615
|
+
"reasoning": self._last_reasoning,
|
|
531
616
|
}
|
|
532
617
|
|
|
533
618
|
if output_format == "toon":
|
|
@@ -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,10 @@ 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
|
|
114
|
+
|
|
115
|
+
# Reasoning content from last response
|
|
116
|
+
self._last_reasoning: str | None = None
|
|
112
117
|
|
|
113
118
|
# Persistence
|
|
114
119
|
self._conversation_id = conversation_id or str(uuid.uuid4())
|
|
@@ -122,6 +127,11 @@ class Conversation:
|
|
|
122
127
|
# Public helpers
|
|
123
128
|
# ------------------------------------------------------------------
|
|
124
129
|
|
|
130
|
+
@property
|
|
131
|
+
def last_reasoning(self) -> str | None:
|
|
132
|
+
"""The reasoning/thinking content from the last LLM response, if any."""
|
|
133
|
+
return self._last_reasoning
|
|
134
|
+
|
|
125
135
|
@property
|
|
126
136
|
def messages(self) -> list[dict[str, Any]]:
|
|
127
137
|
"""Read-only view of the conversation history."""
|
|
@@ -338,8 +348,15 @@ class Conversation:
|
|
|
338
348
|
images: Optional list of images to include (bytes, path, URL,
|
|
339
349
|
base64 string, or :class:`ImageContent`).
|
|
340
350
|
"""
|
|
341
|
-
|
|
342
|
-
|
|
351
|
+
self._last_reasoning = None
|
|
352
|
+
|
|
353
|
+
# Route to appropriate tool handling
|
|
354
|
+
if self._tools:
|
|
355
|
+
use_native = getattr(self._driver, "supports_tool_use", False)
|
|
356
|
+
if self._simulated_tools is True or (self._simulated_tools == "auto" and not use_native):
|
|
357
|
+
return self._ask_with_simulated_tools(content, options, images=images)
|
|
358
|
+
elif use_native and self._simulated_tools is not True:
|
|
359
|
+
return self._ask_with_tools(content, options, images=images)
|
|
343
360
|
|
|
344
361
|
merged = {**self._options, **(options or {})}
|
|
345
362
|
messages = self._build_messages(content, images=images)
|
|
@@ -347,6 +364,7 @@ class Conversation:
|
|
|
347
364
|
|
|
348
365
|
text = resp.get("text", "")
|
|
349
366
|
meta = resp.get("meta", {})
|
|
367
|
+
self._last_reasoning = resp.get("reasoning_content")
|
|
350
368
|
|
|
351
369
|
# Record in history — store content with images for context
|
|
352
370
|
user_content = self._build_content_with_images(content, images)
|
|
@@ -382,6 +400,7 @@ class Conversation:
|
|
|
382
400
|
|
|
383
401
|
if not tool_calls:
|
|
384
402
|
# No tool calls -> final response
|
|
403
|
+
self._last_reasoning = resp.get("reasoning_content")
|
|
385
404
|
self._messages.append({"role": "assistant", "content": text})
|
|
386
405
|
return text
|
|
387
406
|
|
|
@@ -395,6 +414,11 @@ class Conversation:
|
|
|
395
414
|
}
|
|
396
415
|
for tc in tool_calls
|
|
397
416
|
]
|
|
417
|
+
# Preserve reasoning_content for providers that require it
|
|
418
|
+
# on subsequent requests (e.g. Moonshot reasoning models).
|
|
419
|
+
if resp.get("reasoning_content") is not None:
|
|
420
|
+
assistant_msg["reasoning_content"] = resp["reasoning_content"]
|
|
421
|
+
|
|
398
422
|
self._messages.append(assistant_msg)
|
|
399
423
|
msgs.append(assistant_msg)
|
|
400
424
|
|
|
@@ -416,6 +440,63 @@ class Conversation:
|
|
|
416
440
|
|
|
417
441
|
raise RuntimeError(f"Tool execution loop exceeded {self._max_tool_rounds} rounds")
|
|
418
442
|
|
|
443
|
+
def _ask_with_simulated_tools(
|
|
444
|
+
self,
|
|
445
|
+
content: str,
|
|
446
|
+
options: dict[str, Any] | None = None,
|
|
447
|
+
images: list[ImageInput] | None = None,
|
|
448
|
+
) -> str:
|
|
449
|
+
"""Prompt-based tool calling for drivers without native tool use."""
|
|
450
|
+
from .simulated_tools import build_tool_prompt, format_tool_result, parse_simulated_response
|
|
451
|
+
|
|
452
|
+
merged = {**self._options, **(options or {})}
|
|
453
|
+
tool_prompt = build_tool_prompt(self._tools)
|
|
454
|
+
|
|
455
|
+
# Augment system prompt with tool descriptions
|
|
456
|
+
augmented_system = tool_prompt
|
|
457
|
+
if self._system_prompt:
|
|
458
|
+
augmented_system = f"{self._system_prompt}\n\n{tool_prompt}"
|
|
459
|
+
|
|
460
|
+
# Record user message in history
|
|
461
|
+
user_content = self._build_content_with_images(content, images)
|
|
462
|
+
self._messages.append({"role": "user", "content": user_content})
|
|
463
|
+
|
|
464
|
+
for _round in range(self._max_tool_rounds):
|
|
465
|
+
# Build messages with the augmented system prompt
|
|
466
|
+
msgs: list[dict[str, Any]] = []
|
|
467
|
+
msgs.append({"role": "system", "content": augmented_system})
|
|
468
|
+
msgs.extend(self._messages)
|
|
469
|
+
|
|
470
|
+
resp = self._driver.generate_messages_with_hooks(msgs, merged)
|
|
471
|
+
text = resp.get("text", "")
|
|
472
|
+
meta = resp.get("meta", {})
|
|
473
|
+
self._accumulate_usage(meta)
|
|
474
|
+
|
|
475
|
+
parsed = parse_simulated_response(text, self._tools)
|
|
476
|
+
|
|
477
|
+
if parsed["type"] == "final_answer":
|
|
478
|
+
answer = parsed["content"]
|
|
479
|
+
self._messages.append({"role": "assistant", "content": answer})
|
|
480
|
+
return answer
|
|
481
|
+
|
|
482
|
+
# Tool call
|
|
483
|
+
tool_name = parsed["name"]
|
|
484
|
+
tool_args = parsed["arguments"]
|
|
485
|
+
|
|
486
|
+
# Record assistant's tool call as an assistant message
|
|
487
|
+
self._messages.append({"role": "assistant", "content": text})
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
result = self._tools.execute(tool_name, tool_args)
|
|
491
|
+
result_msg = format_tool_result(tool_name, result)
|
|
492
|
+
except Exception as exc:
|
|
493
|
+
result_msg = format_tool_result(tool_name, f"Error: {exc}")
|
|
494
|
+
|
|
495
|
+
# Record tool result as a user message (all drivers understand user/assistant)
|
|
496
|
+
self._messages.append({"role": "user", "content": result_msg})
|
|
497
|
+
|
|
498
|
+
raise RuntimeError(f"Simulated tool execution loop exceeded {self._max_tool_rounds} rounds")
|
|
499
|
+
|
|
419
500
|
def _build_messages_raw(self) -> list[dict[str, Any]]:
|
|
420
501
|
"""Build messages array from system prompt + full history (including tool messages)."""
|
|
421
502
|
msgs: list[dict[str, Any]] = []
|
|
@@ -484,6 +565,8 @@ class Conversation:
|
|
|
484
565
|
context clean for subsequent turns.
|
|
485
566
|
"""
|
|
486
567
|
|
|
568
|
+
self._last_reasoning = None
|
|
569
|
+
|
|
487
570
|
merged = {**self._options, **(options or {})}
|
|
488
571
|
|
|
489
572
|
# Build the full prompt with schema instructions inline (handled by ask_for_json)
|
|
@@ -525,6 +608,7 @@ class Conversation:
|
|
|
525
608
|
|
|
526
609
|
text = resp.get("text", "")
|
|
527
610
|
meta = resp.get("meta", {})
|
|
611
|
+
self._last_reasoning = resp.get("reasoning_content")
|
|
528
612
|
|
|
529
613
|
# Store original content (without schema boilerplate) for cleaner context
|
|
530
614
|
# Include images in history so subsequent turns can reference them
|
|
@@ -563,6 +647,7 @@ class Conversation:
|
|
|
563
647
|
"json_object": json_obj,
|
|
564
648
|
"usage": usage,
|
|
565
649
|
"output_format": output_format,
|
|
650
|
+
"reasoning": self._last_reasoning,
|
|
566
651
|
}
|
|
567
652
|
|
|
568
653
|
if output_format == "toon":
|
|
@@ -95,8 +95,17 @@ class AsyncGrokDriver(CostMixin, AsyncDriver):
|
|
|
95
95
|
"model_name": model,
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
message = resp["choices"][0]["message"]
|
|
99
|
+
text = message.get("content") or ""
|
|
100
|
+
reasoning_content = message.get("reasoning_content")
|
|
101
|
+
|
|
102
|
+
if not text and reasoning_content:
|
|
103
|
+
text = reasoning_content
|
|
104
|
+
|
|
105
|
+
result: dict[str, Any] = {"text": text, "meta": meta}
|
|
106
|
+
if reasoning_content is not None:
|
|
107
|
+
result["reasoning_content"] = reasoning_content
|
|
108
|
+
return result
|
|
100
109
|
|
|
101
110
|
# ------------------------------------------------------------------
|
|
102
111
|
# Tool use
|
|
@@ -173,15 +182,20 @@ class AsyncGrokDriver(CostMixin, AsyncDriver):
|
|
|
173
182
|
args = json.loads(tc["function"]["arguments"])
|
|
174
183
|
except (json.JSONDecodeError, TypeError):
|
|
175
184
|
args = {}
|
|
176
|
-
tool_calls_out.append(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
tool_calls_out.append(
|
|
186
|
+
{
|
|
187
|
+
"id": tc["id"],
|
|
188
|
+
"name": tc["function"]["name"],
|
|
189
|
+
"arguments": args,
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
result: dict[str, Any] = {
|
|
183
194
|
"text": text,
|
|
184
195
|
"meta": meta,
|
|
185
196
|
"tool_calls": tool_calls_out,
|
|
186
197
|
"stop_reason": stop_reason,
|
|
187
198
|
}
|
|
199
|
+
if choice["message"].get("reasoning_content") is not None:
|
|
200
|
+
result["reasoning_content"] = choice["message"]["reasoning_content"]
|
|
201
|
+
return result
|
|
@@ -88,8 +88,16 @@ class AsyncGroqDriver(CostMixin, AsyncDriver):
|
|
|
88
88
|
"model_name": model,
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
text = resp.choices[0].message.content
|
|
92
|
-
|
|
91
|
+
text = resp.choices[0].message.content or ""
|
|
92
|
+
reasoning_content = getattr(resp.choices[0].message, "reasoning_content", None)
|
|
93
|
+
|
|
94
|
+
if not text and reasoning_content:
|
|
95
|
+
text = reasoning_content
|
|
96
|
+
|
|
97
|
+
result: dict[str, Any] = {"text": text, "meta": meta}
|
|
98
|
+
if reasoning_content is not None:
|
|
99
|
+
result["reasoning_content"] = reasoning_content
|
|
100
|
+
return result
|
|
93
101
|
|
|
94
102
|
# ------------------------------------------------------------------
|
|
95
103
|
# Tool use
|
|
@@ -152,15 +160,21 @@ class AsyncGroqDriver(CostMixin, AsyncDriver):
|
|
|
152
160
|
args = json.loads(tc.function.arguments)
|
|
153
161
|
except (json.JSONDecodeError, TypeError):
|
|
154
162
|
args = {}
|
|
155
|
-
tool_calls_out.append(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
tool_calls_out.append(
|
|
164
|
+
{
|
|
165
|
+
"id": tc.id,
|
|
166
|
+
"name": tc.function.name,
|
|
167
|
+
"arguments": args,
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
result: dict[str, Any] = {
|
|
162
172
|
"text": text,
|
|
163
173
|
"meta": meta,
|
|
164
174
|
"tool_calls": tool_calls_out,
|
|
165
175
|
"stop_reason": stop_reason,
|
|
166
176
|
}
|
|
177
|
+
reasoning_content = getattr(choice.message, "reasoning_content", None)
|
|
178
|
+
if reasoning_content is not None:
|
|
179
|
+
result["reasoning_content"] = reasoning_content
|
|
180
|
+
return result
|
|
@@ -98,7 +98,12 @@ class AsyncLMStudioDriver(AsyncDriver):
|
|
|
98
98
|
if "choices" not in response_data or not response_data["choices"]:
|
|
99
99
|
raise ValueError(f"Unexpected response format: {response_data}")
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
message = response_data["choices"][0]["message"]
|
|
102
|
+
text = message.get("content") or ""
|
|
103
|
+
reasoning_content = message.get("reasoning_content")
|
|
104
|
+
|
|
105
|
+
if not text and reasoning_content:
|
|
106
|
+
text = reasoning_content
|
|
102
107
|
|
|
103
108
|
usage = response_data.get("usage", {})
|
|
104
109
|
prompt_tokens = usage.get("prompt_tokens", 0)
|
|
@@ -114,7 +119,10 @@ class AsyncLMStudioDriver(AsyncDriver):
|
|
|
114
119
|
"model_name": merged_options.get("model", self.model),
|
|
115
120
|
}
|
|
116
121
|
|
|
117
|
-
|
|
122
|
+
result: dict[str, Any] = {"text": text, "meta": meta}
|
|
123
|
+
if reasoning_content is not None:
|
|
124
|
+
result["reasoning_content"] = reasoning_content
|
|
125
|
+
return result
|
|
118
126
|
|
|
119
127
|
# -- Model management (LM Studio 0.4.0+) ----------------------------------
|
|
120
128
|
|