contextpress 0.5.2__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.2 → contextpress-0.5.3}/CHANGELOG.md +6 -0
  2. {contextpress-0.5.2 → contextpress-0.5.3}/PKG-INFO +9 -2
  3. {contextpress-0.5.2 → contextpress-0.5.3}/README.md +8 -1
  4. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/__init__.py +1 -1
  5. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/core.py +19 -0
  6. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/stats.py +7 -0
  7. contextpress-0.5.3/examples/pick_preset.py +21 -0
  8. {contextpress-0.5.2 → contextpress-0.5.3}/pyproject.toml +1 -1
  9. contextpress-0.5.3/tests/test_v053.py +44 -0
  10. {contextpress-0.5.2 → contextpress-0.5.3}/.gitignore +0 -0
  11. {contextpress-0.5.2 → contextpress-0.5.3}/CITATION.cff +0 -0
  12. {contextpress-0.5.2 → contextpress-0.5.3}/CONTRIBUTING.md +0 -0
  13. {contextpress-0.5.2 → contextpress-0.5.3}/LICENSE +0 -0
  14. {contextpress-0.5.2 → contextpress-0.5.3}/NOTICE +0 -0
  15. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/_bootstrap.py +0 -0
  16. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/compression.py +0 -0
  17. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/llm/__init__.py +0 -0
  18. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/llm/_helpers.py +0 -0
  19. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/llm/adapters.py +0 -0
  20. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/llm/base.py +0 -0
  21. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/models.py +0 -0
  22. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/normalizer.py +0 -0
  23. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/pipeline.py +0 -0
  24. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/profiles.py +0 -0
  25. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/py.typed +0 -0
  26. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/registry.py +0 -0
  27. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/strategies/__init__.py +0 -0
  28. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/strategies/base.py +0 -0
  29. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/strategies/budget.py +0 -0
  30. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/strategies/filler.py +0 -0
  31. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/strategies/recency.py +0 -0
  32. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/strategies/repetition.py +0 -0
  33. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/strategies/resolution.py +0 -0
  34. {contextpress-0.5.2 → contextpress-0.5.3}/contextpress/warnings_capture.py +0 -0
  35. {contextpress-0.5.2 → contextpress-0.5.3}/examples/benchmark_presets.py +0 -0
  36. {contextpress-0.5.2 → contextpress-0.5.3}/examples/dry_run_preview.py +0 -0
  37. {contextpress-0.5.2 → contextpress-0.5.3}/examples/estimate_and_stats.py +0 -0
  38. {contextpress-0.5.2 → contextpress-0.5.3}/examples/llm_tier_claude.py +0 -0
  39. {contextpress-0.5.2 → contextpress-0.5.3}/examples/llm_tier_gemini.py +0 -0
  40. {contextpress-0.5.2 → contextpress-0.5.3}/examples/llm_tier_ollama.py +0 -0
  41. {contextpress-0.5.2 → contextpress-0.5.3}/examples/llm_tier_openai.py +0 -0
  42. {contextpress-0.5.2 → contextpress-0.5.3}/tests/__init__.py +0 -0
  43. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_budget.py +0 -0
  44. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_filler.py +0 -0
  45. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_llm_helpers.py +0 -0
  46. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_models.py +0 -0
  47. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_normalizer.py +0 -0
  48. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_pipeline.py +0 -0
  49. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_recency.py +0 -0
  50. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_repetition.py +0 -0
  51. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_resolution.py +0 -0
  52. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_stats.py +0 -0
  53. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_v03.py +0 -0
  54. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_v04.py +0 -0
  55. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_v05.py +0 -0
  56. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_v051.py +0 -0
  57. {contextpress-0.5.2 → contextpress-0.5.3}/tests/test_v052.py +0 -0
@@ -4,6 +4,12 @@ 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
+
7
13
  ## [0.5.2] - 2026-07-22
8
14
 
9
15
  - **`OpenAIBackend`** — optional `api_key` / env (`OPENAI_API_KEY`); no manual client required.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contextpress
3
- Version: 0.5.2
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+):
@@ -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+):
@@ -16,7 +16,7 @@ __all__ = [
16
16
  "CompressionResult",
17
17
  "CompressionStats",
18
18
  ]
19
- __version__ = "0.5.2"
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,
@@ -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,
@@ -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.2"
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,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