contextpress 0.4.0__tar.gz → 0.5.0__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.4.0 → contextpress-0.5.0}/CHANGELOG.md +8 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/PKG-INFO +17 -2
- {contextpress-0.4.0 → contextpress-0.5.0}/README.md +15 -1
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/__init__.py +1 -1
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/core.py +38 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/llm/__init__.py +2 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/llm/adapters.py +47 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/stats.py +27 -0
- contextpress-0.5.0/examples/benchmark_presets.py +23 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/pyproject.toml +2 -2
- contextpress-0.5.0/tests/test_v05.py +66 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/.gitignore +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/CITATION.cff +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/CONTRIBUTING.md +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/LICENSE +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/NOTICE +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/_bootstrap.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/compression.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/llm/_helpers.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/llm/base.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/models.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/normalizer.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/pipeline.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/profiles.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/py.typed +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/registry.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/strategies/__init__.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/strategies/base.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/strategies/budget.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/strategies/filler.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/strategies/recency.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/strategies/repetition.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/strategies/resolution.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/contextpress/warnings_capture.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/examples/dry_run_preview.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/examples/estimate_and_stats.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/examples/llm_tier_ollama.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/examples/llm_tier_openai.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/tests/__init__.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/tests/test_budget.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/tests/test_filler.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/tests/test_llm_helpers.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/tests/test_models.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/tests/test_normalizer.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/tests/test_pipeline.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/tests/test_recency.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/tests/test_repetition.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/tests/test_resolution.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/tests/test_stats.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/tests/test_v03.py +0 -0
- {contextpress-0.4.0 → contextpress-0.5.0}/tests/test_v04.py +0 -0
|
@@ -4,6 +4,14 @@ 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.0] - 2026-07-22
|
|
8
|
+
|
|
9
|
+
- **`compress_async()`** — async wrapper around ``compress()`` for asyncio apps.
|
|
10
|
+
- **`compare_presets()`** — dry-run low/medium/high on the same messages and compare stats.
|
|
11
|
+
- **`CompressionStats.to_dict()` / `CompressionResult.to_dict()`** — JSON-friendly export.
|
|
12
|
+
- **`GeminiBackend`** — Google Gemini Tier 2 adapter (optional ``google-generativeai``).
|
|
13
|
+
- Example: `examples/benchmark_presets.py`.
|
|
14
|
+
|
|
7
15
|
## [0.4.0] - 2026-07-20
|
|
8
16
|
|
|
9
17
|
- **`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.
|
|
3
|
+
Version: 0.5.0
|
|
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`, `AnthropicBackend`, `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
|
|
|
@@ -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`, `AnthropicBackend`, `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
|
|
|
@@ -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,6 @@
|
|
|
1
1
|
from contextpress.llm.adapters import (
|
|
2
2
|
AnthropicBackend,
|
|
3
|
+
GeminiBackend,
|
|
3
4
|
OllamaBackend,
|
|
4
5
|
OpenAIBackend,
|
|
5
6
|
OpenAICompatibleBackend,
|
|
@@ -11,5 +12,6 @@ __all__ = [
|
|
|
11
12
|
"OpenAIBackend",
|
|
12
13
|
"OpenAICompatibleBackend",
|
|
13
14
|
"AnthropicBackend",
|
|
15
|
+
"GeminiBackend",
|
|
14
16
|
"OllamaBackend",
|
|
15
17
|
]
|
|
@@ -262,3 +262,50 @@ class OpenAICompatibleBackend(OpenAIBackend):
|
|
|
262
262
|
client=OpenAI(base_url=base_url, api_key=api_key),
|
|
263
263
|
model=model,
|
|
264
264
|
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class GeminiBackend(LLMBackend):
|
|
268
|
+
"""
|
|
269
|
+
Adapter for Google Gemini via ``google-generativeai``.
|
|
270
|
+
|
|
271
|
+
Requires: ``pip install google-generativeai``
|
|
272
|
+
|
|
273
|
+
Usage::
|
|
274
|
+
|
|
275
|
+
import google.generativeai as genai
|
|
276
|
+
from contextpress.llm.adapters import GeminiBackend
|
|
277
|
+
|
|
278
|
+
genai.configure(api_key=\"...\")
|
|
279
|
+
backend = GeminiBackend(model=genai.GenerativeModel(\"gemini-2.0-flash\"))
|
|
280
|
+
cm = ContextManager(type=\"chat\", llm_backend=backend)
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
def __init__(self, model: Any):
|
|
284
|
+
self.model = model
|
|
285
|
+
|
|
286
|
+
def _generate(self, prompt: str, *, max_tokens: int) -> str:
|
|
287
|
+
resp = self.model.generate_content(
|
|
288
|
+
prompt,
|
|
289
|
+
generation_config={"max_output_tokens": max(64, int(max_tokens))},
|
|
290
|
+
)
|
|
291
|
+
text = getattr(resp, "text", None)
|
|
292
|
+
return str(text).strip() if text else ""
|
|
293
|
+
|
|
294
|
+
def summarize(self, text: str, max_tokens: int) -> str:
|
|
295
|
+
try:
|
|
296
|
+
out = self._generate(f"Summarize concisely:\n\n{text}", max_tokens=max_tokens)
|
|
297
|
+
return out if out else text
|
|
298
|
+
except Exception as exc:
|
|
299
|
+
warnings.warn(f"contextpress GeminiBackend.summarize failed: {exc}", stacklevel=2)
|
|
300
|
+
raise
|
|
301
|
+
|
|
302
|
+
def deduplicate(self, turns: list[str]) -> list[int]:
|
|
303
|
+
if len(turns) <= 1:
|
|
304
|
+
return list(range(len(turns)))
|
|
305
|
+
prompt = f"{DEDUP_SYSTEM_PROMPT}\n\n{format_numbered_turns(turns)}"
|
|
306
|
+
try:
|
|
307
|
+
out = self._generate(prompt, max_tokens=128)
|
|
308
|
+
return parse_keep_indices(out, len(turns))
|
|
309
|
+
except Exception as exc:
|
|
310
|
+
warnings.warn(f"contextpress GeminiBackend.deduplicate failed: {exc}", stacklevel=2)
|
|
311
|
+
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
|
+
)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "contextpress"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
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]
|
|
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
|