contextops 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,187 @@
1
+ """
2
+ Context Normalizer.
3
+
4
+ Converts raw LLM inputs into the canonical ContextBundle format.
5
+ Supports:
6
+ - OpenAI-style message lists
7
+ - Raw dict lists with type/content
8
+ - Single string inputs (treated as system prompt)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from contextops.core.models import ContextBundle, ContextItem, ContextType
14
+
15
+
16
+ # Maps OpenAI message roles to our ContextType enum
17
+ _ROLE_MAP: dict[str, ContextType] = {
18
+ "system": ContextType.SYSTEM,
19
+ "user": ContextType.MESSAGE,
20
+ "assistant": ContextType.MESSAGE,
21
+ "tool": ContextType.TOOL,
22
+ "function": ContextType.TOOL,
23
+ }
24
+
25
+
26
+ def normalize(raw_input: str | list[dict] | dict) -> ContextBundle:
27
+ """
28
+ Normalize any supported raw input into a ContextBundle.
29
+
30
+ Args:
31
+ raw_input: One of:
32
+ - A plain string (treated as a system prompt)
33
+ - A list of OpenAI-style message dicts
34
+ - A dict with explicit 'messages', 'chunks', 'memory', 'system' keys
35
+
36
+ Returns:
37
+ A ContextBundle with all items normalized.
38
+
39
+ Raises:
40
+ ValueError: If the input format is not recognized.
41
+ """
42
+ if isinstance(raw_input, str):
43
+ return _normalize_string(raw_input)
44
+ elif isinstance(raw_input, list):
45
+ return _normalize_message_list(raw_input)
46
+ elif isinstance(raw_input, dict):
47
+ # Unwrap benchmark-style "input" wrapper if present
48
+ if "input" in raw_input and isinstance(raw_input["input"], dict):
49
+ raw_input = raw_input["input"]
50
+ return _normalize_structured_dict(raw_input)
51
+ else:
52
+ raise ValueError(
53
+ f"Unsupported input type: {type(raw_input).__name__}. "
54
+ "Expected str, list[dict], or dict."
55
+ )
56
+
57
+
58
+ def _normalize_string(text: str) -> ContextBundle:
59
+ """Treat a single string as a system prompt."""
60
+ item = ContextItem(
61
+ type=ContextType.SYSTEM,
62
+ content=text,
63
+ source="raw_string",
64
+ )
65
+ return ContextBundle(items=[item])
66
+
67
+
68
+ def _normalize_message_list(messages: list[dict]) -> ContextBundle:
69
+ """
70
+ Normalize an OpenAI-style message list.
71
+
72
+ Each dict should have at least 'role' and 'content' keys.
73
+ """
74
+ items: list[ContextItem] = []
75
+
76
+ for i, msg in enumerate(messages):
77
+ if not isinstance(msg, dict):
78
+ raise ValueError(f"Message at index {i} is not a dict: {type(msg).__name__}")
79
+
80
+ role = msg.get("role", "user")
81
+ content = msg.get("content", "")
82
+ context_type = _ROLE_MAP.get(role, ContextType.MESSAGE)
83
+
84
+ # Extract source hint if available
85
+ source = msg.get("name") or msg.get("source") or f"message_{i}"
86
+
87
+ item = ContextItem(
88
+ type=context_type,
89
+ content=content if content else "",
90
+ source=source,
91
+ metadata={"role": role, "index": i},
92
+ )
93
+ items.append(item)
94
+
95
+ return ContextBundle(items=items)
96
+
97
+
98
+ def _normalize_structured_dict(data: dict) -> ContextBundle:
99
+ """
100
+ Normalize a structured dict with explicit context sections.
101
+
102
+ Expected keys (all optional):
103
+ - system: str
104
+ - messages: list[dict] with role/content
105
+ - chunks / retrieval: list[str | dict]
106
+ - memory: list[str | dict]
107
+ - tools: list[str | dict]
108
+ """
109
+ items: list[ContextItem] = []
110
+
111
+ # System prompt
112
+ if "system" in data:
113
+ items.append(ContextItem(
114
+ type=ContextType.SYSTEM,
115
+ content=data["system"],
116
+ source="system_prompt",
117
+ ))
118
+
119
+ # Chat messages
120
+ for i, msg in enumerate(data.get("messages", [])):
121
+ if isinstance(msg, str):
122
+ items.append(ContextItem(
123
+ type=ContextType.MESSAGE,
124
+ content=msg,
125
+ source=f"message_{i}",
126
+ ))
127
+ elif isinstance(msg, dict):
128
+ role = msg.get("role", "user")
129
+ items.append(ContextItem(
130
+ type=_ROLE_MAP.get(role, ContextType.MESSAGE),
131
+ content=msg.get("content", ""),
132
+ source=msg.get("source", f"message_{i}"),
133
+ metadata={"role": role, "index": i},
134
+ ))
135
+
136
+ # Retrieval chunks (key can be 'chunks' or 'retrieval')
137
+ chunks = data.get("chunks", data.get("retrieval", []))
138
+ for i, chunk in enumerate(chunks):
139
+ if isinstance(chunk, str):
140
+ items.append(ContextItem(
141
+ type=ContextType.RETRIEVAL,
142
+ content=chunk,
143
+ source=f"chunk_{i}",
144
+ ))
145
+ elif isinstance(chunk, dict):
146
+ items.append(ContextItem(
147
+ type=ContextType.RETRIEVAL,
148
+ content=chunk.get("content", ""),
149
+ source=chunk.get("source", f"chunk_{i}"),
150
+ metadata={k: v for k, v in chunk.items() if k not in ("content", "source")},
151
+ ))
152
+
153
+ # Memory
154
+ for i, mem in enumerate(data.get("memory", [])):
155
+ if isinstance(mem, str):
156
+ items.append(ContextItem(
157
+ type=ContextType.MEMORY,
158
+ content=mem,
159
+ source=f"memory_{i}",
160
+ ))
161
+ elif isinstance(mem, dict):
162
+ items.append(ContextItem(
163
+ type=ContextType.MEMORY,
164
+ content=mem.get("content", ""),
165
+ source=mem.get("source", f"memory_{i}"),
166
+ metadata={k: v for k, v in mem.items() if k not in ("content", "source")},
167
+ ))
168
+
169
+ # Tool outputs
170
+ for i, tool in enumerate(data.get("tools", [])):
171
+ if isinstance(tool, str):
172
+ items.append(ContextItem(
173
+ type=ContextType.TOOL,
174
+ content=tool,
175
+ source=f"tool_{i}",
176
+ ))
177
+ elif isinstance(tool, dict):
178
+ # Tool outputs may use 'content' or 'output' as the text key
179
+ tool_content = tool.get("content", "") or tool.get("output", "")
180
+ items.append(ContextItem(
181
+ type=ContextType.TOOL,
182
+ content=tool_content,
183
+ source=tool.get("source", tool.get("name", f"tool_{i}")),
184
+ metadata={k: v for k, v in tool.items() if k not in ("content", "source", "output", "name")},
185
+ ))
186
+
187
+ return ContextBundle(items=items)
@@ -0,0 +1,272 @@
1
+ Metadata-Version: 2.4
2
+ Name: contextops
3
+ Version: 0.1.0
4
+ Summary: Deterministic context linter for LLM applications — analyze, score, and optimize your LLM context payloads.
5
+ Author: Abhijeet Baug
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Abhijeet777/contextops
8
+ Project-URL: Repository, https://github.com/Abhijeet777/contextops
9
+ Project-URL: Issues, https://github.com/Abhijeet777/contextops/issues
10
+ Project-URL: Documentation, https://github.com/Abhijeet777/contextops#readme
11
+ Project-URL: Changelog, https://github.com/Abhijeet777/contextops/blob/main/CHANGELOG.md
12
+ Keywords: llm,context,observability,rag,token,optimization,linter,ci,deterministic,prompt-engineering
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Classifier: Topic :: Software Development :: Testing
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: tiktoken>=0.5.0
29
+ Requires-Dist: click>=8.0.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == "dev"
32
+ Requires-Dist: pytest-cov; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # ContextOps
36
+
37
+ **The deterministic context linter for LLM applications.**
38
+
39
+ [![PyPI version](https://img.shields.io/pypi/v/contextops.svg)](https://pypi.org/project/contextops/)
40
+ [![Python](https://img.shields.io/pypi/pyversions/contextops.svg)](https://pypi.org/project/contextops/)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
42
+ [![CI](https://img.shields.io/badge/CI-stable-brightgreen.svg)](STABILITY.md)
43
+
44
+ ContextOps analyzes the context fed into your LLM and tells you what's broken — redundant chunks, wasted tokens, structural imbalance — with a **deterministic 0–100 score** and actionable fixes.
45
+
46
+ Think of it as **ESLint for your LLM prompts**.
47
+
48
+ ---
49
+
50
+ ## Why ContextOps?
51
+
52
+ Most LLM applications blindly stuff context into the prompt window. This leads to:
53
+
54
+ - 💸 **Wasted spend** — paying for redundant tokens that don't improve output
55
+ - 🔁 **Silent regressions** — a "small RAG change" floods the context with duplicates
56
+ - 🏗️ **Structural drift** — retrieval chunks slowly dominate the entire prompt
57
+ - 🎯 **No visibility** — teams have no way to measure context quality in CI
58
+
59
+ ContextOps gives you that visibility. It runs in your CI pipeline, scores every context payload, and fails the build if quality degrades.
60
+
61
+ ---
62
+
63
+ ## Quick Start
64
+
65
+ ```bash
66
+ pip install contextops
67
+ ```
68
+
69
+ ### See it in action
70
+
71
+ ```bash
72
+ # Run the built-in demo — instant "wow moment"
73
+ contextops demo
74
+ ```
75
+
76
+ ### Analyze your own context
77
+
78
+ ```bash
79
+ # Full analysis with rich terminal output
80
+ contextops inspect context.json
81
+
82
+ # CI mode: fail if score drops below threshold
83
+ contextops check context.json --min-score 70
84
+
85
+ # Compare two snapshots for regressions
86
+ contextops diff before.json after.json
87
+
88
+ # JSON output for dashboards and automation
89
+ contextops inspect context.json --json-output
90
+ ```
91
+
92
+ ### Python API
93
+
94
+ ```python
95
+ from contextops.api.inspect import inspect_context
96
+
97
+ result = inspect_context({
98
+ "system": "You are a helpful assistant.",
99
+ "chunks": [
100
+ {"content": "Refund policy: 30 days...", "source": "docs/refund.md"},
101
+ {"content": "Refund policy: within 30 days...", "source": "docs/refund.md"},
102
+ ],
103
+ "memory": ["User asked about refunds before."],
104
+ })
105
+
106
+ print(f"Score: {result.score}/100")
107
+ print(f"Wasted tokens: {result.token_breakdown.wasted_tokens}")
108
+ for rec in result.recommendations:
109
+ print(f" → {rec.fix}")
110
+ ```
111
+
112
+ ---
113
+
114
+ ## What It Measures
115
+
116
+ ContextOps computes a **0–100 Context Score** from four independent penalty dimensions:
117
+
118
+ | Dimension | What It Detects | Max Penalty |
119
+ |---|---|---|
120
+ | **Redundancy** | Duplicate / near-duplicate chunks (N-gram + Jaccard) | 30 pts |
121
+ | **Density** | Wasted tokens from structural bloat | 30 pts |
122
+ | **Structure** | Imbalanced type distribution (e.g., retrieval > 70%) | 20 pts |
123
+ | **Concentration** | Source dominance or highly imbalanced chunk distribution | 20 pts |
124
+
125
+ ```
126
+ Context Score = 100 - (Redundancy + Density + Structure + Concentration)
127
+ ```
128
+
129
+ Every penalty maps to a **specific finding** with **token savings** and an **actionable fix**.
130
+
131
+ ---
132
+
133
+ ## CI / CD Integration
134
+
135
+ ### GitHub Actions
136
+
137
+ ```yaml
138
+ name: Context Quality Gate
139
+
140
+ on: [pull_request]
141
+
142
+ jobs:
143
+ context-check:
144
+ runs-on: ubuntu-latest
145
+ steps:
146
+ - uses: actions/checkout@v4
147
+ - uses: actions/setup-python@v5
148
+ with:
149
+ python-version: "3.12"
150
+
151
+ - run: pip install contextops
152
+
153
+ - name: Check context quality
154
+ run: contextops check prompts/context.json --min-score 75
155
+ ```
156
+
157
+ ### Exit Codes
158
+
159
+ | Code | Meaning |
160
+ |---|---|
161
+ | `0` | Score meets threshold — build passes |
162
+ | `1` | Score below threshold — build fails |
163
+
164
+ ### Regression Detection
165
+
166
+ ```bash
167
+ # Save a baseline
168
+ contextops inspect prompts/v1.json --json-output > baseline.json
169
+
170
+ # After changes, compare
171
+ contextops diff baseline.json prompts/v2.json
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Context File Format
177
+
178
+ ContextOps accepts a JSON file with any combination of these keys:
179
+
180
+ ```json
181
+ {
182
+ "system": "Your system prompt here",
183
+ "messages": [
184
+ {"role": "user", "content": "User question"}
185
+ ],
186
+ "chunks": [
187
+ {"content": "Retrieved chunk text", "source": "docs/page.md"}
188
+ ],
189
+ "memory": [
190
+ "Previous conversation context"
191
+ ],
192
+ "tools": [
193
+ {"name": "search_api", "output": "Tool response text"}
194
+ ]
195
+ }
196
+ ```
197
+
198
+ It also accepts raw OpenAI message lists:
199
+
200
+ ```json
201
+ [
202
+ {"role": "system", "content": "You are helpful."},
203
+ {"role": "user", "content": "What is the refund policy?"}
204
+ ]
205
+ ```
206
+
207
+ ---
208
+
209
+ ## CLI Reference
210
+
211
+ | Command | Purpose |
212
+ |---|---|
213
+ | `contextops inspect <file>` | Analyze and display results |
214
+ | `contextops check <file> --min-score N` | CI gate with exit codes |
215
+ | `contextops demo` | Built-in demo context |
216
+ | `contextops stability <file>` | Deterministic stability report |
217
+ | `contextops diff <file_a> <file_b>` | Compare two snapshots |
218
+
219
+ ### Flags
220
+
221
+ | Flag | Commands | Purpose |
222
+ |---|---|---|
223
+ | `--json-output` | inspect, check | Machine-readable JSON output |
224
+ | `--min-score N` | check | Minimum passing score (0–100) |
225
+ | `--model <name>` | inspect, check | Target model for cost estimation |
226
+ | `--explain` | inspect, check | Show detailed penalty reasoning |
227
+ | `--config <file>` | inspect, check | Custom threshold config file |
228
+
229
+ ---
230
+
231
+ ## Design Principles
232
+
233
+ 1. **Deterministic** — Same input → same output. Always. No randomness, no embeddings, no LLM calls.
234
+ 2. **Explainable** — Every penalty maps to a real issue with a token count and a fix.
235
+ 3. **CI-native** — Designed for pipelines first. Exit codes, JSON output, threshold gating.
236
+ 4. **Zero network** — Runs entirely offline. No API keys, no external services.
237
+
238
+ ---
239
+
240
+ ## Stability Contract
241
+
242
+ ContextOps ships with a formal [Stability Contract](STABILITY.md) that guarantees:
243
+
244
+ - **Scoring determinism** — same input always produces the same score
245
+ - **Schema stability** — JSON output fields never change within a major version
246
+ - **Performance bounds** — sub-second for payloads up to 50,000 tokens
247
+ - **Semantic versioning** — scoring formula changes require a major version bump
248
+
249
+ This contract exists so teams can trust ContextOps in production CI pipelines.
250
+
251
+ ---
252
+
253
+ ## Development
254
+
255
+ ```bash
256
+ # Clone and install in dev mode
257
+ git clone https://github.com/Abhijeet777/contextops.git
258
+ cd contextops
259
+ pip install -e ".[dev]"
260
+
261
+ # Run tests
262
+ pytest
263
+
264
+ # Run chaos stress tests
265
+ pytest tests/test_chaos.py -v
266
+ ```
267
+
268
+ ---
269
+
270
+ ## License
271
+
272
+ [MIT](LICENSE)
@@ -0,0 +1,24 @@
1
+ contextops/__init__.py,sha256=40J9Neb2T1yPnISprBxgUDKHnr9XJDvY85Vz5ne6Kh0,88
2
+ contextops/analyzers/__init__.py,sha256=bG7cSV3bZwongV_AIWoVhrXrdSNmuFNGelFaJKHwe00,23
3
+ contextops/analyzers/density.py,sha256=xlIHwgd17rOVWevimRs3fj6vFoUhsimH9oRjw-wIvT4,4838
4
+ contextops/analyzers/redundancy.py,sha256=GpUasQ3zpMn6j091Z3XJL4pcFe8h9W_A-5Exfn6qU4M,13336
5
+ contextops/analyzers/structure.py,sha256=-5Sdsu5KBDxg5MNuX0epwNrx_vgOSOC3OEq9L2zQV8c,4090
6
+ contextops/analyzers/tokens.py,sha256=GUY1V1DTd8go27LNx7HEvmujzl3MKFNb8Feqsj0i0n0,2143
7
+ contextops/api/__init__.py,sha256=vxJNiD7nkMawrrr8Q0A6BE5rUhumj7WA9zsJEJC4gHc,17
8
+ contextops/api/diff.py,sha256=OQGjCdmRmIl2EruJXqgjfYRoY0WnyNGu8uIhvpGv7yo,4457
9
+ contextops/api/inspect.py,sha256=6B9LyK3Fn9L2o--wWy8iFuBKxWqDYxVLON6H53cnkNU,1660
10
+ contextops/api/stability.py,sha256=t25ewU9lmec76kM5WxNYWrZHa6RWvr8GV7dGgGA97_4,9049
11
+ contextops/cli/__init__.py,sha256=S4gk3Xl6GIbvLFlda3yITbWovOuwTu0JViGhtfG1sZo,17
12
+ contextops/cli/main.py,sha256=GJ2q9vRNAfqPSll4TnXyhvwKhRvYmmUTwIN3M4ZNprM,11881
13
+ contextops/cli/renderer.py,sha256=EB4Iu_gqq0GF4ZjKHXDb8rf9NM2LYUZjGYTjWeqy6zU,18391
14
+ contextops/core/__init__.py,sha256=km68QyE3lKvt3nk2S7bxZaTZWxKWB53apJFRoRTTJjo,18
15
+ contextops/core/config.py,sha256=7FL1D5VaYWmTPamz5HRv8LrXmvoWDNtyiECNM14GNq0,1767
16
+ contextops/core/engine.py,sha256=TRG-82FTi-Oymjjoo59q2UIsBxoQvPTdjBxU2QBCfe4,13643
17
+ contextops/core/models.py,sha256=uzjLT3-DiJ9zrShw8EWSY7982RfXc0OJkc5xbZ5tc-Q,8510
18
+ contextops/core/normalizer.py,sha256=2LvJTnpuHTIoPalSwgl4PqlKgsx7FfblNkDU9T6Zd4g,6194
19
+ contextops-0.1.0.dist-info/licenses/LICENSE,sha256=Hr9dxbQeTKHbjT1hjE5Kj_eTjTMlD4YHwirrrKF1opo,1070
20
+ contextops-0.1.0.dist-info/METADATA,sha256=ZY99Q-Gl9pkgWzt0hwNK2jIg3LlrAc25Fr0RalIstn8,8269
21
+ contextops-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
22
+ contextops-0.1.0.dist-info/entry_points.txt,sha256=_ESCua3aXYYCq5qDqRzBEFWGXx-BRk49lNsxydH_HZQ,55
23
+ contextops-0.1.0.dist-info/top_level.txt,sha256=wyZsAyPljX_F4eF9heBKToX1JBMLyZbK1Lirppld3YM,11
24
+ contextops-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ contextops = contextops.cli.main:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abhijeet Baug
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ contextops