contextpress 0.5.4__tar.gz → 0.5.6__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.5.6/AGENTS.md +45 -0
- contextpress-0.5.6/AUDIT.md +89 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/CHANGELOG.md +14 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/PKG-INFO +2 -1
- {contextpress-0.5.4 → contextpress-0.5.6}/README.md +1 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/__init__.py +1 -1
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/_bootstrap.py +3 -2
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/pipeline.py +7 -1
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/strategies/budget.py +5 -23
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/strategies/recency.py +2 -10
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/strategies/repetition.py +3 -8
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/strategies/resolution.py +2 -8
- contextpress-0.5.6/contextpress/text_sim.py +32 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/pyproject.toml +2 -2
- contextpress-0.5.6/tests/test_v056.py +39 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/.gitignore +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/CITATION.cff +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/CONTRIBUTING.md +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/LICENSE +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/NOTICE +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/compression.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/core.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/llm/__init__.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/llm/_helpers.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/llm/adapters.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/llm/base.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/models.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/normalizer.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/profiles.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/py.typed +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/registry.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/stats.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/strategies/__init__.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/strategies/base.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/strategies/filler.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/contextpress/warnings_capture.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/examples/agent_pipeline.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/examples/benchmark_presets.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/examples/dry_run_preview.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/examples/estimate_and_stats.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/examples/llm_tier_claude.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/examples/llm_tier_gemini.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/examples/llm_tier_ollama.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/examples/llm_tier_openai.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/examples/pick_preset.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/__init__.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_budget.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_filler.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_llm_helpers.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_models.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_normalizer.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_pipeline.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_recency.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_repetition.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_resolution.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_stats.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_v03.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_v04.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_v05.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_v051.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_v052.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_v053.py +0 -0
- {contextpress-0.5.4 → contextpress-0.5.6}/tests/test_v054.py +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Notes for agents and contributors
|
|
2
|
+
|
|
3
|
+
## Branches
|
|
4
|
+
|
|
5
|
+
- **`dev`** — default working branch; all feature work and fixes land here first.
|
|
6
|
+
- **`main`** — release branch; merge from `dev` when cutting a PyPI release.
|
|
7
|
+
|
|
8
|
+
Always `git checkout dev` before making changes in this repo.
|
|
9
|
+
|
|
10
|
+
## Where things live
|
|
11
|
+
|
|
12
|
+
| Area | Path |
|
|
13
|
+
|------|------|
|
|
14
|
+
| Public API | `contextpress/core.py` → `ContextManager` |
|
|
15
|
+
| I/O formats | `contextpress/normalizer.py` |
|
|
16
|
+
| Stage presets | `contextpress/compression.py` (`STAGE_ORDER`, `VALID_STAGES`) |
|
|
17
|
+
| Tier 1 stages | `contextpress/strategies/*.py` |
|
|
18
|
+
| Tier 2 | `contextpress/llm/` (`LLMBackend`, `adapters.py`) |
|
|
19
|
+
| Custom stages | `contextpress/registry.py` + `ContextManager.register_stage()` |
|
|
20
|
+
| Orchestration | `contextpress/pipeline.py` (behavior contract at top of file) |
|
|
21
|
+
| Profiles | `contextpress/profiles.py` |
|
|
22
|
+
|
|
23
|
+
`contextpress/__init__.py` lazy-loads `ContextManager` so `from contextpress.models import Turn` avoids pulling optional Tier-2 / sumy import chains when possible.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -e ".[dev]"
|
|
29
|
+
pytest tests -q
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Invariants (non-negotiable)
|
|
33
|
+
|
|
34
|
+
See the numbered block at the top of `contextpress/pipeline.py`. Tests assume Tier 1 is deterministic (no live LLM calls in `tests/`).
|
|
35
|
+
|
|
36
|
+
## Stabilization backlog
|
|
37
|
+
|
|
38
|
+
See [`AUDIT.md`](AUDIT.md) for pre-0.6.0 findings (contract gaps, refactors, fixture gaps). Prefer low-risk items from that backlog before adding stages or new dependencies.
|
|
39
|
+
|
|
40
|
+
## Adding a stage
|
|
41
|
+
|
|
42
|
+
1. Subclass `BaseStrategy`, implement `process`.
|
|
43
|
+
2. Register it in `Pipeline._build_strategy`.
|
|
44
|
+
3. Add the name to `compression.STAGE_ORDER` and presets if it should be user-selectable.
|
|
45
|
+
4. Extend `profiles.Profile` / `PROFILES` if it needs per-type defaults.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Pre-0.6.0 stabilization audit (0.5.5)
|
|
2
|
+
|
|
3
|
+
Recorded after the 0.5.x ergonomics arc. This is the Phase 1 deliverable:
|
|
4
|
+
findings + backlog for Phases 2–4. Behavior changes are deferred unless marked
|
|
5
|
+
**fix now**.
|
|
6
|
+
|
|
7
|
+
## Architecture (summary)
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
ContextManager (core.py)
|
|
11
|
+
→ normalize_messages (normalizer.py)
|
|
12
|
+
→ apply_stage_selection (compression.py)
|
|
13
|
+
→ Pipeline.run (pipeline.py)
|
|
14
|
+
→ filler → repetition → resolution → recency → budget
|
|
15
|
+
→ optional Tier 2 LLM
|
|
16
|
+
→ denormalize_output
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Profiles: `chat`, `rag_doc`, `agent`. Tier 1 deps: nltk, scikit-learn, sumy, tiktoken.
|
|
20
|
+
|
|
21
|
+
## Findings
|
|
22
|
+
|
|
23
|
+
### Contract / correctness
|
|
24
|
+
|
|
25
|
+
| ID | Severity | Finding | Action |
|
|
26
|
+
|----|----------|---------|--------|
|
|
27
|
+
| C1 | Medium | Invariant 1 says system turns are untouched; `BudgetStrategy` truncates system as last resort (`budget.py`) | Document in contract (Phase 2) or change behavior later |
|
|
28
|
+
| C2 | Medium | LLM dedup failures in `Pipeline._run_llm_tier` fall back silently (no warning); summarize path warns | Add warning (Phase 2) |
|
|
29
|
+
| C3 | Medium | Filler drops ack phrases like `"sounds good"` before resolution can use them (`filler.py` / `resolution.py`) | Document + fixture test (Phases 3–4) |
|
|
30
|
+
| C4 | Low | `Turn.importance` / `Turn.resolved` set by resolution but unused by pipeline | Leave; optional cleanup later |
|
|
31
|
+
|
|
32
|
+
### Duplication / maintainability (Phase 2)
|
|
33
|
+
|
|
34
|
+
| ID | Finding | Action |
|
|
35
|
+
|----|---------|--------|
|
|
36
|
+
| R1 | TF-IDF + cosine similarity copied in `repetition.py`, `recency.py`, `resolution.py` | Extract shared helper |
|
|
37
|
+
| R2 | Token counting duplicated in `stats.py` and `budget.py` | Budget uses `stats` helpers |
|
|
38
|
+
| R3 | `_bootstrap.py` downloads unused `stopwords`; warning mentions recency (doesn't use NLTK) | Trim packages; fix message |
|
|
39
|
+
| R4 | Strategies use ad hoc `deepcopy` vs `clone_turn` | Prefer `clone_turn` where safe |
|
|
40
|
+
|
|
41
|
+
### Test / fixture gaps (Phases 3–4)
|
|
42
|
+
|
|
43
|
+
| ID | Gap |
|
|
44
|
+
|----|-----|
|
|
45
|
+
| T1 | No assert that input `messages` are immutable after `compress()` |
|
|
46
|
+
| T2 | No end-to-end `rag_doc` / richer `agent` pipeline tests |
|
|
47
|
+
| T3 | Normalizer edge cases: `None`, bad types, tuple skip, LangChain turn-count mismatch |
|
|
48
|
+
| T4 | No offline real-chat fixtures under `tests/fixtures/` |
|
|
49
|
+
| T5 | Cross-stage filler → resolution interaction untested |
|
|
50
|
+
|
|
51
|
+
### Normalizer fragility (document; fix only if fixtures break)
|
|
52
|
+
|
|
53
|
+
| ID | Area |
|
|
54
|
+
|----|------|
|
|
55
|
+
| N1 | LangChain detection heuristic on first list element |
|
|
56
|
+
| N2 | Dict/tuple skips change turn count vs input |
|
|
57
|
+
| N3 | New turns (RESOLVED / LLM summary) lose `_original_dict` keys |
|
|
58
|
+
| N4 | `apply_text_to_turn` only replaces the first text block |
|
|
59
|
+
|
|
60
|
+
### Deferred (not in 0.5.x stabilization)
|
|
61
|
+
|
|
62
|
+
- Resolution / normalizer rewrites
|
|
63
|
+
- New pipeline stages
|
|
64
|
+
- New LLM backends
|
|
65
|
+
- New hard dependencies
|
|
66
|
+
- Changing stage order without a proven bug
|
|
67
|
+
|
|
68
|
+
## Phase plan
|
|
69
|
+
|
|
70
|
+
| Phase | Version | Scope |
|
|
71
|
+
|-------|---------|--------|
|
|
72
|
+
| 1 Analysis | **0.5.5** | This document + backlog — shipped |
|
|
73
|
+
| 2 Refactor | **0.5.6** | R1–R3, C1 doc, C2 warning (no semantic stage changes) — shipped |
|
|
74
|
+
| 3 Fixtures | **0.5.7** | Offline chat samples + smoke/invariant tests |
|
|
75
|
+
| 4 Fixes | **0.5.8** | Bugs proven by fixtures + T1–T5 regression tests |
|
|
76
|
+
|
|
77
|
+
## Invariants checklist (from `pipeline.py`)
|
|
78
|
+
|
|
79
|
+
1. System turns pass through stages untouched — **except** budget last-resort truncation (C1).
|
|
80
|
+
2. Input never mutated — enforced by clones; **needs T1**.
|
|
81
|
+
3. Output format mirrors input — denormalize; weaker after turn collapse.
|
|
82
|
+
4. Last 3 non-system not compressed by recency — unit tested.
|
|
83
|
+
5. Last 2 non-system not removed by budget — unit tested.
|
|
84
|
+
6. Resolution (chat) needs both sides — unit tested; C3 may weaken signals.
|
|
85
|
+
7. Repetition: more recent wins — unit tested.
|
|
86
|
+
8. Tier 1 deterministic — assumed; sumy/NLTK have fallbacks.
|
|
87
|
+
9. LLM failure → Tier 1 + warning — summarize yes; dedup **C2**.
|
|
88
|
+
10. `token_budget=None` skips budget enforcement — OK.
|
|
89
|
+
11–12. Presets + Tier 2 modes — covered by existing tests.
|
|
@@ -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.6] - 2026-07-26
|
|
8
|
+
|
|
9
|
+
- **Stabilization Phase 2** — low-risk refactors (see [`AUDIT.md`](AUDIT.md)):
|
|
10
|
+
- Shared ``tfidf_cosine`` / ``tfidf_similarity_matrix`` in ``contextpress.text_sim``.
|
|
11
|
+
- ``BudgetStrategy`` uses ``stats.get_encoding`` / ``count_turn_tokens``.
|
|
12
|
+
- NLTK bootstrap drops unused ``stopwords``; warning text fixed.
|
|
13
|
+
- Behavior contract clarifies budget may truncate system as last resort.
|
|
14
|
+
- LLM deduplicate failures now emit a warning (invariant 9).
|
|
15
|
+
|
|
16
|
+
## [0.5.5] - 2026-07-26
|
|
17
|
+
|
|
18
|
+
- **Stabilization Phase 1** — codebase audit and prioritized backlog in [`AUDIT.md`](AUDIT.md).
|
|
19
|
+
- No runtime API changes; prep for low-risk refactors (0.5.6) and fixture-driven fixes (0.5.7–0.5.8).
|
|
20
|
+
|
|
7
21
|
## [0.5.4] - 2026-07-25
|
|
8
22
|
|
|
9
23
|
- **`compress_many()`** — batch ``compress()`` over a list of conversations.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contextpress
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
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
|
|
@@ -561,6 +561,7 @@ For academic use, cite this package in your paper’s software or methods sectio
|
|
|
561
561
|
|
|
562
562
|
## Extension and growth
|
|
563
563
|
|
|
564
|
+
- **Stabilization audit** — See [`AUDIT.md`](AUDIT.md) for known contract gaps and the 0.5.5–0.5.8 backlog (refactors, fixtures, fixes).
|
|
564
565
|
- **Custom stages** — Subclass `contextpress.strategies.base.BaseStrategy` and plug in via a custom `Pipeline` subclass or future registry hooks.
|
|
565
566
|
- **Tier 2** — Implement `LLMBackend` (`summarize`, `deduplicate`) for provider-specific semantic compression; failures fall back to Tier 1.
|
|
566
567
|
- **Presets API** — `from contextpress.compression import VALID_STAGES, STAGE_ORDER` for tooling and experiments.
|
|
@@ -320,6 +320,7 @@ For academic use, cite this package in your paper’s software or methods sectio
|
|
|
320
320
|
|
|
321
321
|
## Extension and growth
|
|
322
322
|
|
|
323
|
+
- **Stabilization audit** — See [`AUDIT.md`](AUDIT.md) for known contract gaps and the 0.5.5–0.5.8 backlog (refactors, fixtures, fixes).
|
|
323
324
|
- **Custom stages** — Subclass `contextpress.strategies.base.BaseStrategy` and plug in via a custom `Pipeline` subclass or future registry hooks.
|
|
324
325
|
- **Tier 2** — Implement `LLMBackend` (`summarize`, `deduplicate`) for provider-specific semantic compression; failures fall back to Tier 1.
|
|
325
326
|
- **Presets API** — `from contextpress.compression import VALID_STAGES, STAGE_ORDER` for tooling and experiments.
|
|
@@ -17,7 +17,8 @@ def bootstrap_nltk() -> None:
|
|
|
17
17
|
flag = Path.home() / ".contextpress" / "nltk_ready"
|
|
18
18
|
if flag.exists():
|
|
19
19
|
return
|
|
20
|
-
|
|
20
|
+
# Used by ResolutionStrategy._noun_subject (word_tokenize + pos_tag).
|
|
21
|
+
packages = ["punkt", "averaged_perceptron_tagger"]
|
|
21
22
|
failed: list[str] = []
|
|
22
23
|
for pkg in packages:
|
|
23
24
|
try:
|
|
@@ -27,7 +28,7 @@ def bootstrap_nltk() -> None:
|
|
|
27
28
|
if failed:
|
|
28
29
|
warnings.warn(
|
|
29
30
|
f"contextpress: could not download NLTK data {failed}; "
|
|
30
|
-
"resolution
|
|
31
|
+
"resolution subject extraction may degrade. Check network or set NLTK_DATA.",
|
|
31
32
|
stacklevel=2,
|
|
32
33
|
)
|
|
33
34
|
flag.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
2
|
CONTEXTPRESS BEHAVIOR CONTRACT
|
|
3
3
|
===============================
|
|
4
|
-
1. system turns are ALWAYS passed through
|
|
4
|
+
1. system turns are ALWAYS passed through NLP stages untouched. Budget may
|
|
5
|
+
truncate system content as a last resort when the token cap cannot otherwise
|
|
6
|
+
be met (emits a warning).
|
|
5
7
|
2. Input is NEVER mutated. Always return new objects.
|
|
6
8
|
3. Output format ALWAYS mirrors input format.
|
|
7
9
|
4. Most recent 3 non-system turns are NEVER compressed by Stage 4.
|
|
@@ -203,6 +205,10 @@ class Pipeline:
|
|
|
203
205
|
try:
|
|
204
206
|
keep_idx = self.llm_backend.deduplicate(texts)
|
|
205
207
|
except Exception:
|
|
208
|
+
warnings.warn(
|
|
209
|
+
"contextpress: LLM deduplicate failed; keeping all non-system turns",
|
|
210
|
+
stacklevel=2,
|
|
211
|
+
)
|
|
206
212
|
keep_idx = list(range(len(ns_turns)))
|
|
207
213
|
valid = sorted(
|
|
208
214
|
{
|
|
@@ -6,20 +6,10 @@ import warnings
|
|
|
6
6
|
import tiktoken
|
|
7
7
|
|
|
8
8
|
from contextpress.models import Conversation, Turn
|
|
9
|
-
from contextpress.
|
|
9
|
+
from contextpress.stats import count_turn_tokens, get_encoding
|
|
10
10
|
from contextpress.strategies.base import BaseStrategy
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def _turn_tokens(turn: Turn, encoding: tiktoken.Encoding) -> int:
|
|
14
|
-
role = turn.role
|
|
15
|
-
if isinstance(turn.content, str):
|
|
16
|
-
body = turn.content
|
|
17
|
-
else:
|
|
18
|
-
body = extract_text_for_processing(turn)
|
|
19
|
-
text = f"{role}\n{body}"
|
|
20
|
-
return len(encoding.encode(text))
|
|
21
|
-
|
|
22
|
-
|
|
23
13
|
def _truncate_system_turn(turn: Turn, encoding: tiktoken.Encoding, max_tokens: int) -> Turn:
|
|
24
14
|
if isinstance(turn.content, str):
|
|
25
15
|
tokens = encoding.encode(turn.content)
|
|
@@ -62,20 +52,12 @@ class BudgetStrategy(BaseStrategy):
|
|
|
62
52
|
self.token_budget = int(token_budget)
|
|
63
53
|
self.model = model
|
|
64
54
|
|
|
65
|
-
def _encoding(self) -> tiktoken.Encoding:
|
|
66
|
-
if self.model:
|
|
67
|
-
try:
|
|
68
|
-
return tiktoken.encoding_for_model(self.model)
|
|
69
|
-
except KeyError:
|
|
70
|
-
pass
|
|
71
|
-
return tiktoken.get_encoding("cl100k_base")
|
|
72
|
-
|
|
73
55
|
def process(self, conversation: Conversation) -> Conversation:
|
|
74
|
-
enc = self.
|
|
56
|
+
enc = get_encoding(self.model)
|
|
75
57
|
turns: list[Turn] = [copy.deepcopy(t) for t in conversation.turns]
|
|
76
58
|
|
|
77
59
|
def total_toks(ts: list[Turn]) -> int:
|
|
78
|
-
return sum(
|
|
60
|
+
return sum(count_turn_tokens(t, enc) for t in ts)
|
|
79
61
|
|
|
80
62
|
if total_toks(turns) <= self.token_budget:
|
|
81
63
|
return Conversation(
|
|
@@ -96,7 +78,7 @@ class BudgetStrategy(BaseStrategy):
|
|
|
96
78
|
n_removed += 1
|
|
97
79
|
continue
|
|
98
80
|
|
|
99
|
-
#
|
|
81
|
+
# Last resort: truncate system (see behavior contract note on invariant 1)
|
|
100
82
|
warnings.warn(
|
|
101
83
|
"contextpress: truncating system prompt to satisfy token budget",
|
|
102
84
|
stacklevel=2,
|
|
@@ -104,7 +86,7 @@ class BudgetStrategy(BaseStrategy):
|
|
|
104
86
|
for si, t in enumerate(turns):
|
|
105
87
|
if t.role != "system":
|
|
106
88
|
continue
|
|
107
|
-
others = total_toks(turns) -
|
|
89
|
+
others = total_toks(turns) - count_turn_tokens(t, enc)
|
|
108
90
|
room = max(1, self.token_budget - others)
|
|
109
91
|
turns[si] = _truncate_system_turn(t, enc, room)
|
|
110
92
|
break
|
|
@@ -3,8 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import copy
|
|
4
4
|
import re
|
|
5
5
|
|
|
6
|
-
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
7
|
-
from sklearn.metrics.pairwise import cosine_similarity
|
|
8
6
|
from sumy.nlp.tokenizers import Tokenizer
|
|
9
7
|
from sumy.parsers.plaintext import PlaintextParser
|
|
10
8
|
from sumy.summarizers.lsa import LsaSummarizer
|
|
@@ -12,6 +10,7 @@ from sumy.summarizers.lsa import LsaSummarizer
|
|
|
12
10
|
from contextpress.models import Conversation, Turn
|
|
13
11
|
from contextpress.normalizer import apply_text_to_turn, extract_text_for_processing
|
|
14
12
|
from contextpress.strategies.base import BaseStrategy
|
|
13
|
+
from contextpress.text_sim import tfidf_cosine
|
|
15
14
|
|
|
16
15
|
_SENT_SPLIT = re.compile(r"(?<=[.!?])\s+")
|
|
17
16
|
|
|
@@ -126,11 +125,4 @@ class RecencyStrategy(BaseStrategy):
|
|
|
126
125
|
)
|
|
127
126
|
|
|
128
127
|
def _relevance_score(self, query: str, chunk: str) -> float:
|
|
129
|
-
|
|
130
|
-
return 0.0
|
|
131
|
-
try:
|
|
132
|
-
vec = TfidfVectorizer(min_df=1, max_df=1.0)
|
|
133
|
-
mat = vec.fit_transform([query, chunk])
|
|
134
|
-
return float(cosine_similarity(mat[0:1], mat[1:2])[0, 0])
|
|
135
|
-
except ValueError:
|
|
136
|
-
return 0.0
|
|
128
|
+
return tfidf_cosine(query, chunk)
|
|
@@ -2,12 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
4
|
|
|
5
|
-
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
6
|
-
from sklearn.metrics.pairwise import cosine_similarity
|
|
7
|
-
|
|
8
5
|
from contextpress.models import Conversation, Turn
|
|
9
6
|
from contextpress.normalizer import extract_text_for_processing
|
|
10
7
|
from contextpress.strategies.base import BaseStrategy
|
|
8
|
+
from contextpress.text_sim import tfidf_similarity_matrix
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
def _threshold_for_aggr(aggressiveness: float) -> float:
|
|
@@ -55,12 +53,9 @@ class RepetitionStrategy(BaseStrategy):
|
|
|
55
53
|
g_idx.append(gi)
|
|
56
54
|
if len(texts) < 2:
|
|
57
55
|
return
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
mat = vec.fit_transform(texts)
|
|
61
|
-
except ValueError:
|
|
56
|
+
sim = tfidf_similarity_matrix(texts)
|
|
57
|
+
if sim is None:
|
|
62
58
|
return
|
|
63
|
-
sim = cosine_similarity(mat)
|
|
64
59
|
n = len(g_idx)
|
|
65
60
|
for a in range(n):
|
|
66
61
|
for b in range(a + 1, n):
|
|
@@ -4,12 +4,11 @@ import copy
|
|
|
4
4
|
import re
|
|
5
5
|
|
|
6
6
|
import nltk
|
|
7
|
-
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
8
|
-
from sklearn.metrics.pairwise import cosine_similarity
|
|
9
7
|
|
|
10
8
|
from contextpress.models import Conversation, Turn
|
|
11
9
|
from contextpress.normalizer import extract_text_for_processing
|
|
12
10
|
from contextpress.strategies.base import BaseStrategy
|
|
11
|
+
from contextpress.text_sim import tfidf_cosine
|
|
13
12
|
|
|
14
13
|
LAYER_A_PHRASES = [
|
|
15
14
|
"let's go with",
|
|
@@ -78,12 +77,7 @@ def _find_thread_start(conv: Conversation, res_idx: int, max_lookback: int = 20)
|
|
|
78
77
|
if turns[i].role == "system":
|
|
79
78
|
break
|
|
80
79
|
ttext = extract_text_for_processing(turns[i])
|
|
81
|
-
|
|
82
|
-
vec = TfidfVectorizer(min_df=1, max_df=1.0)
|
|
83
|
-
mat = vec.fit_transform([ttext, res_text])
|
|
84
|
-
sim = cosine_similarity(mat[0:1], mat[1:2])[0, 0]
|
|
85
|
-
except ValueError:
|
|
86
|
-
sim = 0.0
|
|
80
|
+
sim = tfidf_cosine(ttext, res_text)
|
|
87
81
|
if sim >= 0.3:
|
|
88
82
|
start = i
|
|
89
83
|
else:
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Shared TF-IDF cosine helpers for Tier 1 strategies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
8
|
+
from sklearn.metrics.pairwise import cosine_similarity
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def tfidf_cosine(a: str, b: str) -> float:
|
|
12
|
+
"""Cosine similarity of two texts; ``0.0`` if vectors cannot be built."""
|
|
13
|
+
if not a.strip() or not b.strip():
|
|
14
|
+
return 0.0
|
|
15
|
+
try:
|
|
16
|
+
vec = TfidfVectorizer(min_df=1, max_df=1.0)
|
|
17
|
+
mat = vec.fit_transform([a, b])
|
|
18
|
+
return float(cosine_similarity(mat[0:1], mat[1:2])[0, 0])
|
|
19
|
+
except ValueError:
|
|
20
|
+
return 0.0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def tfidf_similarity_matrix(texts: list[str]) -> Any | None:
|
|
24
|
+
"""Pairwise cosine matrix for ``texts``, or ``None`` if unfit."""
|
|
25
|
+
if len(texts) < 2:
|
|
26
|
+
return None
|
|
27
|
+
try:
|
|
28
|
+
vec = TfidfVectorizer(min_df=1, max_df=1.0)
|
|
29
|
+
mat = vec.fit_transform(texts)
|
|
30
|
+
return cosine_similarity(mat)
|
|
31
|
+
except ValueError:
|
|
32
|
+
return None
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "contextpress"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.6"
|
|
8
8
|
description = "Deterministic context compression for LLM chat, RAG, and agent pipelines"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -59,7 +59,7 @@ packages = ["contextpress"]
|
|
|
59
59
|
"contextpress/py.typed" = "contextpress/py.typed"
|
|
60
60
|
|
|
61
61
|
[tool.hatch.build.targets.sdist]
|
|
62
|
-
include = ["/contextpress", "/tests", "/examples", "/README.md", "/LICENSE", "/NOTICE", "/CHANGELOG.md", "/CITATION.cff", "/CONTRIBUTING.md"]
|
|
62
|
+
include = ["/contextpress", "/tests", "/examples", "/README.md", "/LICENSE", "/NOTICE", "/CHANGELOG.md", "/CITATION.cff", "/CONTRIBUTING.md", "/AUDIT.md", "/AGENTS.md"]
|
|
63
63
|
|
|
64
64
|
[tool.black]
|
|
65
65
|
line-length = 100
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from unittest.mock import MagicMock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from contextpress import ContextManager
|
|
6
|
+
from contextpress.text_sim import tfidf_cosine, tfidf_similarity_matrix
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_tfidf_cosine_identical():
|
|
10
|
+
assert tfidf_cosine("hello world", "hello world") == pytest.approx(1.0)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_tfidf_cosine_empty():
|
|
14
|
+
assert tfidf_cosine("", "hello") == 0.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_tfidf_similarity_matrix_shape():
|
|
18
|
+
mat = tfidf_similarity_matrix(["alpha beta", "alpha gamma", "unrelated words here"])
|
|
19
|
+
assert mat is not None
|
|
20
|
+
assert mat.shape == (3, 3)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_llm_dedup_failure_emits_warning():
|
|
24
|
+
backend = MagicMock()
|
|
25
|
+
backend.deduplicate.side_effect = RuntimeError("boom")
|
|
26
|
+
|
|
27
|
+
cm = ContextManager(
|
|
28
|
+
type="chat",
|
|
29
|
+
llm_backend=backend,
|
|
30
|
+
llm_min_input_chars=1,
|
|
31
|
+
llm_mode="dedupe_only",
|
|
32
|
+
)
|
|
33
|
+
messages = [
|
|
34
|
+
{"role": "user", "content": "first turn about databases"},
|
|
35
|
+
{"role": "assistant", "content": "second turn about databases"},
|
|
36
|
+
]
|
|
37
|
+
result = cm.compress(messages, token_budget=None, return_stats=True)
|
|
38
|
+
assert any("LLM deduplicate failed" in w for w in result.stats.warnings_emitted)
|
|
39
|
+
assert len(result.messages) == 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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|