contextpress 0.5.2__tar.gz → 0.5.4__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 (59) hide show
  1. {contextpress-0.5.2 → contextpress-0.5.4}/CHANGELOG.md +11 -0
  2. {contextpress-0.5.2 → contextpress-0.5.4}/PKG-INFO +17 -2
  3. {contextpress-0.5.2 → contextpress-0.5.4}/README.md +16 -1
  4. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/__init__.py +1 -1
  5. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/core.py +46 -0
  6. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/stats.py +7 -0
  7. contextpress-0.5.4/examples/agent_pipeline.py +29 -0
  8. contextpress-0.5.4/examples/pick_preset.py +21 -0
  9. {contextpress-0.5.2 → contextpress-0.5.4}/pyproject.toml +1 -1
  10. contextpress-0.5.4/tests/test_v053.py +44 -0
  11. contextpress-0.5.4/tests/test_v054.py +53 -0
  12. {contextpress-0.5.2 → contextpress-0.5.4}/.gitignore +0 -0
  13. {contextpress-0.5.2 → contextpress-0.5.4}/CITATION.cff +0 -0
  14. {contextpress-0.5.2 → contextpress-0.5.4}/CONTRIBUTING.md +0 -0
  15. {contextpress-0.5.2 → contextpress-0.5.4}/LICENSE +0 -0
  16. {contextpress-0.5.2 → contextpress-0.5.4}/NOTICE +0 -0
  17. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/_bootstrap.py +0 -0
  18. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/compression.py +0 -0
  19. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/llm/__init__.py +0 -0
  20. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/llm/_helpers.py +0 -0
  21. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/llm/adapters.py +0 -0
  22. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/llm/base.py +0 -0
  23. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/models.py +0 -0
  24. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/normalizer.py +0 -0
  25. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/pipeline.py +0 -0
  26. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/profiles.py +0 -0
  27. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/py.typed +0 -0
  28. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/registry.py +0 -0
  29. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/strategies/__init__.py +0 -0
  30. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/strategies/base.py +0 -0
  31. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/strategies/budget.py +0 -0
  32. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/strategies/filler.py +0 -0
  33. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/strategies/recency.py +0 -0
  34. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/strategies/repetition.py +0 -0
  35. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/strategies/resolution.py +0 -0
  36. {contextpress-0.5.2 → contextpress-0.5.4}/contextpress/warnings_capture.py +0 -0
  37. {contextpress-0.5.2 → contextpress-0.5.4}/examples/benchmark_presets.py +0 -0
  38. {contextpress-0.5.2 → contextpress-0.5.4}/examples/dry_run_preview.py +0 -0
  39. {contextpress-0.5.2 → contextpress-0.5.4}/examples/estimate_and_stats.py +0 -0
  40. {contextpress-0.5.2 → contextpress-0.5.4}/examples/llm_tier_claude.py +0 -0
  41. {contextpress-0.5.2 → contextpress-0.5.4}/examples/llm_tier_gemini.py +0 -0
  42. {contextpress-0.5.2 → contextpress-0.5.4}/examples/llm_tier_ollama.py +0 -0
  43. {contextpress-0.5.2 → contextpress-0.5.4}/examples/llm_tier_openai.py +0 -0
  44. {contextpress-0.5.2 → contextpress-0.5.4}/tests/__init__.py +0 -0
  45. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_budget.py +0 -0
  46. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_filler.py +0 -0
  47. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_llm_helpers.py +0 -0
  48. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_models.py +0 -0
  49. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_normalizer.py +0 -0
  50. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_pipeline.py +0 -0
  51. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_recency.py +0 -0
  52. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_repetition.py +0 -0
  53. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_resolution.py +0 -0
  54. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_stats.py +0 -0
  55. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_v03.py +0 -0
  56. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_v04.py +0 -0
  57. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_v05.py +0 -0
  58. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_v051.py +0 -0
  59. {contextpress-0.5.2 → contextpress-0.5.4}/tests/test_v052.py +0 -0
@@ -4,6 +4,17 @@ 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.4] - 2026-07-25
8
+
9
+ - **`compress_many()`** — batch ``compress()`` over a list of conversations.
10
+ - Example: `examples/agent_pipeline.py` for ``type="agent"`` tool/task threads.
11
+
12
+ ## [0.5.3] - 2026-07-24
13
+
14
+ - **`recommend_preset()`** — pick the mildest preset (low → medium → high) that fits a token budget.
15
+ - **`CompressionStats.token_savings_pct`** — percentage tokens saved; included in ``to_dict()``.
16
+ - Example: `examples/pick_preset.py`.
17
+
7
18
  ## [0.5.2] - 2026-07-22
8
19
 
9
20
  - **`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.4
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,20 @@ 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)
324
+ ```
325
+
326
+ **Batch compress** (0.5.4+):
327
+
328
+ ```python
329
+ results = cm.compress_many(list_of_conversations, token_budget=2000, return_stats=True)
317
330
  ```
318
331
 
319
332
  **Async** (0.5+):
@@ -384,6 +397,8 @@ ContextManager(type="rag_doc")
384
397
  ContextManager(type="agent")
385
398
  ```
386
399
 
400
+ Runnable agent example: [`examples/agent_pipeline.py`](examples/agent_pipeline.py).
401
+
387
402
  ## Pipeline stages
388
403
 
389
404
  1. **Filler** — Removes low-semantic filler words and (in chat/agent) drops acknowledgement-only assistant turns.
@@ -72,7 +72,20 @@ 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)
83
+ ```
84
+
85
+ **Batch compress** (0.5.4+):
86
+
87
+ ```python
88
+ results = cm.compress_many(list_of_conversations, token_budget=2000, return_stats=True)
76
89
  ```
