prompture 0.0.36.dev1__tar.gz → 0.0.37.dev1__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.
Files changed (134) hide show
  1. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.env.copy +1 -0
  2. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.github/scripts/update_docs_version.py +41 -2
  3. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.github/workflows/dev.yml +7 -0
  4. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.github/workflows/documentation.yml +7 -4
  5. {prompture-0.0.36.dev1/prompture.egg-info → prompture-0.0.37.dev1}/PKG-INFO +1 -1
  6. prompture-0.0.37.dev1/ROADMAP.md +341 -0
  7. prompture-0.0.37.dev1/VERSION +1 -0
  8. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/conf.py +14 -2
  9. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/__init__.py +120 -2
  10. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/_version.py +2 -2
  11. prompture-0.0.37.dev1/prompture/agent.py +925 -0
  12. prompture-0.0.37.dev1/prompture/agent_types.py +156 -0
  13. prompture-0.0.37.dev1/prompture/async_agent.py +879 -0
  14. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/async_conversation.py +199 -17
  15. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/async_driver.py +24 -0
  16. prompture-0.0.37.dev1/prompture/async_groups.py +551 -0
  17. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/conversation.py +213 -18
  18. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/core.py +30 -12
  19. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/discovery.py +24 -1
  20. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/driver.py +38 -0
  21. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/__init__.py +5 -1
  22. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/async_azure_driver.py +7 -1
  23. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/async_claude_driver.py +7 -1
  24. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/async_google_driver.py +24 -4
  25. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/async_grok_driver.py +7 -1
  26. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/async_groq_driver.py +7 -1
  27. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/async_lmstudio_driver.py +59 -3
  28. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/async_ollama_driver.py +7 -0
  29. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/async_openai_driver.py +7 -1
  30. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/async_openrouter_driver.py +7 -1
  31. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/async_registry.py +5 -1
  32. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/azure_driver.py +7 -1
  33. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/claude_driver.py +7 -1
  34. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/google_driver.py +24 -4
  35. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/grok_driver.py +7 -1
  36. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/groq_driver.py +7 -1
  37. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/lmstudio_driver.py +58 -6
  38. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/ollama_driver.py +7 -0
  39. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/openai_driver.py +7 -1
  40. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/openrouter_driver.py +7 -1
  41. prompture-0.0.37.dev1/prompture/drivers/vision_helpers.py +153 -0
  42. prompture-0.0.37.dev1/prompture/group_types.py +147 -0
  43. prompture-0.0.37.dev1/prompture/groups.py +530 -0
  44. prompture-0.0.37.dev1/prompture/image.py +180 -0
  45. prompture-0.0.37.dev1/prompture/persistence.py +254 -0
  46. prompture-0.0.37.dev1/prompture/persona.py +482 -0
  47. prompture-0.0.37.dev1/prompture/serialization.py +218 -0
  48. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/settings.py +1 -0
  49. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1/prompture.egg-info}/PKG-INFO +1 -1
  50. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture.egg-info/SOURCES.txt +11 -3
  51. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/pyproject.toml +1 -1
  52. prompture-0.0.36.dev1/.mcp.json +0 -19
  53. prompture-0.0.36.dev1/0.23.0 +0 -8
  54. prompture-0.0.36.dev1/8 +0 -0
  55. prompture-0.0.36.dev1/ROADMAP.md +0 -295
  56. prompture-0.0.36.dev1/VERSION +0 -1
  57. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.claude/skills/add-driver/SKILL.md +0 -0
  58. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.claude/skills/add-driver/references/driver-template.md +0 -0
  59. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.claude/skills/add-example/SKILL.md +0 -0
  60. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.claude/skills/add-field/SKILL.md +0 -0
  61. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.claude/skills/add-test/SKILL.md +0 -0
  62. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.claude/skills/run-tests/SKILL.md +0 -0
  63. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.claude/skills/scaffold-extraction/SKILL.md +0 -0
  64. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.claude/skills/update-pricing/SKILL.md +0 -0
  65. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.github/FUNDING.yml +0 -0
  66. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.github/scripts/update_wrapper_version.py +0 -0
  67. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/.github/workflows/publish.yml +0 -0
  68. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/CLAUDE.md +0 -0
  69. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/LICENSE +0 -0
  70. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/MANIFEST.in +0 -0
  71. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/README.md +0 -0
  72. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/_static/custom.css +0 -0
  73. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/api/core.rst +0 -0
  74. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/api/drivers.rst +0 -0
  75. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/api/field_definitions.rst +0 -0
  76. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/api/index.rst +0 -0
  77. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/api/runner.rst +0 -0
  78. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/api/tools.rst +0 -0
  79. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/api/validator.rst +0 -0
  80. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/contributing.rst +0 -0
  81. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/examples.rst +0 -0
  82. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/field_definitions_reference.rst +0 -0
  83. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/index.rst +0 -0
  84. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/installation.rst +0 -0
  85. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/quickstart.rst +0 -0
  86. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/docs/source/toon_input_guide.rst +0 -0
  87. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/packages/README.md +0 -0
  88. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/packages/llm_to_json/README.md +0 -0
  89. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/packages/llm_to_json/llm_to_json/__init__.py +0 -0
  90. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/packages/llm_to_json/pyproject.toml +0 -0
  91. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/packages/llm_to_json/test.py +0 -0
  92. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/packages/llm_to_toon/README.md +0 -0
  93. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/packages/llm_to_toon/llm_to_toon/__init__.py +0 -0
  94. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/packages/llm_to_toon/pyproject.toml +0 -0
  95. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/packages/llm_to_toon/test.py +0 -0
  96. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/aio/__init__.py +0 -0
  97. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/async_core.py +0 -0
  98. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/cache.py +0 -0
  99. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/callbacks.py +0 -0
  100. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/cli.py +0 -0
  101. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/cost_mixin.py +0 -0
  102. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/airllm_driver.py +0 -0
  103. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/async_airllm_driver.py +0 -0
  104. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/async_hugging_driver.py +0 -0
  105. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/async_local_http_driver.py +0 -0
  106. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/hugging_driver.py +0 -0
  107. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/local_http_driver.py +0 -0
  108. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/drivers/registry.py +0 -0
  109. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/field_definitions.py +0 -0
  110. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/logging.py +0 -0
  111. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/model_rates.py +0 -0
  112. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/runner.py +0 -0
  113. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/scaffold/__init__.py +0 -0
  114. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/scaffold/generator.py +0 -0
  115. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/scaffold/templates/Dockerfile.j2 +0 -0
  116. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/scaffold/templates/README.md.j2 +0 -0
  117. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/scaffold/templates/config.py.j2 +0 -0
  118. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/scaffold/templates/env.example.j2 +0 -0
  119. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/scaffold/templates/main.py.j2 +0 -0
  120. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/scaffold/templates/models.py.j2 +0 -0
  121. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/scaffold/templates/requirements.txt.j2 +0 -0
  122. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/server.py +0 -0
  123. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/session.py +0 -0
  124. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/tools.py +0 -0
  125. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/tools_schema.py +0 -0
  126. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture/validator.py +0 -0
  127. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture.egg-info/dependency_links.txt +0 -0
  128. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture.egg-info/entry_points.txt +0 -0
  129. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture.egg-info/requires.txt +0 -0
  130. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/prompture.egg-info/top_level.txt +0 -0
  131. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/requirements.txt +0 -0
  132. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/setup.cfg +0 -0
  133. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/test.py +0 -0
  134. {prompture-0.0.36.dev1 → prompture-0.0.37.dev1}/test_version_diagnosis.py +0 -0
