contextpress 0.5.1__tar.gz → 0.5.3__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 (57) hide show
  1. {contextpress-0.5.1 → contextpress-0.5.3}/CHANGELOG.md +10 -0
  2. {contextpress-0.5.1 → contextpress-0.5.3}/PKG-INFO +10 -4
  3. {contextpress-0.5.1 → contextpress-0.5.3}/README.md +9 -3
  4. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/__init__.py +1 -1
  5. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/core.py +19 -0
  6. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/llm/adapters.py +34 -9
  7. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/stats.py +7 -0
  8. {contextpress-0.5.1 → contextpress-0.5.3}/examples/llm_tier_openai.py +1 -3
  9. contextpress-0.5.3/examples/pick_preset.py +21 -0
  10. {contextpress-0.5.1 → contextpress-0.5.3}/pyproject.toml +1 -1
  11. contextpress-0.5.3/tests/test_v052.py +32 -0
  12. contextpress-0.5.3/tests/test_v053.py +44 -0
  13. {contextpress-0.5.1 → contextpress-0.5.3}/.gitignore +0 -0
  14. {contextpress-0.5.1 → contextpress-0.5.3}/CITATION.cff +0 -0
  15. {contextpress-0.5.1 → contextpress-0.5.3}/CONTRIBUTING.md +0 -0
  16. {contextpress-0.5.1 → contextpress-0.5.3}/LICENSE +0 -0
  17. {contextpress-0.5.1 → contextpress-0.5.3}/NOTICE +0 -0
  18. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/_bootstrap.py +0 -0
  19. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/compression.py +0 -0
  20. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/llm/__init__.py +0 -0
  21. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/llm/_helpers.py +0 -0
  22. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/llm/base.py +0 -0
  23. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/models.py +0 -0
  24. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/normalizer.py +0 -0
  25. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/pipeline.py +0 -0
  26. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/profiles.py +0 -0
  27. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/py.typed +0 -0
  28. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/registry.py +0 -0
  29. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/strategies/__init__.py +0 -0
  30. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/strategies/base.py +0 -0
  31. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/strategies/budget.py +0 -0
  32. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/strategies/filler.py +0 -0
  33. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/strategies/recency.py +0 -0
  34. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/strategies/repetition.py +0 -0
  35. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/strategies/resolution.py +0 -0
  36. {contextpress-0.5.1 → contextpress-0.5.3}/contextpress/warnings_capture.py +0 -0
  37. {contextpress-0.5.1 → contextpress-0.5.3}/examples/benchmark_presets.py +0 -0
  38. {contextpress-0.5.1 → contextpress-0.5.3}/examples/dry_run_preview.py +0 -0
  39. {contextpress-0.5.1 → contextpress-0.5.3}/examples/estimate_and_stats.py +0 -0
  40. {contextpress-0.5.1 → contextpress-0.5.3}/examples/llm_tier_claude.py +0 -0
  41. {contextpress-0.5.1 → contextpress-0.5.3}/examples/llm_tier_gemini.py +0 -0
  42. {contextpress-0.5.1 → contextpress-0.5.3}/examples/llm_tier_ollama.py +0 -0
  43. {contextpress-0.5.1 → contextpress-0.5.3}/tests/__init__.py +0 -0
  44. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_budget.py +0 -0
  45. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_filler.py +0 -0
  46. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_llm_helpers.py +0 -0
  47. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_models.py +0 -0
  48. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_normalizer.py +0 -0
  49. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_pipeline.py +0 -0
  50. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_recency.py +0 -0
  51. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_repetition.py +0 -0
  52. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_resolution.py +0 -0
  53. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_stats.py +0 -0
  54. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_v03.py +0 -0
  55. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_v04.py +0 -0
  56. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_v05.py +0 -0
  57. {contextpress-0.5.1 → contextpress-0.5.3}/tests/test_v051.py +0 -0
@@ -4,6 +4,16 @@ All notable changes to `contextpress` are recorded here.
4
4
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.5.3] - 2026-07-24
8
+
9
+ - **`recommend_preset()`** — pick the mildest preset (low → medium → high) that fits a token budget.
10
+ - **`CompressionStats.token_savings_pct`** — percentage tokens saved; included in ``to_dict()``.
11
+ - Example: `examples/pick_preset.py`.
12
+
13
+ ## [0.5.2] - 2026-07-22
14
+
15
+ - **`OpenAIBackend`** — optional `api_key` / env (`OPENAI_API_KEY`); no manual client required.
16
+
7
17
  ## [0.5.1] - 2026-07-22