77
90
 
78
91
  **Async** (0.5+):
@@ -143,6 +156,8 @@ ContextManager(type="rag_doc")
143
156
  ContextManager(type="agent")
144
157
  ```
145
158
 
159
+ Runnable agent example: [`examples/agent_pipeline.py`](examples/agent_pipeline.py).
160
+
146
161
  ## Pipeline stages
147
162
 
148
163
  1. **Filler** — Removes low-semantic filler words and (in chat/agent) drops acknowledgement-only assistant turns.
@@ -16,7 +16,7 @@ __all__ = [
16
16
  "CompressionResult",
17
17
  "CompressionStats",
18
18
  ]
19
- __version__ = "0.5.2"
19
+ __version__ = "0.5.4"
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,
@@ -191,6 +210,33 @@ class ContextManager:
191
210
  return CompressionResult(messages=messages_out, stats=stats)
192
211
  return messages_out
193
212
 
213
+ def compress_many(
214
+ self,
215
+ conversations: list[Any],
216
+ token_budget: int | None = None,
217
+ *,
218
+ compression: str | None = None,
219
+ stages: list[str] | None = None,
220
+ disable: list[str] | None = None,
221
+ return_stats: bool = False,
222
+ dry_run: bool = False,
223
+ ) -> list[Any] | list[CompressionResult]:
224
+ """Run ``compress()`` on each conversation in ``conversations``."""
225
+ if not isinstance(conversations, list):
226
+ raise TypeError("conversations must be a list")
227
+ return [
228
+ self.compress(
229
+ messages,
230
+ token_budget=token_budget,
231
+ compression=compression,
232
+ stages=stages,
233
+ disable=disable,
234
+ return_stats=return_stats,
235
+ dry_run=dry_run,
236
+ )
237
+ for messages in conversations
238
+ ]
239
+
194
240
  async def compress_async(
195
241
  self,
196
242
  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,29 @@
1
+ """Agent thread compression — ``type=\"agent\"`` for tool-using pipelines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextpress import ContextManager
6
+
7
+ messages = [
8
+ {"role": "system", "content": "You are a task agent with tools."},
9
+ {"role": "user", "content": "Check whether api-v2 is deployed to staging."},
10
+ {
11
+ "role": "assistant",
12
+ "content": "Checking deploy status <tool_call> get_deploy_status(api-v2)",
13
+ },
14
+ {"role": "user", "content": "We've decided on using the new pipeline for deploy."},
15
+ {"role": "assistant", "content": "Confirmed — staging deploy is scheduled."},
16
+ {"role": "user", "content": "What's the current status?"},
17
+ {"role": "assistant", "content": "Basically, honestly, " + "still rolling out " * 30},
18
+ ]
19
+
20
+ cm = ContextManager(type="agent", compression="high")
21
+ result = cm.compress(messages, token_budget=300, return_stats=True)
22
+
23
+ print("context type:", result.stats.context_type)
24
+ print("turns:", result.stats.turns_before, "->", result.stats.turns_after)
25
+ print("tokens:", result.stats.tokens_before, "->", result.stats.tokens_after)
26
+ print("stages:", result.stats.stages_run)
27
+ for i, m in enumerate(result.messages):
28
+ preview = str(m.get("content", ""))[:120]
29
+ print(i, m["role"] + ":", preview)
@@ -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.4"
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
@@ -0,0 +1,53 @@
1
+ import pytest
2
+
3
+ from contextpress import CompressionResult, ContextManager
4
+
5
+
6
+ def test_compress_many_returns_list():
7
+ cm = ContextManager(type="chat")
8
+ batches = [
9
+ [{"role": "user", "content": "hello basically"}],
10
+ [{"role": "user", "content": "thanks basically"}],
11
+ ]
12
+ out = cm.compress_many(batches, token_budget=200)
13
+ assert isinstance(out, list)
14
+ assert len(out) == 2
15
+ assert all(isinstance(item, list) for item in out)
16
+
17
+
18
+ def test_compress_many_with_stats():
19
+ cm = ContextManager(type="chat")
20
+ batches = [
21
+ [{"role": "user", "content": "hello basically there"}],
22
+ [{"role": "assistant", "content": "sounds good"}],
23
+ ]
24
+ results = cm.compress_many(batches, token_budget=500, return_stats=True)
25
+ assert len(results) == 2
26
+ assert all(isinstance(r, CompressionResult) for r in results)
27
+ assert results[0].stats.turns_before == 1
28
+ assert results[1].stats.turns_before == 1
29
+
30
+
31
+ def test_compress_many_empty():
32
+ cm = ContextManager(type="chat")
33
+ assert cm.compress_many([], token_budget=100) == []
34
+
35
+
36
+ def test_compress_many_requires_list():
37
+ cm = ContextManager(type="chat")
38
+ with pytest.raises(TypeError, match="conversations must be a list"):
39
+ cm.compress_many("not a list") # type: ignore[arg-type]
40
+
41
+
42
+ def test_agent_pipeline_compresses():
43
+ cm = ContextManager(type="agent", compression="high")
44
+ messages = [
45
+ {"role": "system", "content": "Agent."},
46
+ {"role": "user", "content": "Deploy api-v2?"},
47
+ {"role": "assistant", "content": "Using pipeline <tool_call> deploy"},
48
+ {"role": "user", "content": "We've decided on using the new pipeline for deploy."},
49
+ {"role": "assistant", "content": "Confirmed."},
50
+ ]
51
+ result = cm.compress(messages, token_budget=200, return_stats=True)
52
+ assert result.stats.context_type == "agent"
53
+ assert result.stats.tokens_after <= result.stats.tokens_before
File without changes
File without changes
File without changes
File without changes