@@ -23,6 +23,7 @@ OLLAMA_MODEL=gemma:latest
23
23
  # Required if AI_PROVIDER=lmstudio
24
24
  LMSTUDIO_ENDPOINT=http://127.0.0.1:1234/v1/chat/completions
25
25
  LMSTUDIO_MODEL=deepseek/deepseek-r1-0528-qwen3-8b
26
+ LMSTUDIO_API_KEY=
26
27
 
27
28
  # Azure OpenAI Configuration
28
29
  AZURE_API_KEY=
@@ -174,6 +174,41 @@ def update_index_rst(index_file, version):
174
174
  return False
175
175
 
176
176
 
177
+ def update_conf_py(conf_file, version):
178
+ """
179
+ Update the release version in docs/source/conf.py fallback.
180
+
181
+ Args:
182
+ conf_file: Path to the conf.py file
183
+ version: Version string to insert
184
+
185
+ Returns:
186
+ bool: True if file was updated, False otherwise
187
+ """
188
+ if not conf_file.exists():
189
+ print(f"✗ File not found: {conf_file}", file=sys.stderr)
190
+ return False
191
+
192
+ try:
193
+ content = conf_file.read_text(encoding="utf-8")
194
+ # Update the hardcoded fallback version in conf.py
195
+ new_content, count = re.subn(
196
+ r'(release\s*=\s*")[^"]+(")',
197
+ rf"\g<1>{version}\g<2>",
198
+ content,
199
+ )
200
+ if count > 0:
201
+ conf_file.write_text(new_content, encoding="utf-8")
202
+ print(f"✓ Updated fallback release version in {conf_file}")
203
+ return True
204
+ else:
205
+ print(f"✗ release pattern not found in {conf_file}", file=sys.stderr)
206
+ return False
207
+ except Exception as e:
208
+ print(f"✗ Error updating conf.py: {e}", file=sys.stderr)
209
+ return False
210
+
211
+
177
212
  def main():
178
213
  """Main entry point for the script."""
179
214
  # Determine project root (two levels up from this script)
@@ -192,9 +227,13 @@ def main():
192
227
 
193
228
  # Update index.rst
194
229
  index_file = project_root / "docs" / "source" / "index.rst"
195
- success = update_index_rst(index_file, version)
230
+ rst_ok = update_index_rst(index_file, version)
231
+
232
+ # Update conf.py fallback version
233
+ conf_file = project_root / "docs" / "source" / "conf.py"
234
+ conf_ok = update_conf_py(conf_file, version)
196
235
 
197
- if success:
236
+ if rst_ok or conf_ok:
198
237
  print("\n✓ Documentation version updated successfully")
199
238
  sys.exit(0)
200
239
  else:
@@ -62,6 +62,13 @@ jobs:
62
62
  git config --global user.name "github-actions[bot]"
63
63
  git config --global user.email "github-actions[bot]@users.noreply.github.com"
64
64
 
65
+ - name: Update VERSION file
66
+ run: |
67
+ echo "${{ steps.ver.outputs.new }}" > VERSION
68
+ git add VERSION
69
+ git commit -m "chore: bump dev version to ${{ steps.ver.outputs.new }} [skip ci]" || true
70
+ git push origin dev || true
71
+
65
72
  - name: Create and push dev tag
