contextpress 0.4.0__tar.gz → 0.5.1__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 (54) hide show
  1. {contextpress-0.4.0 → contextpress-0.5.1}/CHANGELOG.md +14 -0
  2. {contextpress-0.4.0 → contextpress-0.5.1}/PKG-INFO +43 -2
  3. {contextpress-0.4.0 → contextpress-0.5.1}/README.md +41 -1
  4. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/__init__.py +1 -1
  5. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/core.py +38 -0
  6. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/llm/__init__.py +4 -0
  7. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/llm/adapters.py +108 -0
  8. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/stats.py +27 -0
  9. contextpress-0.5.1/examples/benchmark_presets.py +23 -0
  10. contextpress-0.5.1/examples/llm_tier_claude.py +50 -0
  11. contextpress-0.5.1/examples/llm_tier_gemini.py +50 -0
  12. {contextpress-0.4.0 → contextpress-0.5.1}/pyproject.toml +2 -2
  13. contextpress-0.5.1/tests/test_v05.py +66 -0
  14. contextpress-0.5.1/tests/test_v051.py +50 -0
  15. {contextpress-0.4.0 → contextpress-0.5.1}/.gitignore +0 -0
  16. {contextpress-0.4.0 → contextpress-0.5.1}/CITATION.cff +0 -0
  17. {contextpress-0.4.0 → contextpress-0.5.1}/CONTRIBUTING.md +0 -0
  18. {contextpress-0.4.0 → contextpress-0.5.1}/LICENSE +0 -0
  19. {contextpress-0.4.0 → contextpress-0.5.1}/NOTICE +0 -0
  20. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/_bootstrap.py +0 -0
  21. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/compression.py +0 -0
  22. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/llm/_helpers.py +0 -0
  23. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/llm/base.py +0 -0
  24. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/models.py +0 -0
  25. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/normalizer.py +0 -0
  26. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/pipeline.py +0 -0
  27. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/profiles.py +0 -0
  28. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/py.typed +0 -0
  29. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/registry.py +0 -0
  30. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/strategies/__init__.py +0 -0
  31. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/strategies/base.py +0 -0
  32. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/strategies/budget.py +0 -0
  33. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/strategies/filler.py +0 -0
  34. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/strategies/recency.py +0 -0
  35. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/strategies/repetition.py +0 -0
  36. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/strategies/resolution.py +0 -0
  37. {contextpress-0.4.0 → contextpress-0.5.1}/contextpress/warnings_capture.py +0 -0
  38. {contextpress-0.4.0 → contextpress-0.5.1}/examples/dry_run_preview.py +0 -0
  39. {contextpress-0.4.0 → contextpress-0.5.1}/examples/estimate_and_stats.py +0 -0
  40. {contextpress-0.4.0 → contextpress-0.5.1}/examples/llm_tier_ollama.py +0 -0
  41. {contextpress-0.4.0 → contextpress-0.5.1}/examples/llm_tier_openai.py +0 -0
  42. {contextpress-0.4.0 → contextpress-0.5.1}/tests/__init__.py +0 -0
  43. {contextpress-0.4.0 → contextpress-0.5.1}/tests/test_budget.py +0 -0
  44. {contextpress-0.4.0 → contextpress-0.5.1}/tests/test_filler.py +0 -0
  45. {contextpress-0.4.0 → contextpress-0.5.1}/tests/test_llm_helpers.py +0 -0
  46. {contextpress-0.4.0 → contextpress-0.5.1}/tests/test_models.py +0 -0
  47. {contextpress-0.4.0 → contextpress-0.5.1}/tests/test_normalizer.py +0 -0
  48. {contextpress-0.4.0 → contextpress-0.5.1}/tests/test_pipeline.py +0 -0
  49. {contextpress-0.4.0 → contextpress-0.5.1}/tests/test_recency.py +0 -0
  50. {contextpress-0.4.0 → contextpress-0.5.1}/tests/test_repetition.py +0 -0
  51. {contextpress-0.4.0 → contextpress-0.5.1}/tests/test_resolution.py +0 -0
  52. {contextpress-0.4.0 → contextpress-0.5.1}/tests/test_stats.py +0 -0
  53. {contextpress-0.4.0 → contextpress-0.5.1}/tests/test_v03.py +0 -0
  54. {contextpress-0.4.0 → contextpress-0.5.1}/tests/test_v04.py +0 -0
@@ -4,6 +4,20 @@ 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.1] - 2026-07-22
8
+
9
+ - **`ClaudeBackend`** — shortcut for Anthropic Claude (`ANTHROPIC_API_KEY` or `api_key=`).
10
+ - **`GeminiBackend`** — optional `model_name` + `api_key` / env (`GOOGLE_API_KEY`, `GEMINI_API_KEY`).
11
+ - Examples: `examples/llm_tier_claude.py`, `examples/llm_tier_gemini.py`.
12
+
13
+ ## [0.5.0] - 2026-07-22
14
+
15
+ - **`compress_async()`** — async wrapper around ``compress()`` for asyncio apps.
16
+ - **`compare_presets()`** — dry-run low/medium/high on the same messages and compare stats.
17
+ - **`CompressionStats.to_dict()` / `CompressionResult.to_dict()`** — JSON-friendly export.
18
+ - **`GeminiBackend`** — Google Gemini Tier 2 adapter (optional ``google-generativeai``).
19
+ - Example: `examples/benchmark_presets.py`.
20
+
7
21
  ## [0.4.0] - 2026-07-20
8
22
 
9
23
  - **`preview()` / `dry_run=True`** — simulate compression and get stats without changing messages (Tier 1 only; no LLM calls).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contextpress
3
- Version: 0.4.0
3
+ Version: 0.5.1
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
@@ -234,6 +234,7 @@ Requires-Dist: pytest>=7.0; extra == 'dev'
234
234
  Requires-Dist: ruff; extra == 'dev'
235
235
  Provides-Extra: llm
236
236
  Requires-Dist: anthropic>=0.18; extra == 'llm'
237
+ Requires-Dist: google-generativeai>=0.8.0; extra == 'llm'
237
238
  Requires-Dist: ollama>=0.3.0; extra == 'llm'
238
239
  Requires-Dist: openai>=1.0; extra == 'llm'
239
240
  Description-Content-Type: text/markdown
@@ -307,6 +308,20 @@ if cm.fits_budget(messages, 4000):
307
308
  ...
308
309
  ```
309
310
 
311
+ **Compare presets** (0.5+):
312
+
313
+ ```python
314
+ rows = cm.compare_presets(messages, token_budget=500)
315
+ for preset, stats in rows.items():
316
+ print(preset, stats.tokens_saved)
317
+ ```
318
+
319
+ **Async** (0.5+):
320
+
321
+ ```python
322
+ out = await cm.compress_async(messages, token_budget=2000)
323
+ ```
324
+
310
325
  ### Custom stages (0.3+)
311
326
 
312
327
  Register a `BaseStrategy` and include it in `stages=`:
@@ -386,7 +401,7 @@ ContextManager(type="agent")
386
401
  | **Techniques** | Rules, TF–IDF, cosine similarity, NLTK, Sumy extractive summarization, tiktoken | Your provider’s chat/completions API (you supply the client) |
387
402
  | **API key** | None | Required for your chosen provider (OpenAI, Anthropic, …) |
388
403
  | **Determinism** | Deterministic for a fixed input and settings | Non-deterministic (model sampling) |
389
- | **How to enable** | Default: `ContextManager()` runs Tier 1 only | Pass `llm_backend=` (`OpenAIBackend`, `AnthropicBackend`, **`OllamaBackend`**, or custom `LLMBackend`) |
404
+ | **How to enable** | Default: `ContextManager()` runs Tier 1 only | Pass `llm_backend=` (`OpenAIBackend`, **`ClaudeBackend`**, `GeminiBackend`, `OllamaBackend`, or custom `LLMBackend`) |
390
405
 
391
406
  **Note:** `ContextManager(model="gpt-4")` is only for **tiktoken** encoding when counting tokens in the **budget** stage. It does **not** call that model unless you also pass **`llm_backend`**.
392
407
 
@@ -479,6 +494,32 @@ out = cm.compress(messages, token_budget=4000)
479
494
 
480
495
  Runnable script: [`examples/llm_tier_ollama.py`](examples/llm_tier_ollama.py).
481
496
 
497
+ **Claude (Anthropic)** — `pip install anthropic`, set `ANTHROPIC_API_KEY`:
498
+
499
+ ```python
500
+ from contextpress import ContextManager
501
+ from contextpress.llm.adapters import ClaudeBackend
502
+
503
+ backend = ClaudeBackend(model="claude-haiku-4-5")
504
+ cm = ContextManager(type="chat", llm_backend=backend, llm_min_input_chars=500)
505
+ out = cm.compress(messages, token_budget=4000)
506
+ ```
507
+
508
+ Runnable script: [`examples/llm_tier_claude.py`](examples/llm_tier_claude.py).
509
+
510
+ **Gemini (Google)** — `pip install google-generativeai`, set `GOOGLE_API_KEY`:
511
+
512
+ ```python
513
+ from contextpress import ContextManager
514
+ from contextpress.llm.adapters import GeminiBackend
515
+
516
+ backend = GeminiBackend(model_name="gemini-2.0-flash")
517
+ cm = ContextManager(type="chat", llm_backend=backend, llm_min_input_chars=500)
518
+ out = cm.compress(messages, token_budget=4000)
519
+ ```
520
+
521
+ Runnable script: [`examples/llm_tier_gemini.py`](examples/llm_tier_gemini.py).
522
+
482
523
  ```bash
483
524
  pip install ollama
484
525
  ollama pull llama3.2
@@ -67,6 +67,20 @@ if cm.fits_budget(messages, 4000):
67
67
  ...
68
68
  ```
69
69
 
70
+ **Compare presets** (0.5+):
71
+
72
+ ```python
73
+ rows = cm.compare_presets(messages, token_budget=500)
74
+ for preset, stats in rows.items():
75
+ print(preset, stats.tokens_saved)
76
+ ```
77
+
78
+ **Async** (0.5+):
79
+
80
+ ```python
81
+ out = await cm.compress_async(messages, token_budget=2000)
82
+ ```
83
+
70
84
  ### Custom stages (0.3+)
71
85
 
72
86
  Register a `BaseStrategy` and include it in `stages=`:
@@ -146,7 +160,7 @@ ContextManager(type="agent")
146
160
  | **Techniques** | Rules, TF–IDF, cosine similarity, NLTK, Sumy extractive summarization, tiktoken | Your provider’s chat/completions API (you supply the client) |
147
161
  | **API key** | None | Required for your chosen provider (OpenAI, Anthropic, …) |
148
162
  | **Determinism** | Deterministic for a fixed input and settings | Non-deterministic (model sampling) |
149
- | **How to enable** | Default: `ContextManager()` runs Tier 1 only | Pass `llm_backend=` (`OpenAIBackend`, `AnthropicBackend`, **`OllamaBackend`**, or custom `LLMBackend`) |
163
+ | **How to enable** | Default: `ContextManager()` runs Tier 1 only | Pass `llm_backend=` (`OpenAIBackend`, **`ClaudeBackend`**, `GeminiBackend`, `OllamaBackend`, or custom `LLMBackend`) |
150
164
 
151
165
  **Note:** `ContextManager(model="gpt-4")` is only for **tiktoken** encoding when counting tokens in the **budget** stage. It does **not** call that model unless you also pass **`llm_backend`**.
152
166
 
@@ -239,6 +253,32 @@ out = cm.compress(messages, token_budget=4000)
239
253
 
240
254
  Runnable script: [`examples/llm_tier_ollama.py`](examples/llm_tier_ollama.py).
241
255
 
256
+ **Claude (Anthropic)** — `pip install anthropic`, set `ANTHROPIC_API_KEY`:
257
+
258
+ ```python
259
+ from contextpress import ContextManager
260
+ from contextpress.llm.adapters import ClaudeBackend
261
+
262
+ backend = ClaudeBackend(model="claude-haiku-4-5")
263
+ cm = ContextManager(type="chat", llm_backend=backend, llm_min_input_chars=500)
264
+ out = cm.compress(messages, token_budget=4000)
265
+ ```
266
+
267
+ Runnable script: [`examples/llm_tier_claude.py`](examples/llm_tier_claude.py).
268
+
269
+ **Gemini (Google)** — `pip install google-generativeai`, set `GOOGLE_API_KEY`:
270
+
271
+ ```python
272
+ from contextpress import ContextManager
273
+ from contextpress.llm.adapters import GeminiBackend
274
+
275
+ backend = GeminiBackend(model_name="gemini-2.0-flash")
276
+ cm = ContextManager(type="chat", llm_backend=backend, llm_min_input_chars=500)
277
+ out = cm.compress(messages, token_budget=4000)
278
+ ```
279
+
280
+ Runnable script: [`examples/llm_tier_gemini.py`](examples/llm_tier_gemini.py).
281
+
242
282
  ```bash
243
283
  pip install ollama
244
284
  ollama pull llama3.2
@@ -16,7 +16,7 @@ __all__ = [
16
16
  "CompressionResult",
17
17
  "CompressionStats",
18
18
  ]
19
- __version__ = "0.4.0"
19
+ __version__ = "0.5.1"
20
20
 
21
21
 
22
22
  def __getattr__(name: str) -> Any:
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  import copy
4
5
  import warnings
5
6
  from typing import TYPE_CHECKING, Any
@@ -86,6 +87,20 @@ class ContextManager:
86
87
  )
87
88
  return preview.stats.tokens_after <= token_budget
88
89
 
90
+ def compare_presets(
91
+ self,
92
+ messages: Any,
93
+ token_budget: int | None = None,
94
+ *,
95
+ presets: tuple[str, ...] = ("low", "medium", "high"),
96
+ ) -> dict[str, CompressionStats]:
97
+ """Dry-run each compression preset and return stats keyed by preset name."""
98
+ results: dict[str, CompressionStats] = {}
99
+ for preset in presets:
100
+ preview = self.preview(messages, token_budget=token_budget, compression=preset)
101
+ results[preset] = preview.stats
102
+ return results
103
+
89
104
  def preview(
90
105
  self,
91
106
  messages: Any,
@@ -176,6 +191,29 @@ class ContextManager:
176
191
  return CompressionResult(messages=messages_out, stats=stats)
177
192
  return messages_out
178
193
 
194
+ async def compress_async(
195
+ self,
196
+ messages: Any,
197
+ token_budget: int | None = None,
198
+ *,
199
+ compression: str | None = None,
200
+ stages: list[str] | None = None,
201
+ disable: list[str] | None = None,
202
+ return_stats: bool = False,
203
+ dry_run: bool = False,
204
+ ) -> Any | CompressionResult:
205
+ """Async wrapper around ``compress()`` (runs in a worker thread)."""
206
+ return await asyncio.to_thread(
207
+ self.compress,
208
+ messages,
209
+ token_budget,
210
+ compression=compression,
211
+ stages=stages,
212
+ disable=disable,
213
+ return_stats=return_stats,
214
+ dry_run=dry_run,
215
+ )
216
+
179
217
  def set_compression(self, compression: str) -> None:
180
218
  """Change the default preset for subsequent ``compress()`` calls (low / medium / high)."""
181
219
  self._compression = normalize_compression_level(compression)
@@ -1,5 +1,7 @@
1
1
  from contextpress.llm.adapters import (
2
2
  AnthropicBackend,
3
+ ClaudeBackend,
4
+ GeminiBackend,
3
5
  OllamaBackend,
4
6
  OpenAIBackend,
5
7
  OpenAICompatibleBackend,
@@ -11,5 +13,7 @@ __all__ = [
11
13
  "OpenAIBackend",
12
14
  "OpenAICompatibleBackend",
13
15
  "AnthropicBackend",
16
+ "ClaudeBackend",
17
+ "GeminiBackend",
14
18
  "OllamaBackend",
15
19
  ]
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  import warnings
4
5
  from typing import Any
5
6
 
@@ -135,6 +136,43 @@ class AnthropicBackend(LLMBackend):
135
136
  return list(range(len(turns)))
136
137
 
137
138
 
139
+ class ClaudeBackend(AnthropicBackend):
140
+ """
141
+ Convenience adapter for **Anthropic Claude** (wraps ``AnthropicBackend``).
142
+
143
+ Requires: ``pip install anthropic``
144
+
145
+ Usage::
146
+
147
+ from contextpress.llm.adapters import ClaudeBackend
148
+
149
+ backend = ClaudeBackend(model=\"claude-haiku-4-5\") # uses ANTHROPIC_API_KEY
150
+ cm = ContextManager(type=\"chat\", llm_backend=backend)
151
+ """
152
+
153
+ def __init__(
154
+ self,
155
+ model: str = "claude-haiku-4-5",
156
+ *,
157
+ api_key: str | None = None,
158
+ client: Any | None = None,
159
+ ):
160
+ if client is not None:
161
+ super().__init__(client=client, model=model)
162
+ return
163
+ try:
164
+ import anthropic
165
+ except ImportError as exc:
166
+ raise ImportError(
167
+ "ClaudeBackend requires the 'anthropic' package. "
168
+ "Install with: pip install anthropic"
169
+ ) from exc
170
+ key = api_key or os.environ.get("ANTHROPIC_API_KEY")
171
+ if not key:
172
+ raise ValueError("Set ANTHROPIC_API_KEY or pass api_key= for ClaudeBackend")
173
+ super().__init__(client=anthropic.Anthropic(api_key=key), model=model)
174
+
175
+
138
176
  class OllamaBackend(LLMBackend):
139
177
  """
140
178
  Adapter for **Ollama** (local or remote) using the official ``ollama`` Python library.
@@ -262,3 +300,73 @@ class OpenAICompatibleBackend(OpenAIBackend):
262
300
  client=OpenAI(base_url=base_url, api_key=api_key),
263
301
  model=model,
264
302
  )
303
+
304
+
305
+ class GeminiBackend(LLMBackend):
306
+ """
307
+ Adapter for Google Gemini via ``google-generativeai``.
308
+
309
+ Requires: ``pip install google-generativeai``
310
+
311
+ Usage (API key from env ``GOOGLE_API_KEY`` or ``GEMINI_API_KEY``)::
312
+
313
+ from contextpress.llm.adapters import GeminiBackend
314
+
315
+ backend = GeminiBackend(model_name=\"gemini-2.0-flash\")
316
+ cm = ContextManager(type=\"chat\", llm_backend=backend)
317
+
318
+ Or pass a preconfigured model instance::
319
+
320
+ import google.generativeai as genai
321
+ genai.configure(api_key=\"...\")
322
+ backend = GeminiBackend(model=genai.GenerativeModel(\"gemini-2.0-flash\"))
323
+ """
324
+
325
+ def __init__(
326
+ self,
327
+ model: Any | None = None,
328
+ *,
329
+ model_name: str = "gemini-2.0-flash",
330
+ api_key: str | None = None,
331
+ ):
332
+ if model is not None:
333
+ self.model = model
334
+ return
335
+ try:
336
+ import google.generativeai as genai
337
+ except ImportError as exc:
338
+ raise ImportError(
339
+ "GeminiBackend requires 'google-generativeai'. "
340
+ "Install with: pip install google-generativeai"
341
+ ) from exc
342
+ key = api_key or os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY")
343
+ if key:
344
+ genai.configure(api_key=key)
345
+ self.model = genai.GenerativeModel(model_name)
346
+
347
+ def _generate(self, prompt: str, *, max_tokens: int) -> str:
348
+ resp = self.model.generate_content(
349
+ prompt,
350
+ generation_config={"max_output_tokens": max(64, int(max_tokens))},
351
+ )
352
+ text = getattr(resp, "text", None)
353
+ return str(text).strip() if text else ""
354
+
355
+ def summarize(self, text: str, max_tokens: int) -> str:
356
+ try:
357
+ out = self._generate(f"Summarize concisely:\n\n{text}", max_tokens=max_tokens)
358
+ return out if out else text
359
+ except Exception as exc:
360
+ warnings.warn(f"contextpress GeminiBackend.summarize failed: {exc}", stacklevel=2)
361
+ raise
362
+
363
+ def deduplicate(self, turns: list[str]) -> list[int]:
364
+ if len(turns) <= 1:
365
+ return list(range(len(turns)))
366
+ prompt = f"{DEDUP_SYSTEM_PROMPT}\n\n{format_numbered_turns(turns)}"
367
+ try:
368
+ out = self._generate(prompt, max_tokens=128)
369
+ return parse_keep_indices(out, len(turns))
370
+ except Exception as exc:
371
+ warnings.warn(f"contextpress GeminiBackend.deduplicate failed: {exc}", stacklevel=2)
372
+ return list(range(len(turns)))
@@ -60,6 +60,27 @@ class CompressionStats:
60
60
  def tokens_saved(self) -> int:
61
61
  return max(0, self.tokens_before - self.tokens_after)
62
62
 
63
+ def to_dict(self) -> dict[str, Any]:
64
+ """JSON-serializable snapshot of this run."""
65
+ return {
66
+ "turns_before": self.turns_before,
67
+ "turns_after": self.turns_after,
68
+ "turns_removed": self.turns_removed,
69
+ "tokens_before": self.tokens_before,
70
+ "tokens_after": self.tokens_after,
71
+ "tokens_saved": self.tokens_saved,
72
+ "stages_run": list(self.stages_run),
73
+ "turn_delta_by_stage": dict(self.turn_delta_by_stage),
74
+ "llm_tier_applied": self.llm_tier_applied,
75
+ "llm_dedup_turns_before": self.llm_dedup_turns_before,
76
+ "llm_dedup_turns_after": self.llm_dedup_turns_after,
77
+ "compression_level": self.compression_level,
78
+ "context_type": self.context_type,
79
+ "token_budget": self.token_budget,
80
+ "dry_run": self.dry_run,
81
+ "warnings_emitted": list(self.warnings_emitted),
82
+ }
83
+
63
84
 
64
85
  @dataclass
65
86
  class CompressionResult:
@@ -67,3 +88,9 @@ class CompressionResult:
67
88
 
68
89
  messages: Any
69
90
  stats: CompressionStats
91
+
92
+ def to_dict(self, *, include_messages: bool = True) -> dict[str, Any]:
93
+ data = {"stats": self.stats.to_dict()}
94
+ if include_messages:
95
+ data["messages"] = self.messages
96
+ return data
@@ -0,0 +1,23 @@
1
+ """Compare compression presets on the same fixture (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
+ rows = cm.compare_presets(messages, token_budget=200)
15
+
16
+ print(f"{'preset':<8} {'tokens':>12} {'saved':>8} {'turns':>12}")
17
+ for preset, stats in rows.items():
18
+ print(
19
+ f"{preset:<8} "
20
+ f"{stats.tokens_before:>5}->{stats.tokens_after:<5} "
21
+ f"{stats.tokens_saved:>8} "
22
+ f"{stats.turns_before:>5}->{stats.turns_after:<5}"
23
+ )
@@ -0,0 +1,50 @@
1
+ """
2
+ Tier 2 compression with Anthropic Claude.
3
+
4
+ Requires ``pip install anthropic`` and ``ANTHROPIC_API_KEY``.
5
+
6
+ Run:
7
+ set ANTHROPIC_API_KEY=sk-ant-... # Windows
8
+ python examples/llm_tier_claude.py
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import sys
15
+
16
+ from contextpress import ContextManager
17
+ from contextpress.llm.adapters import ClaudeBackend
18
+
19
+
20
+ def main() -> None:
21
+ if not os.environ.get("ANTHROPIC_API_KEY"):
22
+ print("Set ANTHROPIC_API_KEY to run this example.", file=sys.stderr)
23
+ sys.exit(1)
24
+
25
+ messages = [
26
+ {"role": "system", "content": "You are a concise assistant."},
27
+ {"role": "user", "content": "What is Python? " + ("Explain briefly. " * 200)},
28
+ {"role": "assistant", "content": "Python is a programming language. " * 150},
29
+ {"role": "user", "content": "And numpy? " * 80},
30
+ ]
31
+
32
+ backend = ClaudeBackend(model="claude-haiku-4-5")
33
+ cm = ContextManager(
34
+ type="chat",
35
+ llm_backend=backend,
36
+ compression="medium",
37
+ llm_min_input_chars=500,
38
+ llm_max_summary_tokens=512,
39
+ )
40
+
41
+ out = cm.compress(messages, token_budget=2000)
42
+ print("turns in:", len(messages), "out:", len(out))
43
+ for i, m in enumerate(out):
44
+ c = m["content"]
45
+ preview = (c[:200] + "…") if isinstance(c, str) and len(c) > 200 else c
46
+ print(i, m["role"] + ":", preview)
47
+
48
+
49
+ if __name__ == "__main__":
50
+ main()
@@ -0,0 +1,50 @@
1
+ """
2
+ Tier 2 compression with Google Gemini.
3
+
4
+ Requires ``pip install google-generativeai`` and ``GOOGLE_API_KEY``.
5
+
6
+ Run:
7
+ set GOOGLE_API_KEY=... # or GEMINI_API_KEY
8
+ python examples/llm_tier_gemini.py
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import sys
15
+
16
+ from contextpress import ContextManager
17
+ from contextpress.llm.adapters import GeminiBackend
18
+
19
+
20
+ def main() -> None:
21
+ if not (os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY")):
22
+ print("Set GOOGLE_API_KEY or GEMINI_API_KEY to run this example.", file=sys.stderr)
23
+ sys.exit(1)
24
+
25
+ messages = [
26
+ {"role": "system", "content": "You are a concise assistant."},
27
+ {"role": "user", "content": "What is Python? " + ("Explain briefly. " * 200)},
28
+ {"role": "assistant", "content": "Python is a programming language. " * 150},
29
+ {"role": "user", "content": "And numpy? " * 80},
30
+ ]
31
+
32
+ backend = GeminiBackend(model_name="gemini-2.0-flash")
33
+ cm = ContextManager(
34
+ type="chat",
35
+ llm_backend=backend,
36
+ compression="medium",
37
+ llm_min_input_chars=500,
38
+ llm_max_summary_tokens=512,
39
+ )
40
+
41
+ out = cm.compress(messages, token_budget=2000)
42
+ print("turns in:", len(messages), "out:", len(out))
43
+ for i, m in enumerate(out):
44
+ c = m["content"]
45
+ preview = (c[:200] + "…") if isinstance(c, str) and len(c) > 200 else c
46
+ print(i, m["role"] + ":", preview)
47
+
48
+
49
+ if __name__ == "__main__":
50
+ main()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "contextpress"
7
- version = "0.4.0"
7
+ version = "0.5.1"
8
8
  description = "Deterministic context compression for LLM chat, RAG, and agent pipelines"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -44,7 +44,7 @@ dependencies = [
44
44
 
45
45
  [project.optional-dependencies]
46
46
  dev = ["pytest>=7.0", "pytest-cov>=4.0", "black", "ruff"]
47
- llm = ["openai>=1.0", "anthropic>=0.18", "ollama>=0.3.0"]
47
+ llm = ["openai>=1.0", "anthropic>=0.18", "ollama>=0.3.0", "google-generativeai>=0.8.0"]
48
48
 
49
49
  [project.urls]
50
50
  Homepage = "https://github.com/Taha-azizi/contextpress"
@@ -0,0 +1,66 @@
1
+ import asyncio
2
+ from unittest.mock import MagicMock
3
+
4
+ from contextpress import ContextManager
5
+ from contextpress.llm.adapters import GeminiBackend
6
+ from contextpress.stats import CompressionStats
7
+
8
+
9
+ def test_stats_to_dict():
10
+ stats = CompressionStats(
11
+ turns_before=5,
12
+ turns_after=3,
13
+ tokens_before=100,
14
+ tokens_after=60,
15
+ stages_run=["filler", "budget"],
16
+ compression_level="medium",
17
+ )
18
+ d = stats.to_dict()
19
+ assert d["turns_removed"] == 2
20
+ assert d["tokens_saved"] == 40
21
+ assert d["stages_run"] == ["filler", "budget"]
22
+
23
+
24
+ def test_result_to_dict():
25
+ cm = ContextManager()
26
+ result = cm.preview([{"role": "user", "content": "hello basically"}], token_budget=100)
27
+ d = result.to_dict()
28
+ assert "stats" in d and "messages" in d
29
+ assert d["stats"]["dry_run"] is True
30
+
31
+
32
+ def test_compare_presets():
33
+ cm = ContextManager(type="chat")
34
+ messages = [
35
+ {"role": "user", "content": "hello basically there"},
36
+ {"role": "assistant", "content": "sounds good"},
37
+ ]
38
+ rows = cm.compare_presets(messages, token_budget=500)
39
+ assert set(rows) == {"low", "medium", "high"}
40
+ assert all(isinstance(v, CompressionStats) for v in rows.values())
41
+
42
+
43
+ def test_compress_async():
44
+ async def _run() -> list:
45
+ cm = ContextManager(type="chat")
46
+ return await cm.compress_async(
47
+ [{"role": "user", "content": "hello basically"}],
48
+ token_budget=200,
49
+ )
50
+
51
+ out = asyncio.run(_run())
52
+ assert isinstance(out, list)
53
+
54
+
55
+ def test_gemini_backend_summarize():
56
+ model = MagicMock()
57
+ model.generate_content.return_value = MagicMock(text="summary")
58
+ backend = GeminiBackend(model=model)
59
+ assert backend.summarize("long text", 50) == "summary"
60
+
61
+
62
+ def test_gemini_backend_deduplicate():
63
+ model = MagicMock()
64
+ model.generate_content.return_value = MagicMock(text="0, 2")
65
+ backend = GeminiBackend(model=model)
66
+ assert backend.deduplicate(["a", "b", "c", "d"]) == [0, 2]
@@ -0,0 +1,50 @@
1
+ import os
2
+ from unittest.mock import MagicMock, patch
3
+
4
+ import pytest
5
+
6
+ from contextpress.llm.adapters import ClaudeBackend, GeminiBackend
7
+
8
+
9
+ def test_claude_backend_with_client():
10
+ client = MagicMock()
11
+ backend = ClaudeBackend(model="claude-haiku-4-5", client=client)
12
+ assert backend.client is client
13
+ assert backend.model == "claude-haiku-4-5"
14
+
15
+
16
+ def test_claude_backend_requires_api_key():
17
+ mock_anthropic = MagicMock()
18
+ with patch.dict(os.environ, {}, clear=True):
19
+ with patch.dict("sys.modules", {"anthropic": mock_anthropic}):
20
+ with pytest.raises(ValueError, match="ANTHROPIC_API_KEY"):
21
+ ClaudeBackend()
22
+
23
+
24
+ def test_claude_backend_from_api_key():
25
+ mock_module = MagicMock()
26
+ mock_client = MagicMock()
27
+ mock_module.Anthropic.return_value = mock_client
28
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-test"}):
29
+ with patch.dict("sys.modules", {"anthropic": mock_module}):
30
+ backend = ClaudeBackend(model="claude-haiku-4-5")
31
+ assert backend.client is mock_client
32
+
33
+
34
+ def test_gemini_backend_with_model_instance():
35
+ model = MagicMock()
36
+ backend = GeminiBackend(model=model)
37
+ assert backend.model is model
38
+
39
+
40
+ def test_gemini_backend_from_model_name():
41
+ mock_genai = MagicMock()
42
+ mock_model = MagicMock()
43
+ mock_genai.GenerativeModel.return_value = mock_model
44
+ fake_google = MagicMock()
45
+ fake_google.generativeai = mock_genai
46
+ with patch.dict(os.environ, {"GOOGLE_API_KEY": "key-test"}):
47
+ with patch.dict("sys.modules", {"google": fake_google, "google.generativeai": mock_genai}):
48
+ backend = GeminiBackend(model_name="gemini-2.0-flash")
49
+ assert backend.model is mock_model
50
+ mock_genai.configure.assert_called_once_with(api_key="key-test")
File without changes
File without changes
File without changes
File without changes