contextpress 0.5.6__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.
- {contextpress-0.5.6 → contextpress-0.5.8}/AUDIT.md +12 -3
- {contextpress-0.5.6 → contextpress-0.5.8}/CHANGELOG.md +14 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/PKG-INFO +1 -1
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/__init__.py +1 -1
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/normalizer.py +1 -1
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/strategies/filler.py +20 -2
- {contextpress-0.5.6 → contextpress-0.5.8}/pyproject.toml +1 -1
- contextpress-0.5.8/tests/fixtures/chats/01_filler_heavy.json +14 -0
- contextpress-0.5.8/tests/fixtures/chats/02_resolution_thread.json +16 -0
- contextpress-0.5.8/tests/fixtures/chats/03_repetition.json +13 -0
- contextpress-0.5.8/tests/fixtures/chats/04_long_history.json +20 -0
- contextpress-0.5.8/tests/fixtures/chats/05_agent_tools.json +16 -0
- contextpress-0.5.8/tests/fixtures/chats/06_rag_chunks.json +13 -0
- contextpress-0.5.8/tests/fixtures/chats/07_short_stable.json +9 -0
- contextpress-0.5.8/tests/fixtures/chats/08_mixed_ack_resolution.json +13 -0
- contextpress-0.5.8/tests/fixtures/chats/README.md +15 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_filler.py +17 -0
- contextpress-0.5.8/tests/test_fixture_chats.py +72 -0
- contextpress-0.5.8/tests/test_v058.py +55 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/.gitignore +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/AGENTS.md +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/CITATION.cff +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/CONTRIBUTING.md +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/LICENSE +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/NOTICE +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/README.md +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/_bootstrap.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/compression.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/core.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/llm/__init__.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/llm/_helpers.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/llm/adapters.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/llm/base.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/models.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/pipeline.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/profiles.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/py.typed +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/registry.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/stats.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/strategies/__init__.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/strategies/base.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/strategies/budget.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/strategies/recency.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/strategies/repetition.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/strategies/resolution.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/text_sim.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/contextpress/warnings_capture.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/examples/agent_pipeline.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/examples/benchmark_presets.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/examples/dry_run_preview.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/examples/estimate_and_stats.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/examples/llm_tier_claude.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/examples/llm_tier_gemini.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/examples/llm_tier_ollama.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/examples/llm_tier_openai.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/examples/pick_preset.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/__init__.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_budget.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_llm_helpers.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_models.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_normalizer.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_pipeline.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_recency.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_repetition.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_resolution.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_stats.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_v03.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_v04.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_v05.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_v051.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_v052.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_v053.py +0 -0
- {contextpress-0.5.6 → contextpress-0.5.8}/tests/test_v054.py +0 -0
- {contextpress-0.5.6 → 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 |
|
|
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)
|
|
@@ -71,8 +71,17 @@ Profiles: `chat`, `rag_doc`, `agent`. Tier 1 deps: nltk, scikit-learn, sumy, tik
|
|
|
71
71
|
|-------|---------|--------|
|
|
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
|
-
| 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 |
|
|
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 — 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,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.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
|
+
|
|
15
|
+
## [0.5.7] - 2026-07-26
|
|
16
|
+
|
|
17
|
+
- **Stabilization Phase 3** — offline chat fixtures under `tests/fixtures/chats/` (8 synthetic threads).
|
|
18
|
+
- Fixture smoke tests: compress, input immutability, preview, ``recommend_preset``.
|
|
19
|
+
- Documents AUDIT C3 (filler ack vs resolution) with a dedicated fixture.
|
|
20
|
+
|
|
7
21
|
## [0.5.6] - 2026-07-26
|
|
8
22
|
|
|
9
23
|
- **Stabilization Phase 2** — low-risk refactors (see [`AUDIT.md`](AUDIT.md)):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contextpress
|
|
3
|
-
Version: 0.5.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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:
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "01_filler_heavy",
|
|
3
|
+
"type": "chat",
|
|
4
|
+
"source": "synthetic (ShareGPT-style filler patterns)",
|
|
5
|
+
"messages": [
|
|
6
|
+
{"role": "system", "content": "You are a helpful coding assistant."},
|
|
7
|
+
{"role": "user", "content": "Basically, I was wondering if I should use Postgres or Mongo for my app?"},
|
|
8
|
+
{"role": "assistant", "content": "Honestly, it really depends. Basically Postgres is great for relational data. Just saying."},
|
|
9
|
+
{"role": "user", "content": "Sounds good. What about indexes?"},
|
|
10
|
+
{"role": "assistant", "content": "Yeah, you know, you can add B-tree indexes. Actually, for text search, consider GIN."},
|
|
11
|
+
{"role": "user", "content": "Ok cool. And migrations?"},
|
|
12
|
+
{"role": "assistant", "content": "Basically use Alembic or Flyway. Honestly that should work fine."}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "02_resolution_thread",
|
|
3
|
+
"type": "chat",
|
|
4
|
+
"source": "synthetic (decision / agreement thread)",
|
|
5
|
+
"messages": [
|
|
6
|
+
{"role": "system", "content": "You help teams pick technologies."},
|
|
7
|
+
{"role": "user", "content": "Should we use Postgres or Mongo for the billing service?"},
|
|
8
|
+
{"role": "assistant", "content": "Postgres is a better fit for transactional billing and joins."},
|
|
9
|
+
{"role": "user", "content": "What about schema flexibility?"},
|
|
10
|
+
{"role": "assistant", "content": "JSON columns cover most flexibility needs without giving up ACID."},
|
|
11
|
+
{"role": "user", "content": "Ok let's go with Postgres then."},
|
|
12
|
+
{"role": "assistant", "content": "Agreed. Postgres it is for billing."},
|
|
13
|
+
{"role": "user", "content": "Next: how do we deploy?"},
|
|
14
|
+
{"role": "assistant", "content": "Use blue-green deploys on Kubernetes with a short drain window."}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "03_repetition",
|
|
3
|
+
"type": "chat",
|
|
4
|
+
"source": "synthetic (near-duplicate user/assistant turns)",
|
|
5
|
+
"messages": [
|
|
6
|
+
{"role": "user", "content": "Please explain how Python list comprehensions work with filtering and nesting examples included for beginners."},
|
|
7
|
+
{"role": "assistant", "content": "List comprehensions build lists from iterables: [x for x in items if cond]. Nested ones mirror nested loops and keep filtering close to each level."},
|
|
8
|
+
{"role": "user", "content": "Please explain how Python list comprehensions work with filtering and nesting examples included for beginners please."},
|
|
9
|
+
{"role": "assistant", "content": "List comprehensions build lists from iterables: [x for x in items if cond]. Nested ones mirror nested loops and keep filtering close to each level of iteration."},
|
|
10
|
+
{"role": "user", "content": "Can you show a dict comprehension too?"},
|
|
11
|
+
{"role": "assistant", "content": "Sure: {k: v for k, v in pairs if v is not None}."}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "04_long_history",
|
|
3
|
+
"type": "chat",
|
|
4
|
+
"source": "synthetic (long multi-turn, LMSYS-style length)",
|
|
5
|
+
"messages": [
|
|
6
|
+
{"role": "system", "content": "You are a concise technical assistant for web infrastructure questions."},
|
|
7
|
+
{"role": "user", "content": "Topic A: please explain HTTP in detail including methods, status codes, and common headers used in APIs today."},
|
|
8
|
+
{"role": "assistant", "content": "HTTP is a request-response protocol. Common methods are GET, POST, PUT, PATCH, DELETE. Status codes include 2xx success, 4xx client errors, 5xx server errors. Headers like Content-Type, Authorization, and Cache-Control are ubiquitous."},
|
|
9
|
+
{"role": "user", "content": "Topic B: what is TLS and how does the handshake roughly work for HTTPS websites and APIs?"},
|
|
10
|
+
{"role": "assistant", "content": "TLS encrypts traffic between clients and servers. The handshake negotiates ciphers, authenticates the server certificate, and derives session keys so application data can be encrypted."},
|
|
11
|
+
{"role": "user", "content": "Topic C: explain DNS records people usually need for apps, including A, AAAA, CNAME, and TXT."},
|
|
12
|
+
{"role": "assistant", "content": "DNS maps names to addresses and services. A/AAAA point to IPv4/IPv6, CNAME aliases names, and TXT often stores verification or SPF/DKIM related data."},
|
|
13
|
+
{"role": "user", "content": "Topic D: what problems does a CDN solve and when should I not put everything behind a CDN?"},
|
|
14
|
+
{"role": "assistant", "content": "A CDN caches static and some dynamic content near users, reducing latency and origin load. Avoid caching personalized or rapidly changing private responses without careful cache keys."},
|
|
15
|
+
{"role": "user", "content": "Topic E: summarize REST constraints and how they differ from RPC style APIs in practice."},
|
|
16
|
+
{"role": "assistant", "content": "REST emphasizes resources, uniform interfaces, and hypermedia ideas. RPC centers on procedure calls. In practice many 'REST' APIs are resource-ish HTTP JSON endpoints without full HATEOAS."},
|
|
17
|
+
{"role": "user", "content": "Back to my real question: how do I cache API responses safely for a multi-tenant SaaS with per-user data?"},
|
|
18
|
+
{"role": "assistant", "content": "Use Cache-Control with short TTLs, vary on Authorization or tenant id, prefer ETags for validation, and invalidate on writes. Never share personalized responses across tenants."}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "05_agent_tools",
|
|
3
|
+
"type": "agent",
|
|
4
|
+
"source": "synthetic (tool-using agent thread)",
|
|
5
|
+
"messages": [
|
|
6
|
+
{"role": "system", "content": "You are a deploy agent with tools."},
|
|
7
|
+
{"role": "user", "content": "Is api-v2 on staging?"},
|
|
8
|
+
{"role": "assistant", "content": "Checking status <tool_call> get_deploy_status(api-v2, staging)"},
|
|
9
|
+
{"role": "user", "content": "Tool result: version 2.4.1 healthy"},
|
|
10
|
+
{"role": "assistant", "content": "api-v2 staging is healthy on 2.4.1."},
|
|
11
|
+
{"role": "user", "content": "We've decided on using the new pipeline for deploy."},
|
|
12
|
+
{"role": "assistant", "content": "Confirmed. Scheduling via the new pipeline."},
|
|
13
|
+
{"role": "user", "content": "Any blockers?"},
|
|
14
|
+
{"role": "assistant", "content": "Basically none. Honestly looking good."}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "06_rag_chunks",
|
|
3
|
+
"type": "rag_doc",
|
|
4
|
+
"source": "synthetic (RAG document chunks + query)",
|
|
5
|
+
"messages": [
|
|
6
|
+
{"role": "system", "content": "Answer using the provided document chunks."},
|
|
7
|
+
{"role": "user", "content": "Chunk 1: Contextpress is a Python library for deterministic LLM context compression using classical NLP stages such as filler removal, repetition detection, and token budgets."},
|
|
8
|
+
{"role": "user", "content": "Chunk 2: The package supports chat, rag_doc, and agent profiles. Resolution collapsing is disabled for rag_doc because documents are not speaker dialogues."},
|
|
9
|
+
{"role": "user", "content": "Chunk 3: Optional Tier 2 LLM backends can summarize after Tier 1. Failures fall back to Tier 1 with a warning."},
|
|
10
|
+
{"role": "user", "content": "Chunk 4: Unrelated weather note: rain is expected tomorrow in Seattle with strong winds near the coast."},
|
|
11
|
+
{"role": "user", "content": "Which profiles does contextpress support and is resolution used for rag_doc?"}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "08_mixed_ack_resolution",
|
|
3
|
+
"type": "chat",
|
|
4
|
+
"source": "synthetic (filler ack may remove resolution signal — AUDIT C3)",
|
|
5
|
+
"messages": [
|
|
6
|
+
{"role": "user", "content": "Should we ship Friday or Monday?"},
|
|
7
|
+
{"role": "assistant", "content": "Monday is safer for the release window."},
|
|
8
|
+
{"role": "user", "content": "Sounds good"},
|
|
9
|
+
{"role": "assistant", "content": "Sounds good"},
|
|
10
|
+
{"role": "user", "content": "We'll use Monday then."},
|
|
11
|
+
{"role": "assistant", "content": "Confirmed. Monday release it is."}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Offline chat fixtures (Phase 3)
|
|
2
|
+
|
|
3
|
+
Synthetic conversations inspired by public chat / RAG / agent patterns.
|
|
4
|
+
No network required — used by `tests/test_fixture_chats.py`.
|
|
5
|
+
|
|
6
|
+
| File | Profile | Focus |
|
|
7
|
+
|------|---------|--------|
|
|
8
|
+
| `01_filler_heavy.json` | chat | Filler phrases |
|
|
9
|
+
| `02_resolution_thread.json` | chat | Agreement / RESOLVED |
|
|
10
|
+
| `03_repetition.json` | chat | Near-duplicate turns |
|
|
11
|
+
| `04_long_history.json` | chat | Long multi-topic history |
|
|
12
|
+
| `05_agent_tools.json` | agent | Tool markers |
|
|
13
|
+
| `06_rag_chunks.json` | rag_doc | Chunk + query |
|
|
14
|
+
| `07_short_stable.json` | chat | Minimal change |
|
|
15
|
+
| `08_mixed_ack_resolution.json` | chat | Ack vs resolution (AUDIT C3) |
|
|
@@ -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,72 @@
|
|
|
1
|
+
"""Smoke + invariant tests over offline chat fixtures (Phase 3)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from contextpress import ContextManager
|
|
12
|
+
|
|
13
|
+
FIXTURES = sorted(Path(__file__).parent.joinpath("fixtures", "chats").glob("*.json"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load(path: Path) -> dict:
|
|
17
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parametrize("path", FIXTURES, ids=[p.stem for p in FIXTURES])
|
|
21
|
+
def test_fixture_compress_smoke(path: Path):
|
|
22
|
+
data = _load(path)
|
|
23
|
+
messages = data["messages"]
|
|
24
|
+
cm = ContextManager(type=data.get("type", "chat"), compression="medium")
|
|
25
|
+
result = cm.compress(messages, token_budget=800, return_stats=True)
|
|
26
|
+
assert isinstance(result.messages, list)
|
|
27
|
+
assert result.stats.tokens_after <= result.stats.tokens_before
|
|
28
|
+
assert result.stats.turns_after >= 1 or len(messages) == 0
|
|
29
|
+
assert result.stats.context_type == data.get("type", "chat")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize("path", FIXTURES, ids=[p.stem for p in FIXTURES])
|
|
33
|
+
def test_fixture_input_immutable(path: Path):
|
|
34
|
+
data = _load(path)
|
|
35
|
+
original = copy.deepcopy(data["messages"])
|
|
36
|
+
messages = copy.deepcopy(data["messages"])
|
|
37
|
+
cm = ContextManager(type=data.get("type", "chat"))
|
|
38
|
+
cm.compress(messages, token_budget=400)
|
|
39
|
+
assert messages == original
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.parametrize("path", FIXTURES, ids=[p.stem for p in FIXTURES])
|
|
43
|
+
def test_fixture_preview_unchanged(path: Path):
|
|
44
|
+
data = _load(path)
|
|
45
|
+
messages = data["messages"]
|
|
46
|
+
cm = ContextManager(type=data.get("type", "chat"))
|
|
47
|
+
preview = cm.preview(messages, token_budget=400)
|
|
48
|
+
assert preview.messages == messages
|
|
49
|
+
assert preview.stats.dry_run is True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.parametrize("path", FIXTURES, ids=[p.stem for p in FIXTURES])
|
|
53
|
+
def test_fixture_recommend_preset(path: Path):
|
|
54
|
+
data = _load(path)
|
|
55
|
+
cm = ContextManager(type=data.get("type", "chat"))
|
|
56
|
+
preset = cm.recommend_preset(data["messages"], token_budget=500)
|
|
57
|
+
assert preset in ("low", "medium", "high")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_fixture_catalog_not_empty():
|
|
61
|
+
assert len(FIXTURES) >= 8
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_c3_ack_may_weaken_resolution_signal():
|
|
65
|
+
"""Document AUDIT C3: filler can drop ack-only turns before resolution."""
|
|
66
|
+
data = _load(Path(__file__).parent / "fixtures" / "chats" / "08_mixed_ack_resolution.json")
|
|
67
|
+
cm = ContextManager(type="chat", compression="high")
|
|
68
|
+
result = cm.compress(data["messages"], token_budget=None, return_stats=True)
|
|
69
|
+
# Still a valid pipeline outcome; may or may not emit RESOLVED depending on remaining signals.
|
|
70
|
+
texts = " ".join(str(m.get("content", "")) for m in result.messages)
|
|
71
|
+
assert result.stats.turns_after <= result.stats.turns_before
|
|
72
|
+
assert "Monday" in texts or "RESOLVED" in texts
|
|
@@ -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
|
|
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
|