8
18
 
9
19
  - **`ClaudeBackend`** — shortcut for Anthropic Claude (`ANTHROPIC_API_KEY` or `api_key=`).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contextpress
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: Deterministic context compression for LLM chat, RAG, and agent pipelines
5
5
  Project-URL: Homepage, https://github.com/Taha-azizi/contextpress
6
6
  Project-URL: Documentation, https://github.com/Taha-azizi/contextpress#readme
@@ -313,7 +313,14 @@ if cm.fits_budget(messages, 4000):
313
313
  ```python
314
314
  rows = cm.compare_presets(messages, token_budget=500)
315
315
  for preset, stats in rows.items():
316
- print(preset, stats.tokens_saved)
316
+ print(preset, stats.tokens_saved, stats.token_savings_pct)
317
+ ```
318
+
319
+ **Recommend a preset** (0.5.3+):
320
+
321
+ ```python
322
+ preset = cm.recommend_preset(messages, token_budget=500)
323
+ out = cm.compress(messages, token_budget=500, compression=preset)
317
324
  ```
318
325
 
319
326
  **Async** (0.5+):
@@ -459,11 +466,10 @@ Optional constructor knobs: **`llm_min_input_chars`**, **`llm_max_summary_tokens
459
466
  **Install SDKs** (not bundled): `pip install openai`, `anthropic`, and/or **`ollama`** (for local Ollama), or `pip install "contextpress[llm]"` from this repo’s `pyproject.toml` to pull all optional LLM clients.
460
467
 
461
468
  ```python
462
- from openai import OpenAI
463
469
  from contextpress import ContextManager
464
470
  from contextpress.llm.adapters import OpenAIBackend
465
471
 
466
- backend = OpenAIBackend(client=OpenAI(), model="gpt-4o-mini")
472
+ backend = OpenAIBackend(model="gpt-4o-mini") # uses OPENAI_API_KEY
467
473
  cm = ContextManager(
468
474
  type="chat",
469
475
  llm_backend=backend,
@@ -72,7 +72,14 @@ if cm.fits_budget(messages, 4000):
72
72
  ```python
73
73
  rows = cm.compare_presets(messages, token_budget=500)
74
74
  for preset, stats in rows.items():
75
- print(preset, stats.tokens_saved)
75
+ print(preset, stats.tokens_saved, stats.token_savings_pct)
76
+ ```
77
+
78
+ **Recommend a preset** (0.5.3+):
79
+
80
+ ```python
81
+ preset = cm.recommend_preset(messages, token_budget=500)
82
+ out = cm.compress(messages, token_budget=500, compression=preset)
76
83
  ```
77
84
 
78
85
  **Async** (0.5+):
@@ -218,11 +225,10 @@ Optional constructor knobs: **`llm_min_input_chars`**, **`llm_max_summary_tokens
218
225
  **Install SDKs** (not bundled): `pip install openai`, `anthropic`, and/or **`ollama`** (for local Ollama), or `pip install "contextpress[llm]"` from this repo’s `pyproject.toml` to pull all optional LLM clients.
219
226
 
220
227
  ```python
221
- from openai import OpenAI
222
228
  from contextpress import ContextManager
223
229
  from contextpress.llm.adapters import OpenAIBackend
224
230
 
225
- backend = OpenAIBackend(client=OpenAI(), model="gpt-4o-mini")
231
+ backend = OpenAIBackend(model="gpt-4o-mini") # uses OPENAI_API_KEY
226
232
  cm = ContextManager(
227
233
  type="chat",
228
234
  llm_backend=backend,
@@ -16,7 +16,7 @@ __all__ = [
16
16
  "CompressionResult",
17
17
  "CompressionStats",
18
18
  ]
19
- __version__ = "0.5.1"
19
+ __version__ = "0.5.3"
20
20
 
21
21
 
22
22
  def __getattr__(name: str) -> Any:
@@ -101,6 +101,25 @@ class ContextManager:
101
101
  results[preset] = preview.stats
102
102
  return results
103
103
 
104
+ def recommend_preset(
105
+ self,
106
+ messages: Any,
107
+ token_budget: int,
108
+ *,
109
+ presets: tuple[str, ...] = ("low", "medium", "high"),
110
+ ) -> str:
111
+ """Return the mildest preset whose dry-run output fits ``token_budget``.
112
+
113
+ Presets are tried in order (default: low → medium → high). If none fit,
114
+ returns the preset with the lowest ``tokens_after`` (most compression).
115
+ """
116
+ _validate_token_budget(token_budget)
117
+ rows = self.compare_presets(messages, token_budget=token_budget, presets=presets)
118
+ for preset in presets:
119
+ if rows[preset].tokens_after <= token_budget:
120
+ return preset
121
+ return min(presets, key=lambda name: rows[name].tokens_after)
122
+
104
123
  def preview(
105
124
  self,
106
125
  messages: Any,
@@ -27,20 +27,45 @@ def _ollama_response_text(resp: Any) -> str:
27
27
 
28
28
  class OpenAIBackend(LLMBackend):
29
29
  """
30
- Adapter for OpenAI-compatible APIs.
31
- Requires: pip install openai
32
- User must pass their own client instance.
30
+ Adapter for **OpenAI** chat APIs.
31
+
32
+ Requires: ``pip install openai``
33
+
34
+ Usage (API key from env ``OPENAI_API_KEY`` or ``api_key=``)::
33
35
 
34
- Usage:
35
- from openai import OpenAI
36
36
  from contextpress.llm.adapters import OpenAIBackend
37
37
 
38
- backend = OpenAIBackend(client=OpenAI(), model="gpt-4o-mini")
39
- cm = ContextManager(type="chat", llm_backend=backend)
38
+ backend = OpenAIBackend(model=\"gpt-4o-mini\")
39
+ cm = ContextManager(type=\"chat\", llm_backend=backend)
40
+
41
+ Or pass your own client::
42
+
43
+ from openai import OpenAI
44
+
45
+ backend = OpenAIBackend(client=OpenAI(), model=\"gpt-4o-mini\")
40
46
  """
41
47
 
42
- def __init__(self, client: Any, model: str = "gpt-4o-mini"):
43
- self.client = client
48
+ def __init__(
49
+ self,
50
+ client: Any | None = None,
51
+ model: str = "gpt-4o-mini",
52
+ *,
53
+ api_key: str | None = None,
54
+ ):
55
+ if client is not None:
56
+ self.client = client
57
+ self.model = model
58
+ return
59
+ try:
60
+ from openai import OpenAI
61
+ except ImportError as exc:
62
+ raise ImportError(
63
+ "OpenAIBackend requires the 'openai' package. " "Install with: pip install openai"
64
+ ) from exc
65
+ key = api_key or os.environ.get("OPENAI_API_KEY")
66
+ if not key:
67
+ raise ValueError("Set OPENAI_API_KEY or pass api_key= for OpenAIBackend")
68
+ self.client = OpenAI(api_key=key)
44
69
  self.model = model
45
70
 
46
71
  def summarize(self, text: str, max_tokens: int) -> str:
@@ -60,6 +60,12 @@ class CompressionStats:
60
60
  def tokens_saved(self) -> int:
61
61
  return max(0, self.tokens_before - self.tokens_after)
62
62
 
63
+ @property
64
+ def token_savings_pct(self) -> float:
65
+ if self.tokens_before <= 0:
66
+ return 0.0
67
+ return round(100.0 * self.tokens_saved / self.tokens_before, 2)
68
+
63
69
  def to_dict(self) -> dict[str, Any]:
64
70
  """JSON-serializable snapshot of this run."""
65
71
  return {
@@ -69,6 +75,7 @@ class CompressionStats:
69
75
  "tokens_before": self.tokens_before,
70
76
  "tokens_after": self.tokens_after,
71
77
  "tokens_saved": self.tokens_saved,
78
+ "token_savings_pct": self.token_savings_pct,
72
79
  "stages_run": list(self.stages_run),
73
80
  "turn_delta_by_stage": dict(self.turn_delta_by_stage),
74
81
  "llm_tier_applied": self.llm_tier_applied,
@@ -17,8 +17,6 @@ from __future__ import annotations
17
17
  import os
18
18
  import sys
19
19
 
20
- from openai import OpenAI
21
-
22
20
  from contextpress import ContextManager
23
21
  from contextpress.llm.adapters import OpenAIBackend
24
22
 
@@ -35,7 +33,7 @@ def main() -> None:
35
33
  {"role": "user", "content": "And numpy? " * 80},
36
34
  ]
37
35
 
38
- backend = OpenAIBackend(client=OpenAI(), model="gpt-4o-mini")
36
+ backend = OpenAIBackend(model="gpt-4o-mini")
39
37
  cm = ContextManager(
40
38
  type="chat",
41
39
  llm_backend=backend,
@@ -0,0 +1,21 @@
1
+ """Pick the mildest compression preset that fits a token budget (dry run)."""
2
+
3
+ from contextpress import ContextManager
4
+
5
+ messages = [
6
+ {"role": "system", "content": "You are helpful."},
7
+ {"role": "user", "content": "Old topic: " + "blah " * 40},
8
+ {"role": "assistant", "content": "Basically, honestly, " + "yak " * 40},
9
+ {"role": "user", "content": "What is 2+2?"},
10
+ {"role": "assistant", "content": "Four."},
11
+ ]
12
+
13
+ cm = ContextManager(type="chat")
14
+ budget = 200
15
+ preset = cm.recommend_preset(messages, token_budget=budget)
16
+ stats = cm.compare_presets(messages, token_budget=budget)[preset]
17
+
18
+ print(f"budget={budget} recommended preset={preset!r}")
19
+ print(
20
+ f" tokens {stats.tokens_before}->{stats.tokens_after} " f"({stats.token_savings_pct}% saved)"
21
+ )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "contextpress"
7
- version = "0.5.1"
7
+ version = "0.5.3"
8
8
  description = "Deterministic context compression for LLM chat, RAG, and agent pipelines"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -0,0 +1,32 @@
1
+ import os
2
+ from unittest.mock import MagicMock, patch
3
+
4
+ import pytest
5
+
6
+ from contextpress.llm.adapters import OpenAIBackend
7
+
8
+
9
+ def test_openai_backend_with_client():
10
+ client = MagicMock()
11
+ backend = OpenAIBackend(client=client, model="gpt-4o-mini")
12
+ assert backend.client is client
13
+ assert backend.model == "gpt-4o-mini"
14
+
15
+
16
+ def test_openai_backend_requires_api_key():
17
+ mock_openai = MagicMock()
18
+ with patch.dict(os.environ, {}, clear=True):
19
+ with patch.dict("sys.modules", {"openai": mock_openai}):
20
+ with pytest.raises(ValueError, match="OPENAI_API_KEY"):
21
+ OpenAIBackend()
22
+
23
+
24
+ def test_openai_backend_from_api_key():
25
+ mock_module = MagicMock()
26
+ mock_client = MagicMock()
27
+ mock_module.OpenAI.return_value = mock_client
28
+ with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test"}):
29
+ with patch.dict("sys.modules", {"openai": mock_module}):
30
+ backend = OpenAIBackend(model="gpt-4o-mini")
31
+ assert backend.client is mock_client
32
+ mock_module.OpenAI.assert_called_once_with(api_key="sk-test")
@@ -0,0 +1,44 @@
1
+ from contextpress import ContextManager
2
+ from contextpress.stats import CompressionStats
3
+
4
+
5
+ def test_token_savings_pct():
6
+ stats = CompressionStats(tokens_before=100, tokens_after=60)
7
+ assert stats.token_savings_pct == 40.0
8
+ assert stats.to_dict()["token_savings_pct"] == 40.0
9
+
10
+
11
+ def test_token_savings_pct_zero_when_empty():
12
+ stats = CompressionStats(tokens_before=0, tokens_after=0)
13
+ assert stats.token_savings_pct == 0.0
14
+
15
+
16
+ def test_recommend_preset_picks_mildest_that_fits():
17
+ cm = ContextManager(type="chat")
18
+ messages = [
19
+ {"role": "user", "content": "hello basically there " * 5},
20
+ {"role": "assistant", "content": "sounds good " * 5},
21
+ ]
22
+ budget = 500
23
+ rows = cm.compare_presets(messages, token_budget=budget)
24
+ preset = cm.recommend_preset(messages, token_budget=budget)
25
+ assert rows[preset].tokens_after <= budget
26
+ fitting = [p for p in ("low", "medium", "high") if rows[p].tokens_after <= budget]
27
+ assert preset == fitting[0]
28
+
29
+
30
+ def test_recommend_preset_falls_back_to_most_compression():
31
+ cm = ContextManager(type="chat")
32
+ messages = [{"role": "user", "content": "word " * 500}]
33
+ preset = cm.recommend_preset(messages, token_budget=10)
34
+ rows = cm.compare_presets(messages, token_budget=10)
35
+ assert preset == min(("low", "medium", "high"), key=lambda p: rows[p].tokens_after)
36
+
37
+
38
+ def test_recommend_preset_requires_budget():
39
+ cm = ContextManager(type="chat")
40
+ try:
41
+ cm.recommend_preset([{"role": "user", "content": "hi"}], token_budget=0)
42
+ raise AssertionError("expected ValueError")
43
+ except ValueError:
44
+ pass
File without changes
File without changes
File without changes
File without changes