contextpress 0.5.7__tar.gz → 0.5.8__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 (74) hide show
  1. {contextpress-0.5.7 → contextpress-0.5.8}/AUDIT.md +11 -2
  2. {contextpress-0.5.7 → contextpress-0.5.8}/CHANGELOG.md +8 -0
  3. {contextpress-0.5.7 → contextpress-0.5.8}/PKG-INFO +1 -1
  4. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/__init__.py +1 -1
  5. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/normalizer.py +1 -1
  6. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/strategies/filler.py +20 -2
  7. {contextpress-0.5.7 → contextpress-0.5.8}/pyproject.toml +1 -1
  8. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_filler.py +17 -0
  9. contextpress-0.5.8/tests/test_v058.py +55 -0
  10. {contextpress-0.5.7 → contextpress-0.5.8}/.gitignore +0 -0
  11. {contextpress-0.5.7 → contextpress-0.5.8}/AGENTS.md +0 -0
  12. {contextpress-0.5.7 → contextpress-0.5.8}/CITATION.cff +0 -0
  13. {contextpress-0.5.7 → contextpress-0.5.8}/CONTRIBUTING.md +0 -0
  14. {contextpress-0.5.7 → contextpress-0.5.8}/LICENSE +0 -0
  15. {contextpress-0.5.7 → contextpress-0.5.8}/NOTICE +0 -0
  16. {contextpress-0.5.7 → contextpress-0.5.8}/README.md +0 -0
  17. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/_bootstrap.py +0 -0
  18. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/compression.py +0 -0
  19. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/core.py +0 -0
  20. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/llm/__init__.py +0 -0
  21. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/llm/_helpers.py +0 -0
  22. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/llm/adapters.py +0 -0
  23. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/llm/base.py +0 -0
  24. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/models.py +0 -0
  25. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/pipeline.py +0 -0
  26. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/profiles.py +0 -0
  27. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/py.typed +0 -0
  28. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/registry.py +0 -0
  29. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/stats.py +0 -0
  30. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/strategies/__init__.py +0 -0
  31. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/strategies/base.py +0 -0
  32. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/strategies/budget.py +0 -0
  33. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/strategies/recency.py +0 -0
  34. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/strategies/repetition.py +0 -0
  35. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/strategies/resolution.py +0 -0
  36. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/text_sim.py +0 -0
  37. {contextpress-0.5.7 → contextpress-0.5.8}/contextpress/warnings_capture.py +0 -0
  38. {contextpress-0.5.7 → contextpress-0.5.8}/examples/agent_pipeline.py +0 -0
  39. {contextpress-0.5.7 → contextpress-0.5.8}/examples/benchmark_presets.py +0 -0
  40. {contextpress-0.5.7 → contextpress-0.5.8}/examples/dry_run_preview.py +0 -0
  41. {contextpress-0.5.7 → contextpress-0.5.8}/examples/estimate_and_stats.py +0 -0
  42. {contextpress-0.5.7 → contextpress-0.5.8}/examples/llm_tier_claude.py +0 -0
  43. {contextpress-0.5.7 → contextpress-0.5.8}/examples/llm_tier_gemini.py +0 -0
  44. {contextpress-0.5.7 → contextpress-0.5.8}/examples/llm_tier_ollama.py +0 -0
  45. {contextpress-0.5.7 → contextpress-0.5.8}/examples/llm_tier_openai.py +0 -0
  46. {contextpress-0.5.7 → contextpress-0.5.8}/examples/pick_preset.py +0 -0
  47. {contextpress-0.5.7 → contextpress-0.5.8}/tests/__init__.py +0 -0
  48. {contextpress-0.5.7 → contextpress-0.5.8}/tests/fixtures/chats/01_filler_heavy.json +0 -0
  49. {contextpress-0.5.7 → contextpress-0.5.8}/tests/fixtures/chats/02_resolution_thread.json +0 -0
  50. {contextpress-0.5.7 → contextpress-0.5.8}/tests/fixtures/chats/03_repetition.json +0 -0
  51. {contextpress-0.5.7 → contextpress-0.5.8}/tests/fixtures/chats/04_long_history.json +0 -0
  52. {contextpress-0.5.7 → contextpress-0.5.8}/tests/fixtures/chats/05_agent_tools.json +0 -0
  53. {contextpress-0.5.7 → contextpress-0.5.8}/tests/fixtures/chats/06_rag_chunks.json +0 -0
  54. {contextpress-0.5.7 → contextpress-0.5.8}/tests/fixtures/chats/07_short_stable.json +0 -0
  55. {contextpress-0.5.7 → contextpress-0.5.8}/tests/fixtures/chats/08_mixed_ack_resolution.json +0 -0
  56. {contextpress-0.5.7 → contextpress-0.5.8}/tests/fixtures/chats/README.md +0 -0
  57. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_budget.py +0 -0
  58. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_fixture_chats.py +0 -0
  59. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_llm_helpers.py +0 -0
  60. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_models.py +0 -0
  61. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_normalizer.py +0 -0
  62. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_pipeline.py +0 -0
  63. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_recency.py +0 -0
  64. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_repetition.py +0 -0
  65. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_resolution.py +0 -0
  66. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_stats.py +0 -0
  67. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_v03.py +0 -0
  68. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_v04.py +0 -0
  69. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_v05.py +0 -0
  70. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_v051.py +0 -0
  71. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_v052.py +0 -0
  72. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_v053.py +0 -0
  73. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_v054.py +0 -0
  74. {contextpress-0.5.7 → contextpress-0.5.8}/tests/test_v056.py +0 -0