66
73
  run: |
67
74
  git tag -a "${{ steps.ver.outputs.tag }}" -m "pre: ${{ steps.ver.outputs.new }}"
@@ -1,9 +1,9 @@
1
1
  name: documentation
2
2
 
3
3
  on:
4
- push:
5
- branches:
6
- - main
4
+ workflow_run:
5
+ workflows: ["Publish to PyPI and GitHub Release"]
6
+ types: [completed]
7
7
 
8
8
  permissions:
9
9
  contents: write
@@ -11,8 +11,12 @@ permissions:
11
11
  jobs:
12
12
  docs:
13
13
  runs-on: ubuntu-latest
14
+ # Only run if the publish workflow succeeded
15
+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
14
16
  steps:
15
17
  - uses: actions/checkout@v4
18
+ with:
19
+ ref: main # Ensure we get the latest main (with updated VERSION)
16
20
  - uses: actions/setup-python@v5
17
21
  - name: Install dependencies
18
22
  run: |
@@ -25,7 +29,6 @@ jobs:
25
29
  sphinx-build docs/source _build
26
30
  - name: Deploy to GitHub Pages
27
31
  uses: peaceiris/actions-gh-pages@v3
28
- if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
29
32
  with:
30
33
  publish_branch: gh-pages
31
34
  github_token: ${{ secrets.GITHUB_TOKEN }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prompture
3
- Version: 0.0.36.dev1
3
+ Version: 0.0.37.dev1
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
@@ -0,0 +1,341 @@
1
+ # Prompture Roadmap
2
+
3
+ ## Completed Work
4
+
5
+ ### v0.0.1–v0.0.21: Core Extraction Engine
6
+ - Initial project structure, `ask_for_json()`, JSON schema enforcement
7
+ - OpenAI, Claude, Azure, Ollama drivers
8
+ - JSON cleaning and AI-powered cleanup fallback
9
+ - `extract_and_jsonify()`, `manual_extract_and_jsonify()`
10
+ - Driver flexibility: `get_driver()` interface
11
+ - Example scripts for each provider
12
+
13
+ ### v0.0.22–v0.0.24: Pydantic & Stepwise Extraction
14
+ - `extract_with_model()`: one-shot Pydantic model extraction
15
+ - `stepwise_extract_with_model()`: per-field extraction with smart type coercion
16
+ - `tools.py` utilities: parsing, schema generation, shorthand numbers, multilingual booleans
17
+ - Structured logging and verbose control
18
+
19
+ ### v0.0.25–v0.0.28: Multi-Provider & Field System
20
+ - LM Studio, Google Gemini, Groq, OpenRouter, Grok drivers (12 total)
21
+ - Sphinx documentation site
22
+ - Field definitions registry with 50+ predefined fields
23
+ - Enum field support and validation utilities
24
+ - Template variables (`{{current_year}}`, `{{current_date}}`, etc.)
25
+ - Text classification and analysis examples
26
+
27
+ ### v0.0.29–v0.0.32: TOON, Discovery & AirLLM
28
+ - TOON output format support (compact token-oriented notation)
29
+ - TOON input conversion via `extract_from_data()` / `extract_from_pandas()` (45-60% token savings)
30
+ - `get_available_models()` auto-discovery across configured providers
31
+ - `render_output()` for raw text/HTML/markdown generation
32
+ - AirLLM driver for local inference
33
+ - Live model rates with caching and `get_model_rates()` API
34
+
35
+ ### v0.0.33–v0.0.34: Async, Caching & Conversations
36
+ - `AsyncDriver` base class and async driver implementations
37
+ - `AsyncConversation` for non-blocking multi-turn interactions
38
+ - Response caching with memory, SQLite, and Redis backends
39
+ - `Conversation` class: stateful multi-turn sessions with system prompts and message history
40
+ - Message-based driver APIs: `generate_messages()`, `generate_messages_stream()`
41
+ - Native JSON mode detection per provider (OpenAI `json_schema`, Claude tool-use, Gemini `response_mime_type`)
42
+ - `DriverCallbacks` with `on_request`, `on_response`, `on_error`, `on_stream_delta` hooks
43
+ - `UsageSession` for accumulated token/cost tracking across calls
44
+ - `configure_logging()` with `JSONFormatter` for structured log output
45
+
46
+ ### v0.0.35: Tool Use, Streaming & Plugin System
47
+ - `ToolRegistry` and `ToolDefinition`: register Python functions as LLM-callable tools
48
+ - `tool_from_function()`: auto-generate JSON schemas from type hints
49
+ - Tool use in conversations with multi-round execution (`max_tool_rounds`)
50
+ - Streaming via `ask_stream()` and `generate_messages_stream()`
51
+ - Pluggable driver registry with entry-point discovery
52
+ - `register_driver()` / `register_async_driver()` for third-party provider plugins
53
+
54
+ ### v0.0.36 (current): Vision Support
55
+ - `ImageContent` frozen dataclass and `make_image()` smart constructor for bytes, base64, file path, URL inputs
56
+ - `image_from_bytes()`, `image_from_base64()`, `image_from_file()`, `image_from_url()` constructors
57
+ - `conv.ask("describe", images=[screenshot_bytes])` API on `Conversation` and `AsyncConversation`
58
+ - Image support in `ask_for_json()`, `extract_with_model()`, `ask_stream()`, `add_context()`
59
+ - Image support in standalone core functions: `render_output()`, `ask_for_json()`, `extract_and_jsonify()`, `extract_with_model()`
60
+ - Driver-level `_prepare_messages()` with provider-specific wire formats (OpenAI, Claude, Gemini, Ollama)
61
+ - Shared `vision_helpers.py` module for OpenAI-compatible drivers (Groq, Grok, Azure, LM Studio, OpenRouter)
62
+ - `supports_vision` capability flag on all drivers (sync and async)
63
+ - Universal internal format: `{"type": "image", "source": ImageContent(...)}` content blocks
64
+ - Backward compatible: string-only messages unchanged
65
+
66
+ ### v0.0.37: Conversation Persistence
67
+ - `conv.export() -> dict` serialization (messages, system prompt, tool definitions, usage)
68
+ - `Conversation.from_export(data)` restoration with driver reconstruction
69
+ - File-based persistence: `conv.save("path.json")` / `Conversation.load("path.json")`
70
+ - SQLite `ConversationStore` backend with tag search, listing, and CRUD
71
+ - Optional auto-save on every turn via `auto_save` parameter
72
+ - Conversation metadata: `conversation_id`, `tags`, `created_at`, `last_active`, `turn_count`
73
+ - Export/import of `UsageSession` alongside conversation state
74
+ - `ImageContent` serialization/deserialization with `strip_images` option
75
+ - Versioned export format (`EXPORT_VERSION = 1`) with validation on import
76
+ - `serialization.py` (pure data transforms) and `persistence.py` (storage backends) modules
77
+ - Full async support: mirrored on `AsyncConversation`
78
+
79
+ ---
80
+
81
+ ## Upcoming
82
+
83
+ ### Phase 3: Agent Framework
84
+ **Goal**: Higher-level agent abstraction with a ReAct loop, typed context, structured output, and two-tier execution API (simple `run()` + step-by-step `iter()`).
85
+
86
+ #### Phase 3a: Core Agent Class ✅
87
+ - [x] `Agent` class composing `Conversation` + `ToolRegistry` + system prompt + output type
88
+ - [x] Constructor: `Agent(model, *, driver, tools, system_prompt, output_type, max_iterations, options)`
89
+ - [x] Tool registration via constructor injection (list or `ToolRegistry`) and `@agent.tool` decorator
90
+ - [x] `output_type: type[BaseModel]` for structured agent output with JSON parse + `model_validate()` retry (up to 3 attempts)
91
+ - [x] `agent.run(prompt) -> AgentResult` — high-level, hides the ReAct loop entirely
92
+ - [x] `AgentResult` containing: `output` (typed or str), `output_text`, `messages`, `usage`, `steps: list[AgentStep]`, `all_tool_calls`, `state`
93
+ - [x] `AgentStep` dataclass with `step_type` (think/tool_call/tool_result/output), `timestamp`, `content`, `tool_name`, `tool_args`, `tool_result`, `duration_ms`
94
+ - [x] `AgentState` enum: `idle`, `running`, `stopped`, `errored`
95
+ - [x] `agent.stop()` graceful shutdown flag
96
+ - [x] Fresh `Conversation` per `run()` — no state leakage between runs
97
+ - [x] Internal ReAct loop delegates to `Conversation._ask_with_tools` via `max_tool_rounds`
98
+ - [x] System prompt augmented with JSON schema instructions when `output_type` is set
99
+ - [x] `ModelRetry` exception defined (used in Phase 3b guardrails)
100
+ - [x] Shared types module: `agent_types.py` (AgentState, StepType, AgentStep, AgentResult, ModelRetry)
101
+ - [x] 24 unit tests covering construction, run (no tools / with tools / with output_type), system prompt, stop, state, options
102
+ - [x] Example script: `examples/agent_example.py`
103
+
104
+ #### Phase 3b: Context, Guardrails & Callbacks ✅
105
+ - [x] `RunContext[DepsType]` dataclass passed to tools and system prompt functions: carries deps, model info, usage, message history, iteration count
106
+ - [x] `deps_type` generic on `Agent` for type-safe dependency access in tools
107
+ - [x] Dynamic system prompts: `system_prompt: str | Callable[[RunContext], str]` for context-aware persona rendering
108
+ - [x] `AgentCallbacks` extending `DriverCallbacks` with: `on_step`, `on_tool_start(name, args)`, `on_tool_end(name, result)`, `on_iteration(step_number)`, `on_output(result)`
109
+ - [x] Input validators: `input_guardrails: list[Callable[[RunContext, str], str | None]]` — transform or reject input before loop starts
110
+ - [x] Output validators: `output_guardrails: list[Callable[[RunContext, AgentResult], AgentResult | None]]` — validate final output, raise `ModelRetry` to feed error back to LLM
111
+ - [x] `ModelRetry` exception integration: raised from tools or validators to send error message back to the model with retry budget
112
+ - [x] Per-run `UsageSession` tracking (tokens, cost, errors across all iterations)
113
+ - [x] Iteration limits: `max_cost` (USD budget via `UsageSession`)
114
+
115
+ #### Phase 3c: Streaming, Iteration & Async ✅
116
+ - [x] `agent.iter(prompt) -> AgentIterator` — low-level step-by-step control, yields `AgentStep` per iteration
117
+ - [x] `agent.run_stream(prompt) -> StreamedAgentResult` — streaming with deltas for each step
118
+ - [x] `AsyncAgent` mirroring `Agent` with `async run()`, `async iter()`, `async run_stream()`
119
+ - [x] Async tool support: tools can be sync or async callables (auto-detected)
120
+ - [x] `StreamEvent` and `StreamEventType` types for streaming event classification
121
+ - [x] `AsyncAgentIterator` and `AsyncStreamedAgentResult` wrapper classes with result capture
122
+ - [x] All new types exported from `prompture.__init__`
123
+ - [x] Example script: `examples/async_agent_example.py`
124
+
125
+ ### Phase 4: Persona Templates ✅
126
+ **Goal**: Reusable, composable system prompt definitions with template variables, layered composition, and a thread-safe registry following the `field_definitions.py` pattern.
127
+
128
+ #### Persona Data Model ✅
129
+ - [x] `Persona` frozen dataclass with structured fields: `name`, `system_prompt` (template text), `description` (metadata for registries/docs), `traits: tuple[str, ...]` (behavioral tags), `variables: dict[str, Any]` (default template values), `constraints: list[str]` (rules and guardrails), `model_hint: str | None` (suggested model), `settings: dict[str, Any]` (temperature, max_tokens, etc.)
130
+ - [x] Layered prompt structure within `system_prompt`: role/identity section, behavioral rules, output format constraints, and domain knowledge — constraints rendered as `## Constraints` section
131
+
132
+ #### Template Rendering ✅
133
+ - [x] Reuse `field_definitions.py` `_apply_templates()` and `_get_template_variables()` for persona prompts
134
+ - [x] Built-in runtime variables: `{{current_date}}`, `{{current_year}}`, `{{current_datetime}}`, `{{current_weekday}}` (same as field definitions)
135
+ - [x] Custom per-render variables: `persona.render(user_name="Alice", company="Acme")`
136
+ - [x] `Persona.render(**kwargs) -> str` produces final system prompt with all variables resolved
137
+ - [x] Variable precedence: built-in < `self.variables` < kwargs
138
+
139
+ #### Composition & Extension ✅
140
+ - [x] `persona.extend(additional_instructions) -> Persona` — returns new persona with appended instructions (immutable via `dataclasses.replace`)
141
+ - [x] Trait composition: `Persona(traits=("concise", "technical"))` resolves traits from registry during `render()`
142
+ - [x] Trait registry: `register_trait("concise", "Keep responses under 3 sentences.")`, `get_trait()`, `get_trait_names()`, `reset_trait_registry()` with `threading.Lock`
143
+ - [x] Constraint injection: `persona.with_constraints(["Never discuss competitors"])` — appends rules without modifying the base prompt
144
+ - [x] `persona + other_persona` merge operator: concatenates prompts, dedupes traits, merges variables (right wins + warning on conflict), merges constraints and settings
145
+
146
+ #### Thread-Safe Global Registry ✅
147
+ - [x] `register_persona(persona)` / `get_persona(name)` with `threading.Lock` (mirrors `field_definitions.py` pattern)
148
+ - [x] `get_persona_names()`, `get_persona_registry_snapshot()`, `clear_persona_registry()`, `reset_persona_registry()`
149
+ - [x] `_PersonaRegistryProxy` for dict-like access: `PERSONAS["analyst"]`
150
+ - [x] Auto-initialization with built-in personas on import
151
+
152
+ #### Built-in Personas ✅
153
+ - [x] `json_extractor` — precise structured data extraction, `temperature: 0.0`, JSON-only output constraints
154
+ - [x] `data_analyst` — quantitative analysis, cites sources, confidence level constraints
155
+ - [x] `text_summarizer` — concise summaries, configurable `{{max_sentences}}` variable (default 3)
156
+ - [x] `code_reviewer` — structured feedback format (Summary/Issues/Suggestions sections)
157
+ - [x] `concise_assistant` — brief responses, no unnecessary elaboration
158
+
159
+ #### Integration with Conversation & Agent ✅
160
+ - [x] `Conversation(persona="json_extractor")` shorthand — looks up registry, renders, sets as system prompt
161
+ - [x] `Conversation(persona=my_persona)` — accepts `Persona` instance directly
162
+ - [x] `ValueError` when both `persona` and `system_prompt` provided
163
+ - [x] `persona.settings` applied as default options (explicit options override)
164
+ - [x] `persona.model_hint` used if `model_name` not provided
165
+ - [x] Dynamic persona support for agents: `system_prompt: str | Persona | Callable[[RunContext], str]` — Persona objects auto-render with RunContext variables
166
+ - [x] `description` field available for multi-agent routing
167
+ - [x] Full async support: mirrored on `AsyncConversation` and `AsyncAgent`
168
+ - [x] All new symbols exported from `prompture.__init__`
169
+
170
+ #### Serialization & Persistence ✅
171
+ - [x] `persona.to_dict() -> dict` / `Persona.from_dict(data) -> Persona` with `version: 1`
172
+ - [x] JSON file support: `persona.save_json()` / `Persona.load_json()`
173
+ - [x] YAML file support: `persona.save_yaml()` / `Persona.load_yaml()` (optional `pyyaml` dependency)
174
+ - [x] `load_personas_from_directory("personas/")` — bulk-load `.json`/`.yaml`/`.yml` files into the registry
175
+ - [x] 72 unit tests covering all sub-phases
176
+
177
+ ### Phase 5: Multi-Agent Coordination ✅
178
+ **Goal**: Enable multiple agents to collaborate via deterministic workflow groups (sequential, parallel, router) and agent-as-tool composition, with explicit scoped state sharing and aggregate usage tracking.
179
+
180
+ #### Agent-as-Tool (Foundation Pattern) ✅
181
+ - [x] `agent.as_tool(name, description) -> ToolDefinition` — wraps any `Agent` as a callable tool for another agent
182
+ - [x] Coordinator retains conversation control; sub-agent runs independently and returns result as tool output
183
+ - [x] Optional `custom_output_extractor: Callable[[AgentResult], str]` for transforming sub-agent results before returning to coordinator
184
+ - [x] Sub-agent inherits no conversation history from coordinator (maximum isolation)
185
+ - [x] Sub-agent `AgentResult` captured for tracing even when used as tool
186
+
187
+ #### Deterministic Workflow Groups ✅
188
+ - [x] `SequentialGroup(agents, *, state, error_policy, max_total_turns)` — agents execute in order, each receiving shared state with outputs from prior agents
189
+ - [x] `ParallelGroup(agents, *, state, error_policy, timeout_ms)` — independent agents run concurrently via `asyncio.gather`, results collected into shared state
190
+ - [x] `LoopGroup(agents, *, exit_condition, max_iterations)` — generator-critic cycle: agents execute in sequence repeatedly until `exit_condition(state) -> bool` returns True
191
+ - [x] All groups accept `state: dict[str, Any]` as initial shared context
192
+ - [x] Groups are composable: a `SequentialGroup` can contain a `ParallelGroup` as a step (nested workflows via `GroupAsAgent` adapter)
193
+
194
+ #### Shared State via Named Keys ✅
195
+ - [x] Each agent reads from shared `state: dict[str, Any]` (injected via template variable substitution in prompts)
196
+ - [x] Each agent writes output to a named key: `Agent(output_key="research_data")` — result stored in `state["research_data"]`
197
+ - [x] Template variable injection: agent system prompts can reference `{research_data}` to read other agents' outputs (Google ADK pattern)
198
+ - [x] Explicit data flow: traceable which agent produces and consumes which state keys
199
+ - [x] No shared conversation history — each agent gets only the state keys it needs (minimum necessary context)
200
+
201
+ #### LLM-Driven Router ✅
202
+ - [x] `RouterAgent(model, agents, routing_prompt)` — uses a (cheap) LLM to classify input and delegate to the appropriate specialist agent
203
+ - [x] Routing based on agent `description` fields from Persona metadata (Phase 4 integration)
204
+ - [x] Fallback agent when no specialist matches
205
+ - [x] Router runs a single LLM call for classification, not a full ReAct loop (minimal overhead)
206
+
207
+ #### Error Handling ✅
208
+ - [x] `ErrorPolicy` enum: `fail_fast` (abort group on first failure), `continue_on_error` (skip failed agent, proceed with partial results), `retry_failed` (retry N times with backoff)
209
+ - [x] Per-agent error state captured in `GroupResult.agent_results` and `GroupResult.errors` (list of `AgentError`)
210
+ - [x] Failed agent's error message available in shared state for downstream agents to handle
211
+ - [x] `max_total_turns` across entire group to prevent runaway costs from agents bouncing between each other
212
+
213
+ #### Group-Level Usage & Observability ✅
214
+ - [x] `GroupResult` dataclass: `agent_results: dict[str, AgentResult]`, `aggregate_usage: dict`, `shared_state: dict[str, Any]`, `elapsed_ms: float`, `timeline: list[GroupStep]`
215
+ - [x] `GroupStep` dataclass: `agent_name`, `step_type` (agent_run/agent_error), `timestamp`, `duration_ms`, `usage_delta`
216
+ - [x] Aggregate usage across all agents via `_aggregate_usage()` helper (prompt_tokens, completion_tokens, total_tokens, total_cost, call_count, errors)
217
+ - [x] `GroupCallbacks` with: `on_agent_start(name, prompt)`, `on_agent_complete(name, result)`, `on_agent_error(name, error)`, `on_state_update(key, value)`
218
+ - [x] Interleaved timeline view: all agent steps merged chronologically for debugging
219
+
220
+ #### Timeout & Cancellation ✅
221
+ - [x] Per-agent timeout in `ParallelGroup(timeout_ms=30000)` — enforced via `asyncio.wait_for`
222
+ - [x] Cooperative shutdown: `group.stop()` calls `agent.stop()` on all running agents
223
+ - [x] `max_total_cost` budget across the group (aggregate `UsageSession` enforced)
224
+
225
+ #### Async Support ✅
226
+ - [x] `AsyncSequentialGroup`, `AsyncLoopGroup` mirroring sync variants
227
+ - [x] `ParallelGroup` uses `asyncio.gather` internally (async-native); sync wrapper available via `group.run()` with event loop management
228
+ - [x] `AsyncRouterAgent` for non-blocking routing
229
+
230
+ #### Serialization & Persistence ✅
231
+ - [x] `GroupResult.export() -> dict` with per-agent results, shared state, aggregate usage, and timeline
232
+ - [x] `GroupResult.save("path.json")` for full group result persistence (reuses `serialization.py` patterns)
233
+
234
+ ### Phase 6: Cost Budgets & Guardrails
235
+ **Goal**: Prevent runaway costs with pre-flight estimation and enforcement, manage context windows with token-aware history truncation/summarization, rate-limit requests, and validate input/output content — building on the existing `UsageSession`, `DriverCallbacks`, and `CostMixin` infrastructure.
236
+
237
+ #### Pre-Flight Cost Estimation
238
+ - [ ] `estimate_tokens(text) -> int` using tiktoken (OpenAI models) with fallback to character-based heuristic (~4 chars/token) for other providers
239
+ - [ ] `estimate_cost(prompt, model, options) -> float` — pre-call cost estimate using `get_model_rates()` pricing data
240
+ - [ ] Token count available in `on_request` callback payload: `{"estimated_tokens": int, "estimated_cost": float}` for pre-call decision making
241
+ - [ ] Optional tiktoken dependency: graceful fallback to heuristic when not installed
242
+
243
+ #### Budget Limits & Enforcement
244
+ - [ ] `Conversation(max_cost=0.50, max_tokens=10000)` — per-conversation budget caps
245
+ - [ ] `Agent(max_cost=1.00, max_tokens=50000)` — per-agent-run budget caps (checked between iterations via `UsageSession`)
246
+ - [ ] `BudgetPolicy` enum: `hard_stop` (raise `BudgetExceeded` before the call that would exceed), `warn_and_continue` (fire `on_budget_warning` callback, proceed), `degrade` (switch to cheaper fallback model)
247
+ - [ ] `BudgetExceeded` exception with `usage_at_limit: dict` containing tokens/cost consumed when the limit was hit
248
+ - [ ] Pre-call budget check: compare `estimate_cost()` against remaining budget before each LLM call — reject if estimated cost would exceed remaining budget
249
+ - [ ] Post-call budget check: after each response, update `UsageSession` and check against limits for the *next* call
250
+ - [ ] `on_budget_warning(usage, limit, remaining)` callback fired when usage exceeds configurable threshold (default 80%)
251
+
252
+ #### Model Fallback Chains
253
+ - [ ] `Conversation(model="openai/gpt-4o", fallback_models=["openai/gpt-4o-mini", "groq/llama-3.1-8b"])` — ordered list of progressively cheaper models
254
+ - [ ] Fallback triggers: `BudgetPolicy.degrade` switches to next model in chain when budget threshold reached
255
+ - [ ] Fallback on error: retry with next model on provider errors (rate limit, timeout, 5xx) — configurable via `fallback_on_errors: bool`
256
+ - [ ] `on_model_fallback(from_model, to_model, reason)` callback for observability
257
+ - [ ] Fallback state tracked in `UsageSession`: which models were used and why
258
+
259
+ #### Per-Session & Per-Conversation Tracking
260
+ - [ ] Automatic `UsageSession` on every `Conversation` and `Agent` (no manual callback wiring required)
261
+ - [ ] `conversation.usage_session` property exposing the session with per-model bucketing
262
+ - [ ] `conversation.remaining_budget -> dict` with `{"cost": float, "tokens": int}` remaining before limits
263
+ - [ ] Cross-conversation session: `UsageSession` can be shared across multiple conversations via constructor injection for global budget enforcement
264
+
265
+ #### Rate Limiting
266
+ - [ ] `RateLimiter` class with token bucket algorithm: `RateLimiter(requests_per_minute=60, tokens_per_minute=100000)`
267
+ - [ ] Per-conversation rate limiting: `Conversation(rate_limiter=my_limiter)`
268
+ - [ ] Per-model rate limiting: `RateLimiter` scoped to a specific `"provider/model"` string
269
+ - [ ] Backpressure behavior: `block` (sleep until bucket refills), `reject` (raise `RateLimitExceeded` immediately)
270
+ - [ ] Rate limiter state exposed: `limiter.available_requests`, `limiter.available_tokens`, `limiter.next_available_at`
271
+
272
+ #### Context Window Management
273
+ - [ ] `ContextWindowManager` for token-aware message history management
274
+ - [ ] Token budget allocation: configurable split between system prompt, conversation history, and response — `ContextWindowManager(system_reserve=500, response_reserve=1000, max_context=128000)`
275
+ - [ ] Context window sizes loaded from `get_model_info()` per-model metadata (falls back to configurable default)
276
+ - [ ] Overflow strategy enum: `truncate_oldest` (drop oldest messages first), `summarize` (LLM-compress old messages), `sliding_window` (keep last N messages)
277
+ - [ ] `truncate_oldest`: removes oldest non-system messages until history fits within budget, preserving system prompt and most recent messages
278
+ - [ ] `sliding_window`: keeps last N turns (configurable `window_size`), drops everything before
279
+ - [ ] `Conversation(context_manager=my_manager)` integration — automatically applied before each LLM call
280
+
281
+ #### Conversation Summarization
282
+ - [ ] `summarize` overflow strategy: when history exceeds token budget, compress older messages into a summary using a (cheap) LLM call
283
+ - [ ] Summary inserted as a system-level context message: `{"role": "system", "content": "Previous conversation summary: ..."}`
284
+ - [ ] Configurable summarization model: `ContextWindowManager(summarize_model="openai/gpt-4o-mini")` — use a cheap/fast model for summarization
285
+ - [ ] Hybrid approach: keep last N messages verbatim + summary of everything before (LangChain `ConversationSummaryBufferMemory` pattern)
286
+ - [ ] Summary token budget: summary itself has a max token allocation to prevent unbounded growth
287
+ - [ ] `on_summarize(original_tokens, summary_tokens, messages_removed)` callback for observability
288
+
289
+ #### Content Guardrails
290
+ - [ ] `InputGuardrail` protocol: `check(content: str) -> GuardrailResult` returning `passed`, `blocked` (with reason), or `modified` (with transformed content)
291
+ - [ ] `OutputGuardrail` protocol: `check(content: str, context: dict) -> GuardrailResult` — same return types, with access to conversation context
292
+ - [ ] Built-in input guardrails: `RegexBlocker(patterns: list[str])` for blocking patterns (PII, secrets, profanity), `RegexRequirer(patterns: list[str])` for requiring patterns in output
293
+ - [ ] Built-in output guardrails: `JsonSchemaValidator(schema)` for format compliance, `MaxLengthValidator(max_chars)` for response length
294
+ - [ ] `Conversation(input_guardrails=[...], output_guardrails=[...])` — applied automatically before/after each LLM call
295
+ - [ ] `GuardrailResult.blocked` raises `ContentBlocked(reason, content)` exception
296
+ - [ ] `GuardrailResult.modified` transparently transforms content and proceeds
297
+ - [ ] Guardrail chain: multiple guardrails execute in order; first `blocked` result stops the chain
298
+ - [ ] `on_guardrail_triggered(guardrail_name, result, direction)` callback for logging/observability
299
+
300
+ #### Integration with Agent & Multi-Agent (Phase 3 & 5)
301
+ - [ ] Agent inherits conversation-level budgets and guardrails
302
+ - [ ] `Agent(input_guardrails, output_guardrails)` — Phase 3b guardrails implemented using this Phase 6 infrastructure
303
+ - [ ] Group-level budgets from Phase 5 (`max_total_cost`) enforced via shared `UsageSession` with Phase 6 `BudgetPolicy`
304
+ - [ ] `ModelRetry` (from Phase 3b) integrates with output guardrails: guardrail returns `retry` result → feeds error back to LLM
305
+
306
+ #### Settings & Configuration
307
+ - [ ] `Settings` additions: `default_max_cost`, `default_max_tokens`, `default_rate_limit_rpm`, `default_context_overflow_strategy`
308
+ - [ ] Environment variable support: `PROMPTURE_MAX_COST=0.50`, `PROMPTURE_MAX_TOKENS=10000`, `PROMPTURE_RATE_LIMIT_RPM=60`
309
+ - [ ] All budget/guardrail settings overridable per-conversation or per-agent (constructor params take precedence over Settings defaults)
310
+
311
+ ### Phase 7: Async Tool Execution
312
+ **Goal**: Non-blocking tool execution for long-running operations.
313
+
314
+ - [ ] `@registry.async_tool` decorator for async tool functions
315
+ - [ ] Tool timeout configuration per tool
316
+ - [ ] Parallel tool execution when LLM requests multiple tools in one turn
317
+ - [ ] Tool status polling: tool returns "pending" and agent checks back
318
+ - [ ] Tool cancellation support
319
+ - [ ] Progress reporting from tools back to the conversation
320
+
321
+ ### Phase 8: Middleware & Interceptors
322
+ **Goal**: Pluggable pipeline between conversation and driver for cross-cutting concerns.
323
+
324
+ - [ ] `Middleware` protocol: `process(message, next) -> message`
325
+ - [ ] Built-in middleware: content filtering, prompt compression, rate limiting
326
+ - [ ] History summarization middleware: compress old messages to save tokens
327
+ - [ ] Logging middleware: structured request/response logging
328
+ - [ ] Retry middleware: automatic retry with backoff on transient errors
329
+ - [ ] `Conversation(middleware=[filter, compress, log])` configuration
330
+ - [ ] Middleware ordering and priority
331
+
332
+ ### Phase 9: Structured Observation Input
333
+ **Goal**: Typed input models for feeding structured context to conversations and agents.
334
+
335
+ - [ ] `Observation` base model for structured input (screen state, metrics, events)
336
+ - [ ] Observation-to-prompt template rendering
337
+ - [ ] Built-in observation types: `ScreenObservation`, `MetricsObservation`, `EventObservation`
338
+ - [ ] Custom observation models via Pydantic
339
+ - [ ] `conv.observe(ScreenObservation(app="Chrome", elements=[...]))` API
340
+ - [ ] Automatic observation diffing: only send what changed since last observation
341
+ - [ ] Observation history alongside message history
@@ -0,0 +1 @@
1
+ 0.0.37.dev1
@@ -5,6 +5,7 @@
5
5
 
6
6
  import os
7
7
  import sys
8
+ from pathlib import Path
8
9
 
9
10
  # Add the project root to the Python path so Sphinx can find the prompture package
10
11
  sys.path.insert(0, os.path.abspath("../../"))
@@ -13,9 +14,20 @@ sys.path.insert(0, os.path.abspath("../../"))
13
14
  # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
14
15
 
15
16
  project = "Prompture"
16
- copyright = "2025, Juan Denis"
17
+ copyright = '2026, <a href="https://juandenis.com">Juan Denis</a>'
17
18
  author = "Juan Denis"
18
- release = "0.0.23.dev1"
19
+
20
+ # Read version dynamically: VERSION file > setuptools_scm > fallback
21
+ _project_root = Path(__file__).resolve().parent.parent.parent
22
+ _version_file = _project_root / "VERSION"
23
+ if _version_file.exists():
24
+ release = _version_file.read_text().strip()
25
+ else:
26
+ try:
27
+ from setuptools_scm import get_version
28
+ release = get_version(root=str(_project_root))
29
+ except Exception:
30
+ release = "0.0.36"
19
31
 
20
32
  # -- General configuration ---------------------------------------------------
21
33
  # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration