contextpress 0.6.0__tar.gz → 0.6.2__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.
- {contextpress-0.6.0 → contextpress-0.6.2}/.gitignore +4 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/CHANGELOG.md +15 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/PKG-INFO +22 -1
- {contextpress-0.6.0 → contextpress-0.6.2}/README.md +21 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/ROADMAP.md +3 -3
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/__init__.py +1 -1
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/core.py +13 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/stats.py +63 -0
- contextpress-0.6.2/examples/agent_json_compress.py +46 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/examples/structure_and_cost.py +12 -7
- {contextpress-0.6.0 → contextpress-0.6.2}/pyproject.toml +1 -1
- contextpress-0.6.2/tests/fixtures/chats/09_agent_tool_json.json +22 -0
- contextpress-0.6.2/tests/fixtures/chats/10_agent_repeated_logs.json +22 -0
- contextpress-0.6.2/tests/fixtures/chats/11_agent_mixed.json +32 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/README.md +3 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_fixture_chats.py +1 -1
- contextpress-0.6.2/tests/test_v061.py +55 -0
- contextpress-0.6.2/tests/test_v062.py +120 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/AGENTS.md +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/AUDIT.md +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/CITATION.cff +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/CONTRIBUTING.md +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/LICENSE +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/NOTICE +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/_bootstrap.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/compression.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/costs.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/llm/__init__.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/llm/_helpers.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/llm/adapters.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/llm/base.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/models.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/normalizer.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/pipeline.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/profiles.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/py.typed +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/registry.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/__init__.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/base.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/budget.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/filler.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/recency.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/repetition.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/resolution.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/structure.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/text_sim.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/warnings_capture.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/examples/agent_pipeline.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/examples/benchmark_presets.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/examples/dry_run_preview.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/examples/estimate_and_stats.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/examples/llm_tier_claude.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/examples/llm_tier_gemini.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/examples/llm_tier_ollama.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/examples/llm_tier_openai.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/examples/pick_preset.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/__init__.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/01_filler_heavy.json +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/02_resolution_thread.json +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/03_repetition.json +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/04_long_history.json +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/05_agent_tools.json +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/06_rag_chunks.json +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/07_short_stable.json +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/08_mixed_ack_resolution.json +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_budget.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_filler.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_llm_helpers.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_models.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_normalizer.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_pipeline.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_recency.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_repetition.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_resolution.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_stats.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v03.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v04.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v05.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v051.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v052.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v053.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v054.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v056.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v058.py +0 -0
- {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v060.py +0 -0
|
@@ -4,6 +4,21 @@ 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.6.2] - 2026-07-27
|
|
8
|
+
|
|
9
|
+
- **Agent-oriented fixtures** — three offline agent threads under `tests/fixtures/chats/`
|
|
10
|
+
(large tool JSON, repeated log lines, mixed tool call/result trace).
|
|
11
|
+
- **`CompressionStats.summary()`** / **`CompressionResult.summary()`** — one-line human-readable
|
|
12
|
+
savings report (tokens, stages, optional USD when ``cost_provider`` is set).
|
|
13
|
+
- Example: `examples/agent_json_compress.py`.
|
|
14
|
+
|
|
15
|
+
## [0.6.1] - 2026-07-26
|
|
16
|
+
|
|
17
|
+
- **USD on stats** — ``CompressionStats.attach_cost()`` and optional ``cost_provider`` on
|
|
18
|
+
``ContextManager`` / ``compress()`` fill ``estimated_input_cost_*_usd`` and
|
|
19
|
+
``estimated_cost_saved_usd`` (included in ``to_dict()``).
|
|
20
|
+
- Opt-in only: without ``cost_provider``, cost fields stay ``None``.
|
|
21
|
+
|
|
7
22
|
## [0.6.0] - 2026-07-26
|
|
8
23
|
|
|
9
24
|
- **`structure` stage** — early Tier 1 compaction: JSON minify, whitespace tighten, consecutive log-line dedupe (stdlib only).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contextpress
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
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
|
|
@@ -415,6 +415,27 @@ est = cm.estimate_cost(messages, provider="openai", model="gpt-4o-mini", output_
|
|
|
415
415
|
print(est.total_cost_usd, est.to_dict())
|
|
416
416
|
```
|
|
417
417
|
|
|
418
|
+
**USD on compression stats** (0.6.1+, opt-in):
|
|
419
|
+
|
|
420
|
+
```python
|
|
421
|
+
cm = ContextManager(type="chat", model="gpt-4o-mini", cost_provider="openai")
|
|
422
|
+
result = cm.compress(messages, token_budget=2000, return_stats=True)
|
|
423
|
+
print(result.stats.estimated_input_cost_before_usd)
|
|
424
|
+
print(result.stats.estimated_input_cost_after_usd)
|
|
425
|
+
print(result.stats.estimated_cost_saved_usd)
|
|
426
|
+
# or attach later: result.stats.attach_cost(provider="anthropic", model="claude-haiku-4-5")
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Readable savings report** (0.6.2+):
|
|
430
|
+
|
|
431
|
+
```python
|
|
432
|
+
result = cm.compress(messages, token_budget=2000, return_stats=True)
|
|
433
|
+
print(result.summary())
|
|
434
|
+
# contextpress (chat, medium): 12 -> 8 turns, 842 -> 410 tokens (51.3% saved)
|
|
435
|
+
# stages: structure, filler, repetition, budget
|
|
436
|
+
# est. input cost: $0.000126 -> $0.000061 (saved $0.000065) # when cost_provider set
|
|
437
|
+
```
|
|
438
|
+
|
|
418
439
|
See [`ROADMAP.md`](ROADMAP.md) for positioning vs heavier compression stacks and the 0.6.x plan.
|
|
419
440
|
|
|
420
441
|
## Tier 1 vs Tier 2 (classical NLP vs LLM)
|
|
@@ -174,6 +174,27 @@ est = cm.estimate_cost(messages, provider="openai", model="gpt-4o-mini", output_
|
|
|
174
174
|
print(est.total_cost_usd, est.to_dict())
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
+
**USD on compression stats** (0.6.1+, opt-in):
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
cm = ContextManager(type="chat", model="gpt-4o-mini", cost_provider="openai")
|
|
181
|
+
result = cm.compress(messages, token_budget=2000, return_stats=True)
|
|
182
|
+
print(result.stats.estimated_input_cost_before_usd)
|
|
183
|
+
print(result.stats.estimated_input_cost_after_usd)
|
|
184
|
+
print(result.stats.estimated_cost_saved_usd)
|
|
185
|
+
# or attach later: result.stats.attach_cost(provider="anthropic", model="claude-haiku-4-5")
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Readable savings report** (0.6.2+):
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
result = cm.compress(messages, token_budget=2000, return_stats=True)
|
|
192
|
+
print(result.summary())
|
|
193
|
+
# contextpress (chat, medium): 12 -> 8 turns, 842 -> 410 tokens (51.3% saved)
|
|
194
|
+
# stages: structure, filler, repetition, budget
|
|
195
|
+
# est. input cost: $0.000126 -> $0.000061 (saved $0.000065) # when cost_provider set
|
|
196
|
+
```
|
|
197
|
+
|
|
177
198
|
See [`ROADMAP.md`](ROADMAP.md) for positioning vs heavier compression stacks and the 0.6.x plan.
|
|
178
199
|
|
|
179
200
|
## Tier 1 vs Tier 2 (classical NLP vs LLM)
|
|
@@ -37,8 +37,8 @@ deterministic Tier‑1 NLP for chat / RAG / agent **message histories**, with op
|
|
|
37
37
|
|
|
38
38
|
| Version | Focus |
|
|
39
39
|
|---------|--------|
|
|
40
|
-
| **0.6.0** | `structure` stage + `estimate_cost()` + this roadmap |
|
|
41
|
-
| **0.6.1** | Wire estimated USD into `CompressionStats` / reports |
|
|
42
|
-
| **0.6.2** | Agent-oriented fixtures for JSON/tool payloads;
|
|
40
|
+
| **0.6.0** | `structure` stage + `estimate_cost()` + this roadmap — shipped |
|
|
41
|
+
| **0.6.1** | Wire estimated USD into `CompressionStats` / reports — shipped |
|
|
42
|
+
| **0.6.2** | Agent-oriented fixtures for JSON/tool payloads; `summary()` report — shipped |
|
|
43
43
|
|
|
44
44
|
Stay classical-NLP-first; keep optional LLM extras optional.
|
|
@@ -47,6 +47,7 @@ class ContextManager:
|
|
|
47
47
|
llm_min_input_chars: int = 1500,
|
|
48
48
|
llm_max_summary_tokens: int = 2048,
|
|
49
49
|
llm_mode: str = "replace_all",
|
|
50
|
+
cost_provider: str | None = None,
|
|
50
51
|
):
|
|
51
52
|
if type not in PROFILES:
|
|
52
53
|
raise ValueError(f"unknown context type {type!r}")
|
|
@@ -62,6 +63,8 @@ class ContextManager:
|
|
|
62
63
|
self.llm_min_input_chars = int(llm_min_input_chars)
|
|
63
64
|
self.llm_max_summary_tokens = int(llm_max_summary_tokens)
|
|
64
65
|
self.llm_mode = llm_mode
|
|
66
|
+
# When set, compress(..., return_stats=True) attaches USD fields on stats.
|
|
67
|
+
self.cost_provider = cost_provider
|
|
65
68
|
self._custom_stages: dict[str, StageConfig] = {}
|
|
66
69
|
|
|
67
70
|
def estimate_tokens(self, messages: Any, *, model: str | None = None) -> int:
|
|
@@ -181,12 +184,15 @@ class ContextManager:
|
|
|
181
184
|
disable: list[str] | None = None,
|
|
182
185
|
return_stats: bool = False,
|
|
183
186
|
dry_run: bool = False,
|
|
187
|
+
cost_provider: str | None = None,
|
|
184
188
|
) -> Any | CompressionResult:
|
|
185
189
|
"""Run the pipeline; return value matches input shape (dict list, tuples, strings, etc.).
|
|
186
190
|
|
|
187
191
|
``token_budget`` must be a positive int or None. Unknown keys in ``disable`` are ignored.
|
|
188
192
|
With ``return_stats=True``, returns a ``CompressionResult`` with ``messages`` and ``stats``.
|
|
189
193
|
With ``dry_run=True``, runs Tier 1 only (no LLM calls) and returns the original messages.
|
|
194
|
+
When ``cost_provider`` (or ``self.cost_provider``) is set and stats are returned,
|
|
195
|
+
``stats`` includes approximate input USD before/after compression.
|
|
190
196
|
"""
|
|
191
197
|
if dry_run:
|
|
192
198
|
return_stats = True
|
|
@@ -220,6 +226,9 @@ class ContextManager:
|
|
|
220
226
|
out = pipeline.run(conv, stats=stats, dry_run=dry_run)
|
|
221
227
|
if stats is not None:
|
|
222
228
|
stats.warnings_emitted = captured
|
|
229
|
+
prov = cost_provider if cost_provider is not None else self.cost_provider
|
|
230
|
+
if prov is not None:
|
|
231
|
+
stats.attach_cost(provider=prov, model=self.model or "gpt-4o-mini")
|
|
223
232
|
if dry_run:
|
|
224
233
|
messages_out = denormalize_output(clone_conversation(conv), ctx)
|
|
225
234
|
else:
|
|
@@ -239,6 +248,7 @@ class ContextManager:
|
|
|
239
248
|
disable: list[str] | None = None,
|
|
240
249
|
return_stats: bool = False,
|
|
241
250
|
dry_run: bool = False,
|
|
251
|
+
cost_provider: str | None = None,
|
|
242
252
|
) -> list[Any] | list[CompressionResult]:
|
|
243
253
|
"""Run ``compress()`` on each conversation in ``conversations``."""
|
|
244
254
|
if not isinstance(conversations, list):
|
|
@@ -252,6 +262,7 @@ class ContextManager:
|
|
|
252
262
|
disable=disable,
|
|
253
263
|
return_stats=return_stats,
|
|
254
264
|
dry_run=dry_run,
|
|
265
|
+
cost_provider=cost_provider,
|
|
255
266
|
)
|
|
256
267
|
for messages in conversations
|
|
257
268
|
]
|
|
@@ -266,6 +277,7 @@ class ContextManager:
|
|
|
266
277
|
disable: list[str] | None = None,
|
|
267
278
|
return_stats: bool = False,
|
|
268
279
|
dry_run: bool = False,
|
|
280
|
+
cost_provider: str | None = None,
|
|
269
281
|
) -> Any | CompressionResult:
|
|
270
282
|
"""Async wrapper around ``compress()`` (runs in a worker thread)."""
|
|
271
283
|
return await asyncio.to_thread(
|
|
@@ -277,6 +289,7 @@ class ContextManager:
|
|
|
277
289
|
disable=disable,
|
|
278
290
|
return_stats=return_stats,
|
|
279
291
|
dry_run=dry_run,
|
|
292
|
+
cost_provider=cost_provider,
|
|
280
293
|
)
|
|
281
294
|
|
|
282
295
|
def set_compression(self, compression: str) -> None:
|
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
import tiktoken
|
|
9
9
|
|
|
10
|
+
from contextpress.costs import estimate_token_cost
|
|
10
11
|
from contextpress.models import Conversation, Turn
|
|
11
12
|
from contextpress.normalizer import extract_text_for_processing
|
|
12
13
|
|
|
@@ -51,6 +52,11 @@ class CompressionStats:
|
|
|
51
52
|
token_budget: int | None = None
|
|
52
53
|
dry_run: bool = False
|
|
53
54
|
warnings_emitted: list[str] = field(default_factory=list)
|
|
55
|
+
# Optional USD estimates (0.6.1+); filled when cost_provider is set or via attach_cost()
|
|
56
|
+
cost_provider: str | None = None
|
|
57
|
+
cost_model: str | None = None
|
|
58
|
+
estimated_input_cost_before_usd: float | None = None
|
|
59
|
+
estimated_input_cost_after_usd: float | None = None
|
|
54
60
|
|
|
55
61
|
@property
|
|
56
62
|
def turns_removed(self) -> int:
|
|
@@ -66,6 +72,54 @@ class CompressionStats:
|
|
|
66
72
|
return 0.0
|
|
67
73
|
return round(100.0 * self.tokens_saved / self.tokens_before, 2)
|
|
68
74
|
|
|
75
|
+
@property
|
|
76
|
+
def estimated_cost_saved_usd(self) -> float | None:
|
|
77
|
+
"""Approximate input-USD saved (None if cost was not attached)."""
|
|
78
|
+
before = self.estimated_input_cost_before_usd
|
|
79
|
+
after = self.estimated_input_cost_after_usd
|
|
80
|
+
if before is None or after is None:
|
|
81
|
+
return None
|
|
82
|
+
return round(max(0.0, before - after), 8)
|
|
83
|
+
|
|
84
|
+
def attach_cost(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
provider: str = "openai",
|
|
88
|
+
model: str | None = "gpt-4o-mini",
|
|
89
|
+
) -> CompressionStats:
|
|
90
|
+
"""Fill USD fields from ``tokens_before`` / ``tokens_after``. Returns self."""
|
|
91
|
+
before = estimate_token_cost(self.tokens_before, provider=provider, model=model)
|
|
92
|
+
after = estimate_token_cost(self.tokens_after, provider=provider, model=model)
|
|
93
|
+
self.cost_provider = before.provider
|
|
94
|
+
self.cost_model = before.model
|
|
95
|
+
self.estimated_input_cost_before_usd = before.input_cost_usd
|
|
96
|
+
self.estimated_input_cost_after_usd = after.input_cost_usd
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
def summary(self) -> str:
|
|
100
|
+
"""One-line human-readable report for logs, notebooks, and demos."""
|
|
101
|
+
level = self.compression_level or "default"
|
|
102
|
+
dry = " [dry-run]" if self.dry_run else ""
|
|
103
|
+
lines = [
|
|
104
|
+
(
|
|
105
|
+
f"contextpress ({self.context_type}, {level}){dry}: "
|
|
106
|
+
f"{self.turns_before} -> {self.turns_after} turns, "
|
|
107
|
+
f"{self.tokens_before} -> {self.tokens_after} tokens "
|
|
108
|
+
f"({self.token_savings_pct}% saved)"
|
|
109
|
+
)
|
|
110
|
+
]
|
|
111
|
+
if self.stages_run:
|
|
112
|
+
lines.append(f"stages: {', '.join(self.stages_run)}")
|
|
113
|
+
saved = self.estimated_cost_saved_usd
|
|
114
|
+
before_usd = self.estimated_input_cost_before_usd
|
|
115
|
+
after_usd = self.estimated_input_cost_after_usd
|
|
116
|
+
if saved is not None and before_usd is not None and after_usd is not None:
|
|
117
|
+
lines.append(
|
|
118
|
+
"est. input cost: "
|
|
119
|
+
f"${before_usd:.6f} -> ${after_usd:.6f} (saved ${saved:.6f})"
|
|
120
|
+
)
|
|
121
|
+
return "\n".join(lines)
|
|
122
|
+
|
|
69
123
|
def to_dict(self) -> dict[str, Any]:
|
|
70
124
|
"""JSON-serializable snapshot of this run."""
|
|
71
125
|
return {
|
|
@@ -86,6 +140,11 @@ class CompressionStats:
|
|
|
86
140
|
"token_budget": self.token_budget,
|
|
87
141
|
"dry_run": self.dry_run,
|
|
88
142
|
"warnings_emitted": list(self.warnings_emitted),
|
|
143
|
+
"cost_provider": self.cost_provider,
|
|
144
|
+
"cost_model": self.cost_model,
|
|
145
|
+
"estimated_input_cost_before_usd": self.estimated_input_cost_before_usd,
|
|
146
|
+
"estimated_input_cost_after_usd": self.estimated_input_cost_after_usd,
|
|
147
|
+
"estimated_cost_saved_usd": self.estimated_cost_saved_usd,
|
|
89
148
|
}
|
|
90
149
|
|
|
91
150
|
|
|
@@ -96,6 +155,10 @@ class CompressionResult:
|
|
|
96
155
|
messages: Any
|
|
97
156
|
stats: CompressionStats
|
|
98
157
|
|
|
158
|
+
def summary(self) -> str:
|
|
159
|
+
"""Human-readable report for this compression run."""
|
|
160
|
+
return self.stats.summary()
|
|
161
|
+
|
|
99
162
|
def to_dict(self, *, include_messages: bool = True) -> dict[str, Any]:
|
|
100
163
|
data = {"stats": self.stats.to_dict()}
|
|
101
164
|
if include_messages:
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Agent JSON compression + readable savings report (0.6.2+)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from contextpress import ContextManager
|
|
8
|
+
|
|
9
|
+
payload = {
|
|
10
|
+
"tool": "search_deploys",
|
|
11
|
+
"service": "api-v2",
|
|
12
|
+
"environment": "staging",
|
|
13
|
+
"events": [
|
|
14
|
+
{
|
|
15
|
+
"id": f"evt-{i:03d}",
|
|
16
|
+
"version": f"2.4.{i % 3}",
|
|
17
|
+
"status": "healthy" if i % 2 else "superseded",
|
|
18
|
+
"details": {"replicas": 3, "region": "us-east-1"},
|
|
19
|
+
}
|
|
20
|
+
for i in range(12)
|
|
21
|
+
],
|
|
22
|
+
"meta": {"query_ms": 38, "truncated": False},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
messages = [
|
|
26
|
+
{"role": "system", "content": "You are a deploy agent with tools."},
|
|
27
|
+
{"role": "user", "content": "Summarize recent staging deploys for api-v2."},
|
|
28
|
+
{
|
|
29
|
+
"role": "assistant",
|
|
30
|
+
"content": "Fetching deploy history <tool_call> search_deploys(api-v2, staging)",
|
|
31
|
+
},
|
|
32
|
+
{"role": "user", "content": "Tool result:\n" + json.dumps(payload, indent=2)},
|
|
33
|
+
{"role": "assistant", "content": "Staging has multiple recent releases; latest is healthy."},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
cm = ContextManager(
|
|
37
|
+
type="agent",
|
|
38
|
+
model="gpt-4o-mini",
|
|
39
|
+
compression="medium",
|
|
40
|
+
cost_provider="openai",
|
|
41
|
+
)
|
|
42
|
+
result = cm.compress(messages, token_budget=None, return_stats=True)
|
|
43
|
+
|
|
44
|
+
print(result.summary())
|
|
45
|
+
print()
|
|
46
|
+
print("tool result preview:", result.messages[3]["content"][:120], "...")
|
|
@@ -18,18 +18,23 @@ messages = [
|
|
|
18
18
|
{"role": "assistant", "content": "line\nline\nline\nnext"},
|
|
19
19
|
]
|
|
20
20
|
|
|
21
|
-
cm = ContextManager(
|
|
21
|
+
cm = ContextManager(
|
|
22
|
+
type="agent",
|
|
23
|
+
model="gpt-4o-mini",
|
|
24
|
+
compression="medium",
|
|
25
|
+
cost_provider="openai",
|
|
26
|
+
)
|
|
22
27
|
before = cm.estimate_tokens(messages)
|
|
23
|
-
before_cost = cm.estimate_cost(messages, provider="openai")
|
|
24
28
|
result = cm.compress(messages, token_budget=None, return_stats=True)
|
|
25
|
-
|
|
29
|
+
stats = result.stats
|
|
26
30
|
|
|
27
|
-
print("tokens:", before, "->",
|
|
31
|
+
print("tokens:", before, "->", stats.tokens_after)
|
|
28
32
|
print(
|
|
29
33
|
"est. input USD:",
|
|
30
|
-
f"{
|
|
34
|
+
f"{stats.estimated_input_cost_before_usd:.6f}",
|
|
31
35
|
"->",
|
|
32
|
-
f"{
|
|
36
|
+
f"{stats.estimated_input_cost_after_usd:.6f}",
|
|
37
|
+
f"(saved {stats.estimated_cost_saved_usd:.6f})",
|
|
33
38
|
)
|
|
34
|
-
print("stages:",
|
|
39
|
+
print("stages:", stats.stages_run)
|
|
35
40
|
print("user content:", result.messages[1]["content"][:80])
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "09_agent_tool_json",
|
|
3
|
+
"type": "agent",
|
|
4
|
+
"source": "synthetic (large pretty-printed tool JSON)",
|
|
5
|
+
"messages": [
|
|
6
|
+
{"role": "system", "content": "You are a data agent with search and fetch tools."},
|
|
7
|
+
{"role": "user", "content": "Find recent deploy events for api-v2 on staging."},
|
|
8
|
+
{
|
|
9
|
+
"role": "assistant",
|
|
10
|
+
"content": "Searching deploy logs <tool_call> search_deploys(service=api-v2, env=staging, limit=20)"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"role": "user",
|
|
14
|
+
"content": "Tool result:\n{\n \"service\": \"api-v2\",\n \"environment\": \"staging\",\n \"events\": [\n {\n \"id\": \"evt-001\",\n \"version\": \"2.4.1\",\n \"status\": \"healthy\",\n \"timestamp\": \"2026-07-25T14:22:11Z\",\n \"details\": {\n \"replicas\": 3,\n \"region\": \"us-east-1\",\n \"notes\": \"Rolling update completed without errors\"\n }\n },\n {\n \"id\": \"evt-002\",\n \"version\": \"2.4.0\",\n \"status\": \"superseded\",\n \"timestamp\": \"2026-07-24T09:10:03Z\",\n \"details\": {\n \"replicas\": 3,\n \"region\": \"us-east-1\",\n \"notes\": \"Previous stable release\"\n }\n },\n {\n \"id\": \"evt-003\",\n \"version\": \"2.3.9\",\n \"status\": \"rolled_back\",\n \"timestamp\": \"2026-07-22T18:44:55Z\",\n \"details\": {\n \"replicas\": 2,\n \"region\": \"us-east-1\",\n \"notes\": \"Rollback after health check failure on pod-2\"\n }\n }\n ],\n \"meta\": {\n \"query_ms\": 42,\n \"truncated\": false,\n \"source\": \"deploy-index\"\n }\n}"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"role": "assistant",
|
|
18
|
+
"content": "api-v2 staging is on 2.4.1 (healthy). Prior release 2.4.0 was superseded yesterday."
|
|
19
|
+
},
|
|
20
|
+
{"role": "user", "content": "Any blockers for prod?"}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "10_agent_repeated_logs",
|
|
3
|
+
"type": "agent",
|
|
4
|
+
"source": "synthetic (agent trace with repeated log lines)",
|
|
5
|
+
"messages": [
|
|
6
|
+
{"role": "system", "content": "You are a debugging agent."},
|
|
7
|
+
{"role": "user", "content": "Why is checkout failing on staging?"},
|
|
8
|
+
{
|
|
9
|
+
"role": "assistant",
|
|
10
|
+
"content": "Pulling recent error logs <tool_call> fetch_logs(service=checkout, env=staging, level=error)"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"role": "user",
|
|
14
|
+
"content": "Tool result:\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nWARN checkout: retry attempt 3/3\nWARN checkout: retry attempt 3/3\nWARN checkout: retry attempt 3/3\nINFO checkout: circuit breaker opened\nINFO checkout: circuit breaker opened"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"role": "assistant",
|
|
18
|
+
"content": "Looks like repeated gateway timeouts triggered the circuit breaker."
|
|
19
|
+
},
|
|
20
|
+
{"role": "user", "content": "Summarize the root cause in one sentence."}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "11_agent_mixed",
|
|
3
|
+
"type": "agent",
|
|
4
|
+
"source": "synthetic (mixed user, tool call, tool result, follow-up)",
|
|
5
|
+
"messages": [
|
|
6
|
+
{"role": "system", "content": "You are an ops agent with deploy and status tools."},
|
|
7
|
+
{"role": "user", "content": "We've decided on using the new pipeline for api-v2 staging deploy."},
|
|
8
|
+
{
|
|
9
|
+
"role": "assistant",
|
|
10
|
+
"content": "Acknowledged. Checking current status <tool_call> get_deploy_status(api-v2, staging)"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"role": "user",
|
|
14
|
+
"content": "Tool result: {\"service\":\"api-v2\",\"env\":\"staging\",\"version\":\"2.4.1\",\"healthy\":true,\"pods\":[{\"name\":\"api-v2-0\",\"ready\":true},{\"name\":\"api-v2-1\",\"ready\":true}]}"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"role": "assistant",
|
|
18
|
+
"content": "Staging is healthy on 2.4.1. I can schedule the new pipeline deploy when you confirm."
|
|
19
|
+
},
|
|
20
|
+
{"role": "user", "content": "Go ahead and schedule it for tonight."},
|
|
21
|
+
{
|
|
22
|
+
"role": "assistant",
|
|
23
|
+
"content": "Scheduling deploy <tool_call> schedule_deploy(api-v2, staging, pipeline=new, window=tonight)"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"role": "user",
|
|
27
|
+
"content": "Tool result:\n{\n \"scheduled\": true,\n \"window\": \"2026-07-26T02:00:00Z\",\n \"pipeline\": \"new\",\n \"ticket\": \"DEP-4821\"\n}"
|
|
28
|
+
},
|
|
29
|
+
{"role": "assistant", "content": "Deploy DEP-4821 is scheduled for tonight via the new pipeline."},
|
|
30
|
+
{"role": "user", "content": "Thanks. Basically that's all I needed."}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -13,3 +13,6 @@ No network required — used by `tests/test_fixture_chats.py`.
|
|
|
13
13
|
| `06_rag_chunks.json` | rag_doc | Chunk + query |
|
|
14
14
|
| `07_short_stable.json` | chat | Minimal change |
|
|
15
15
|
| `08_mixed_ack_resolution.json` | chat | Ack vs resolution (AUDIT C3) |
|
|
16
|
+
| `09_agent_tool_json.json` | agent | Large pretty-printed tool JSON |
|
|
17
|
+
| `10_agent_repeated_logs.json` | agent | Repeated log lines in tool output |
|
|
18
|
+
| `11_agent_mixed.json` | agent | Tool call + result + follow-up thread |
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from contextpress import ContextManager
|
|
2
|
+
from contextpress.stats import CompressionStats
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_attach_cost_on_stats():
|
|
6
|
+
stats = CompressionStats(tokens_before=1_000_000, tokens_after=500_000)
|
|
7
|
+
stats.attach_cost(provider="openai", model="gpt-4o-mini")
|
|
8
|
+
assert stats.cost_provider == "openai"
|
|
9
|
+
assert stats.cost_model == "gpt-4o-mini"
|
|
10
|
+
assert stats.estimated_input_cost_before_usd == 0.15
|
|
11
|
+
assert stats.estimated_input_cost_after_usd == 0.075
|
|
12
|
+
assert stats.estimated_cost_saved_usd == 0.075
|
|
13
|
+
d = stats.to_dict()
|
|
14
|
+
assert d["estimated_cost_saved_usd"] == 0.075
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_cost_fields_none_by_default():
|
|
18
|
+
cm = ContextManager(type="chat")
|
|
19
|
+
result = cm.compress(
|
|
20
|
+
[{"role": "user", "content": "hello basically"}],
|
|
21
|
+
token_budget=None,
|
|
22
|
+
return_stats=True,
|
|
23
|
+
)
|
|
24
|
+
assert result.stats.cost_provider is None
|
|
25
|
+
assert result.stats.estimated_cost_saved_usd is None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_compress_attaches_cost_when_provider_set():
|
|
29
|
+
cm = ContextManager(type="chat", model="gpt-4o-mini", cost_provider="openai")
|
|
30
|
+
messages = [{"role": "user", "content": "basically " + ("word " * 80)}]
|
|
31
|
+
result = cm.compress(messages, token_budget=None, return_stats=True)
|
|
32
|
+
assert result.stats.cost_provider == "openai"
|
|
33
|
+
assert result.stats.estimated_input_cost_before_usd is not None
|
|
34
|
+
assert result.stats.estimated_input_cost_after_usd is not None
|
|
35
|
+
assert result.stats.estimated_cost_saved_usd is not None
|
|
36
|
+
assert result.stats.estimated_cost_saved_usd >= 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_compress_cost_provider_kwarg_overrides_default():
|
|
40
|
+
cm = ContextManager(type="chat", model="gpt-4o-mini", cost_provider="openai")
|
|
41
|
+
result = cm.compress(
|
|
42
|
+
[{"role": "user", "content": "hello " * 40}],
|
|
43
|
+
token_budget=None,
|
|
44
|
+
return_stats=True,
|
|
45
|
+
cost_provider="local",
|
|
46
|
+
)
|
|
47
|
+
assert result.stats.cost_provider == "local"
|
|
48
|
+
assert result.stats.estimated_input_cost_before_usd == 0.0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_preview_inherits_cost_provider():
|
|
52
|
+
cm = ContextManager(type="chat", model="gpt-4o-mini", cost_provider="anthropic")
|
|
53
|
+
preview = cm.preview([{"role": "user", "content": "hello " * 20}], token_budget=500)
|
|
54
|
+
assert preview.stats.cost_provider == "anthropic"
|
|
55
|
+
assert preview.stats.estimated_input_cost_before_usd is not None
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""0.6.2 — agent fixtures, stats.summary(), example script."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from contextpress import ContextManager
|
|
9
|
+
from contextpress.stats import CompressionResult, CompressionStats
|
|
10
|
+
|
|
11
|
+
FIXTURES_DIR = Path(__file__).parent / "fixtures" / "chats"
|
|
12
|
+
AGENT_FIXTURES = [
|
|
13
|
+
FIXTURES_DIR / "09_agent_tool_json.json",
|
|
14
|
+
FIXTURES_DIR / "10_agent_repeated_logs.json",
|
|
15
|
+
FIXTURES_DIR / "11_agent_mixed.json",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load(path: Path) -> dict:
|
|
20
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_summary_basic():
|
|
24
|
+
stats = CompressionStats(
|
|
25
|
+
context_type="agent",
|
|
26
|
+
compression_level="medium",
|
|
27
|
+
turns_before=8,
|
|
28
|
+
turns_after=6,
|
|
29
|
+
tokens_before=1000,
|
|
30
|
+
tokens_after=500,
|
|
31
|
+
stages_run=["structure", "filler", "repetition"],
|
|
32
|
+
)
|
|
33
|
+
text = stats.summary()
|
|
34
|
+
assert "agent" in text
|
|
35
|
+
assert "medium" in text
|
|
36
|
+
assert "1000 -> 500 tokens" in text
|
|
37
|
+
assert "50.0% saved" in text
|
|
38
|
+
assert "structure" in text
|
|
39
|
+
assert "est. input cost" not in text
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_summary_includes_cost_when_present():
|
|
43
|
+
stats = CompressionStats(
|
|
44
|
+
context_type="chat",
|
|
45
|
+
compression_level="low",
|
|
46
|
+
turns_before=4,
|
|
47
|
+
turns_after=4,
|
|
48
|
+
tokens_before=200,
|
|
49
|
+
tokens_after=150,
|
|
50
|
+
stages_run=["structure"],
|
|
51
|
+
estimated_input_cost_before_usd=0.000030,
|
|
52
|
+
estimated_input_cost_after_usd=0.0000225,
|
|
53
|
+
)
|
|
54
|
+
text = stats.summary()
|
|
55
|
+
assert "est. input cost:" in text
|
|
56
|
+
assert "saved $0.000008" in text
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_summary_dry_run_flag():
|
|
60
|
+
stats = CompressionStats(
|
|
61
|
+
context_type="chat",
|
|
62
|
+
dry_run=True,
|
|
63
|
+
turns_before=2,
|
|
64
|
+
turns_after=2,
|
|
65
|
+
tokens_before=50,
|
|
66
|
+
tokens_after=50,
|
|
67
|
+
)
|
|
68
|
+
assert "[dry-run]" in stats.summary()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_compression_result_summary_delegates():
|
|
72
|
+
stats = CompressionStats(
|
|
73
|
+
context_type="agent",
|
|
74
|
+
turns_before=3,
|
|
75
|
+
turns_after=3,
|
|
76
|
+
tokens_before=100,
|
|
77
|
+
tokens_after=80,
|
|
78
|
+
)
|
|
79
|
+
result = CompressionResult(messages=[], stats=stats)
|
|
80
|
+
assert result.summary() == stats.summary()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_agent_tool_json_fixture_structure_saves_tokens():
|
|
84
|
+
data = _load(AGENT_FIXTURES[0])
|
|
85
|
+
cm = ContextManager(type="agent", compression="medium")
|
|
86
|
+
result = cm.compress(data["messages"], token_budget=None, return_stats=True)
|
|
87
|
+
assert "structure" in result.stats.stages_run
|
|
88
|
+
assert result.stats.tokens_after < result.stats.tokens_before
|
|
89
|
+
texts = " ".join(str(m.get("content", "")) for m in result.messages)
|
|
90
|
+
assert "tool_call" in texts or "api-v2" in texts
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_agent_repeated_logs_dedupes_lines():
|
|
94
|
+
data = _load(AGENT_FIXTURES[1])
|
|
95
|
+
cm = ContextManager(type="agent", compression="medium")
|
|
96
|
+
result = cm.compress(data["messages"], token_budget=None, return_stats=True)
|
|
97
|
+
assert "structure" in result.stats.stages_run
|
|
98
|
+
tool_turn = next(m for m in result.messages if m.get("role") == "user" and "Tool result" in str(m.get("content", "")))
|
|
99
|
+
content = str(tool_turn["content"])
|
|
100
|
+
assert content.count("payment gateway timeout") == 1
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_agent_mixed_preserves_tool_markers():
|
|
104
|
+
data = _load(AGENT_FIXTURES[2])
|
|
105
|
+
cm = ContextManager(type="agent", compression="high")
|
|
106
|
+
result = cm.compress(data["messages"], token_budget=600, return_stats=True)
|
|
107
|
+
texts = " ".join(str(m.get("content", "")) for m in result.messages)
|
|
108
|
+
assert "tool_call" in texts or "DEP-4821" in texts or "pipeline" in texts
|
|
109
|
+
assert result.stats.tokens_after <= result.stats.tokens_before
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_compress_summary_with_cost_provider():
|
|
113
|
+
cm = ContextManager(type="agent", model="gpt-4o-mini", cost_provider="openai")
|
|
114
|
+
messages = [
|
|
115
|
+
{"role": "user", "content": json.dumps({"hits": list(range(50))}, indent=2)},
|
|
116
|
+
]
|
|
117
|
+
result = cm.compress(messages, token_budget=None, return_stats=True)
|
|
118
|
+
summary = result.summary()
|
|
119
|
+
assert "est. input cost:" in summary
|
|
120
|
+
assert result.stats.estimated_cost_saved_usd is not None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|