@@ -26,7 +26,7 @@ Profiles: `chat`, `rag_doc`, `agent`. Tier 1 deps: nltk, scikit-learn, sumy, tik
26
26
  |----|----------|---------|--------|
27
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
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) |
29
+ | C3 | Low | Filler drops assistant-only ack phrases like `"sounds good"` before resolution (`filler.py` / `resolution.py`) | **By design** fixture `08_*` + Layer-A still collapses; documented |
30
30
  | C4 | Low | `Turn.importance` / `Turn.resolved` set by resolution but unused by pipeline | Leave; optional cleanup later |
31
31
 
32
32
  ### Duplication / maintainability (Phase 2)
@@ -72,7 +72,16 @@ Profiles: `chat`, `rag_doc`, `agent`. Tier 1 deps: nltk, scikit-learn, sumy, tik
72
72
  | 1 Analysis | **0.5.5** | This document + backlog — shipped |
73
73
  | 2 Refactor | **0.5.6** | R1–R3, C1 doc, C2 warning (no semantic stage changes) — shipped |
74
74
  | 3 Fixtures | **0.5.7** | Offline chat samples + smoke/invariant tests — shipped |
75
- | 4 Fixes | **0.5.8** | Bugs proven by fixtures + T1–T5 regression tests |
75
+ | 4 Fixes | **0.5.8** | Bugs proven by fixtures + T1–T5 regression tests — shipped |
76
+
77
+ ### Phase 4 outcomes (0.5.8)
78
+
79
+ | Fix | Detail |
80
+ |-----|--------|
81
+ | Filler punctuation | Cleanup orphan commas / leading punctuation after phrase removal |
82
+ | User filler-only | User turns are not dropped when filler removal empties them |
83
+ | `messages=None` | Raises ``TypeError`` (was silently empty) |
84
+ | Regression tests | Extra keys, rag_doc skips resolution, resolution fixture, ``compress_many`` immutability |
76
85
 
77
86
  ## Invariants checklist (from `pipeline.py`)
78
87
 
@@ -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.8] - 2026-07-26
8
+
9
+ - **Stabilization Phase 4** — fixture-driven fixes:
10
+ - Filler stage cleans orphan commas / leading punctuation after removals.
11
+ - User turns are never dropped when filler removal would empty them.
12
+ - ``compress(None)`` / ``normalize_messages(None)`` raise ``TypeError``.
13
+ - Regression tests for rag_doc (no resolution), resolution fixture, extra dict keys, batch immutability.
14
+
7
15
  ## [0.5.7] - 2026-07-26
8
16
 
9
17
  - **Stabilization Phase 3** — offline chat fixtures under `tests/fixtures/chats/` (8 synthetic threads).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contextpress
3
- Version: 0.5.7
3
+ Version: 0.5.8
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
@@ -16,7 +16,7 @@ __all__ = [
16
16
  "CompressionResult",
17
17
  "CompressionStats",
18
18
  ]
19
- __version__ = "0.5.7"
19
+ __version__ = "0.5.8"
20
20
 
21
21
 
22
22
  def __getattr__(name: str) -> Any:
@@ -133,7 +133,7 @@ def normalize_messages(
133
133
  ctx: dict[str, Any] = {"format": "unknown", "extras": []}
134
134
 
135
135
  if messages is None:
136
- return Conversation(turns=[], type=context_type, metadata={}), ctx
136
+ raise TypeError("contextpress: messages must not be None")
137
137
 
138
138
  # LangChain-style objects (list of objects with .content)
139
139
  if (
@@ -98,13 +98,28 @@ _FILLER_RE = _build_filler_pattern()
98
98
  _ACTUALLY_NO = re.compile(r"^\s*actually\s*,\s*no\b", re.IGNORECASE)
99
99
 
100
100
 
101
+ def _cleanup_after_filler(text: str) -> str:
102
+ """Repair punctuation left behind after filler phrase removal."""
103
+ s = re.sub(r"\s+", " ", text)
104
+ s = re.sub(r"\s*,\s*(?:,\s*)+", ", ", s)
105
+ s = re.sub(r"\s*;\s*(?:;\s*)+", "; ", s)
106
+ s = re.sub(r"\s+([,.;:!?])", r"\1", s)
107
+ s = re.sub(r"^[\s,;:.]+", "", s)
108
+ s = re.sub(r"[,;]\s*$", "", s)
109
+ s = s.strip()
110
+ if s and s[0].islower():
111
+ s = s[0].upper() + s[1:]
112
+ return s
113
+
114
+
101
115
  def _remove_fillers_text(text: str) -> str:
102
116
  if _ACTUALLY_NO.match(text):
103
117
  return text
104
118
  # do not strip "actually" in "actually, no"
105
119
  s = _FILLER_RE.sub("", text)
106
- s = re.sub(r"\s+", " ", s).strip()
107
- return s
120
+ if s == text:
121
+ return text
122
+ return _cleanup_after_filler(s)
108
123
 
109
124
 
110
125
  def _is_acknowledgement_only(text: str) -> bool:
@@ -152,6 +167,9 @@ class FillerStrategy(BaseStrategy):
152
167
 
153
168
  new_text = _remove_fillers_text(text)
154
169
  if not new_text.strip():
170
+ # Never drop user turns entirely (filler-only user messages stay).
171
+ if turn.role == "user":
172
+ new_turns.append(copy.deepcopy(turn))
155
173
  continue
156
174
 
157
175
  if new_text != text:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "contextpress"
7
- version = "0.5.7"
7
+ version = "0.5.8"
8
8
  description = "Deterministic context compression for LLM chat, RAG, and agent pipelines"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -21,6 +21,23 @@ def test_filler_sentence_start():
21
21
  t = Turn(role="assistant", content="Basically, we use Postgres.")
22
22
  out = _run()([t]).turns[0]
23
23
  assert "basically" not in out.content.lower()
24
+ assert not out.content.startswith(",")
25
+ assert out.content.startswith("We")
26
+
27
+
28
+ def test_filler_cleans_orphan_commas():
29
+ t = Turn(role="assistant", content="Yeah, you know, you can add indexes.")
30
+ out = _run()([t]).turns[0]
31
+ assert "you know" not in out.content.lower()
32
+ assert ", ," not in out.content
33
+ assert "Yeah, you can" in out.content
34
+
35
+
36
+ def test_filler_only_user_not_dropped():
37
+ t = Turn(role="user", content="Basically.")
38
+ out = _run()([t]).turns
39
+ assert len(out) == 1
40
+ assert out[0].role == "user"
24
41
 
25
42
 
26
43
  def test_acknowledgement_assistant_dropped():
@@ -0,0 +1,55 @@
1
+ import copy
2
+ import json
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from contextpress import ContextManager
8
+
9
+
10
+ def test_messages_none_raises():
11
+ cm = ContextManager(type="chat")
12
+ with pytest.raises(TypeError, match="must not be None"):
13
+ cm.compress(None)
14
+
15
+
16
+ def test_extra_dict_keys_preserved():
17
+ cm = ContextManager(type="chat")
18
+ messages = [{"role": "user", "content": "hello basically", "name": "alice", "custom": 123}]
19
+ out = cm.compress(messages, token_budget=None)
20
+ assert out[0]["name"] == "alice"
21
+ assert out[0]["custom"] == 123
22
+
23
+
24
+ def test_rag_doc_skips_resolution_stage():
25
+ data = json.loads(
26
+ (Path(__file__).parent / "fixtures" / "chats" / "06_rag_chunks.json").read_text(
27
+ encoding="utf-8"
28
+ )
29
+ )
30
+ cm = ContextManager(type="rag_doc", compression="high")
31
+ result = cm.compress(data["messages"], token_budget=None, return_stats=True)
32
+ assert "resolution" not in result.stats.stages_run
33
+ assert not any("RESOLVED" in str(m.get("content", "")) for m in result.messages)
34
+
35
+
36
+ def test_resolution_fixture_collapses():
37
+ data = json.loads(
38
+ (Path(__file__).parent / "fixtures" / "chats" / "02_resolution_thread.json").read_text(
39
+ encoding="utf-8"
40
+ )
41
+ )
42
+ cm = ContextManager(type="chat", compression="high")
43
+ result = cm.compress(data["messages"], token_budget=None, return_stats=True)
44
+ assert any("RESOLVED" in str(m.get("content", "")) for m in result.messages)
45
+
46
+
47
+ def test_compress_many_immutability():
48
+ batches = [
49
+ [{"role": "user", "content": "hello basically"}],
50
+ [{"role": "user", "content": "thanks basically"}],
51
+ ]
52
+ original = copy.deepcopy(batches)
53
+ cm = ContextManager(type="chat")
54
+ cm.compress_many(batches, token_budget=200)
55
+ assert batches == original
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes