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.
- contextops/__init__.py +3 -0
- contextops/analyzers/__init__.py +1 -0
- contextops/analyzers/density.py +146 -0
- contextops/analyzers/redundancy.py +362 -0
- contextops/analyzers/structure.py +123 -0
- contextops/analyzers/tokens.py +76 -0
- contextops/api/__init__.py +1 -0
- contextops/api/diff.py +124 -0
- contextops/api/inspect.py +52 -0
- contextops/api/stability.py +264 -0
- contextops/cli/__init__.py +1 -0
- contextops/cli/main.py +320 -0
- contextops/cli/renderer.py +424 -0
- contextops/core/__init__.py +1 -0
- contextops/core/config.py +61 -0
- contextops/core/engine.py +355 -0
- contextops/core/models.py +245 -0
- contextops/core/normalizer.py +187 -0
- contextops-0.1.0.dist-info/METADATA +272 -0
- contextops-0.1.0.dist-info/RECORD +24 -0
- contextops-0.1.0.dist-info/WHEEL +5 -0
- contextops-0.1.0.dist-info/entry_points.txt +2 -0
- contextops-0.1.0.dist-info/licenses/LICENSE +21 -0
- contextops-0.1.0.dist-info/top_level.txt +1 -0
contextops/cli/main.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ContextOps CLI.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
contextops inspect <file> — Analyze a context file and display results
|
|
6
|
+
contextops check <file> — CI mode: fail if score below threshold
|
|
7
|
+
contextops demo — Run analysis on a built-in demo context
|
|
8
|
+
|
|
9
|
+
The CLI is a view layer. JSON is the source of truth.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
|
|
20
|
+
from contextops.api.inspect import inspect_context
|
|
21
|
+
from contextops.api.stability import run_stability_report
|
|
22
|
+
from contextops.api.diff import diff_contexts
|
|
23
|
+
from contextops.cli.renderer import render, render_stability, render_diff
|
|
24
|
+
from contextops.core.config import ContextOpsConfig
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _build_config(
|
|
28
|
+
config_file: str | None,
|
|
29
|
+
retrieval_max_ratio: float | None,
|
|
30
|
+
system_max_ratio: float | None,
|
|
31
|
+
memory_max_ratio: float | None,
|
|
32
|
+
tool_max_ratio: float | None,
|
|
33
|
+
) -> ContextOpsConfig:
|
|
34
|
+
if config_file:
|
|
35
|
+
config = ContextOpsConfig.from_file(config_file)
|
|
36
|
+
else:
|
|
37
|
+
config = ContextOpsConfig.default()
|
|
38
|
+
|
|
39
|
+
# CLI overrides
|
|
40
|
+
has_override = False
|
|
41
|
+
if retrieval_max_ratio is not None:
|
|
42
|
+
config.retrieval_max_ratio = retrieval_max_ratio
|
|
43
|
+
has_override = True
|
|
44
|
+
if system_max_ratio is not None:
|
|
45
|
+
config.system_max_ratio = system_max_ratio
|
|
46
|
+
has_override = True
|
|
47
|
+
if memory_max_ratio is not None:
|
|
48
|
+
config.memory_max_ratio = memory_max_ratio
|
|
49
|
+
has_override = True
|
|
50
|
+
if tool_max_ratio is not None:
|
|
51
|
+
config.tool_max_ratio = tool_max_ratio
|
|
52
|
+
has_override = True
|
|
53
|
+
|
|
54
|
+
if has_override:
|
|
55
|
+
config.mode = "custom"
|
|
56
|
+
|
|
57
|
+
return config
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@click.group()
|
|
61
|
+
@click.version_option(version="0.1.0", prog_name="contextops")
|
|
62
|
+
def cli() -> None:
|
|
63
|
+
"""ContextOps — Context observability for LLM applications."""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@cli.command()
|
|
68
|
+
@click.argument("file", type=click.Path(exists=True), required=False)
|
|
69
|
+
@click.option("--json-output", is_flag=True, help="Output raw JSON instead of pretty format")
|
|
70
|
+
@click.option("--model", default="gpt-4o", help="Model for token encoding (default: gpt-4o)")
|
|
71
|
+
@click.option("--config", help="Path to JSON config file")
|
|
72
|
+
@click.option("--retrieval-max-ratio", type=float, help="Override retrieval max ratio")
|
|
73
|
+
@click.option("--system-max-ratio", type=float, help="Override system max ratio")
|
|
74
|
+
@click.option("--memory-max-ratio", type=float, help="Override memory max ratio")
|
|
75
|
+
@click.option("--tool-max-ratio", type=float, help="Override tool max ratio")
|
|
76
|
+
@click.option("--explain", is_flag=True, help="Show detailed reasoning for the penalties (Top Score Drivers)")
|
|
77
|
+
def inspect(
|
|
78
|
+
file: str | None,
|
|
79
|
+
json_output: bool,
|
|
80
|
+
model: str,
|
|
81
|
+
config: str | None,
|
|
82
|
+
retrieval_max_ratio: float | None,
|
|
83
|
+
system_max_ratio: float | None,
|
|
84
|
+
memory_max_ratio: float | None,
|
|
85
|
+
tool_max_ratio: float | None,
|
|
86
|
+
explain: bool,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Analyze a context file and display results.
|
|
89
|
+
|
|
90
|
+
FILE should be a JSON file containing LLM context in any supported format:
|
|
91
|
+
- OpenAI message list
|
|
92
|
+
- Structured dict with system/messages/chunks/memory/tools keys
|
|
93
|
+
"""
|
|
94
|
+
cfg = _build_config(
|
|
95
|
+
config, retrieval_max_ratio, system_max_ratio, memory_max_ratio, tool_max_ratio
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
raw_input: dict | list
|
|
99
|
+
if file is None:
|
|
100
|
+
# No file provided — run demo
|
|
101
|
+
raw_input = _get_demo_context()
|
|
102
|
+
click.echo(click.style("\n [i] No file provided - running demo context\n", fg="cyan"))
|
|
103
|
+
else:
|
|
104
|
+
raw_input = _load_file(file)
|
|
105
|
+
|
|
106
|
+
result = inspect_context(raw_input, model=model, config=cfg)
|
|
107
|
+
output = render(result, use_json=json_output, explain=explain)
|
|
108
|
+
_safe_echo(output)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@cli.command()
|
|
112
|
+
@click.argument("file", type=click.Path(exists=True))
|
|
113
|
+
@click.option("--min-score", type=int, required=True, help="Minimum passing score (0-100)")
|
|
114
|
+
@click.option("--model", default="gpt-4o", help="Model for token encoding (default: gpt-4o)")
|
|
115
|
+
@click.option("--json-output", is_flag=True, help="Output JSON instead of pretty format")
|
|
116
|
+
@click.option("--config", help="Path to JSON config file")
|
|
117
|
+
@click.option("--retrieval-max-ratio", type=float, help="Override retrieval max ratio")
|
|
118
|
+
@click.option("--system-max-ratio", type=float, help="Override system max ratio")
|
|
119
|
+
@click.option("--memory-max-ratio", type=float, help="Override memory max ratio")
|
|
120
|
+
@click.option("--tool-max-ratio", type=float, help="Override tool max ratio")
|
|
121
|
+
@click.option("--explain", is_flag=True, help="Show detailed reasoning for the penalties (Top Score Drivers)")
|
|
122
|
+
def check(
|
|
123
|
+
file: str,
|
|
124
|
+
min_score: int,
|
|
125
|
+
model: str,
|
|
126
|
+
json_output: bool,
|
|
127
|
+
config: str | None,
|
|
128
|
+
retrieval_max_ratio: float | None,
|
|
129
|
+
system_max_ratio: float | None,
|
|
130
|
+
memory_max_ratio: float | None,
|
|
131
|
+
tool_max_ratio: float | None,
|
|
132
|
+
explain: bool,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""CI mode: fail if context score is below threshold.
|
|
135
|
+
|
|
136
|
+
Returns exit code 0 if score >= min_score, exit code 1 otherwise.
|
|
137
|
+
Designed for CI/CD pipelines.
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
contextops check context.json --min-score 70
|
|
141
|
+
"""
|
|
142
|
+
cfg = _build_config(
|
|
143
|
+
config, retrieval_max_ratio, system_max_ratio, memory_max_ratio, tool_max_ratio
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
raw_input = _load_file(file)
|
|
147
|
+
result = inspect_context(raw_input, model=model, config=cfg)
|
|
148
|
+
|
|
149
|
+
if json_output:
|
|
150
|
+
output = render(result, use_json=True, explain=explain)
|
|
151
|
+
else:
|
|
152
|
+
output = render(result, use_json=False, explain=explain)
|
|
153
|
+
|
|
154
|
+
click.echo(output)
|
|
155
|
+
|
|
156
|
+
if result.score >= min_score:
|
|
157
|
+
click.echo(click.style(
|
|
158
|
+
f"\n PASS: score {result.score} >= {min_score}\n",
|
|
159
|
+
fg="green", bold=True
|
|
160
|
+
))
|
|
161
|
+
sys.exit(0)
|
|
162
|
+
else:
|
|
163
|
+
click.echo(click.style(
|
|
164
|
+
f"\n FAIL: score {result.score} < {min_score}\n",
|
|
165
|
+
fg="red", bold=True
|
|
166
|
+
))
|
|
167
|
+
sys.exit(1)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@cli.command()
|
|
171
|
+
@click.option("--json-output", is_flag=True, help="Output raw JSON")
|
|
172
|
+
def demo(json_output: bool) -> None:
|
|
173
|
+
"""Run analysis on a built-in demo context that shows the wow moment."""
|
|
174
|
+
raw_input = _get_demo_context()
|
|
175
|
+
result = inspect_context(raw_input)
|
|
176
|
+
output = render(result, use_json=json_output, explain=False)
|
|
177
|
+
_safe_echo(output)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@cli.command()
|
|
181
|
+
@click.argument("file", type=click.Path(exists=True), required=False)
|
|
182
|
+
def stability(file: str | None) -> None:
|
|
183
|
+
"""Run a deterministic stability report on the context scoring engine."""
|
|
184
|
+
raw_input: dict | list
|
|
185
|
+
if file is None:
|
|
186
|
+
raw_input = _get_demo_context()
|
|
187
|
+
click.echo(click.style("\n [i] No file provided - running demo context\n", fg="cyan"))
|
|
188
|
+
else:
|
|
189
|
+
raw_input = _load_file(file)
|
|
190
|
+
|
|
191
|
+
report = run_stability_report(raw_input)
|
|
192
|
+
output = render_stability(report)
|
|
193
|
+
_safe_echo(output)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@cli.command()
|
|
197
|
+
@click.argument("file_a", type=click.Path(exists=True))
|
|
198
|
+
@click.argument("file_b", type=click.Path(exists=True))
|
|
199
|
+
def diff(file_a: str, file_b: str) -> None:
|
|
200
|
+
"""Compare two context analysis outputs to visualize changes."""
|
|
201
|
+
raw_a = _load_file(file_a)
|
|
202
|
+
raw_b = _load_file(file_b)
|
|
203
|
+
|
|
204
|
+
result = diff_contexts(raw_a, raw_b)
|
|
205
|
+
output = render_diff(result)
|
|
206
|
+
_safe_echo(output)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ── Helpers ─────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _load_file(filepath: str) -> dict | list:
|
|
213
|
+
"""Load a JSON context file."""
|
|
214
|
+
path = Path(filepath)
|
|
215
|
+
try:
|
|
216
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
217
|
+
data = json.load(f)
|
|
218
|
+
except json.JSONDecodeError as e:
|
|
219
|
+
click.echo(click.style(f"Error: Invalid JSON in {filepath}: {e}", fg="red"), err=True)
|
|
220
|
+
sys.exit(2)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
click.echo(click.style(f"Error reading {filepath}: {e}", fg="red"), err=True)
|
|
223
|
+
sys.exit(2)
|
|
224
|
+
return data
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _safe_echo(text: str) -> None:
|
|
228
|
+
"""Print text handling Windows console encoding issues."""
|
|
229
|
+
try:
|
|
230
|
+
click.echo(text)
|
|
231
|
+
except UnicodeEncodeError:
|
|
232
|
+
# Fallback: replace unencodable chars
|
|
233
|
+
encoded = text.encode(sys.stdout.encoding or "utf-8", errors="replace")
|
|
234
|
+
sys.stdout.buffer.write(encoded)
|
|
235
|
+
sys.stdout.buffer.write(b"\n")
|
|
236
|
+
sys.stdout.buffer.flush()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _get_demo_context() -> dict:
|
|
240
|
+
"""
|
|
241
|
+
Built-in demo context designed to trigger the "wow moment".
|
|
242
|
+
|
|
243
|
+
This simulates a real-world RAG pipeline with:
|
|
244
|
+
- A bloated system prompt
|
|
245
|
+
- Redundant retrieval chunks (near-duplicates)
|
|
246
|
+
- Memory that overlaps with retrieval
|
|
247
|
+
- Low source diversity (all chunks from the same doc)
|
|
248
|
+
"""
|
|
249
|
+
return {
|
|
250
|
+
"system": (
|
|
251
|
+
"You are a helpful AI assistant. You must always be polite and professional. "
|
|
252
|
+
"You must always follow the user's instructions carefully. "
|
|
253
|
+
"You must never generate harmful content. "
|
|
254
|
+
"You must always provide accurate and helpful responses. "
|
|
255
|
+
"Please ensure you follow all guidelines at all times. "
|
|
256
|
+
"Remember to always be helpful and follow instructions. "
|
|
257
|
+
"Important: always respond in a structured format. "
|
|
258
|
+
"Note: ensure your responses are accurate and well-formatted. "
|
|
259
|
+
"Guidelines: follow all rules and provide quality answers. "
|
|
260
|
+
"Rules: be professional, be accurate, be helpful at all times."
|
|
261
|
+
),
|
|
262
|
+
"messages": [
|
|
263
|
+
{"role": "user", "content": "What is the refund policy for premium subscriptions?"},
|
|
264
|
+
],
|
|
265
|
+
"chunks": [
|
|
266
|
+
{
|
|
267
|
+
"content": (
|
|
268
|
+
"Refund Policy: Premium subscriptions can be refunded within 30 days "
|
|
269
|
+
"of purchase. To request a refund, contact support@example.com with "
|
|
270
|
+
"your order number. Refunds are processed within 5-7 business days."
|
|
271
|
+
),
|
|
272
|
+
"source": "support_docs/refund_policy.md",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"content": (
|
|
276
|
+
"Premium Subscription Refund: You may request a refund for premium "
|
|
277
|
+
"subscriptions within 30 days of purchase. Send your order number to "
|
|
278
|
+
"support@example.com. Processing takes 5-7 business days."
|
|
279
|
+
),
|
|
280
|
+
"source": "support_docs/refund_policy.md",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"content": (
|
|
284
|
+
"Our refund policy allows premium subscription holders to get a full "
|
|
285
|
+
"refund within 30 days. Contact support@example.com with your order "
|
|
286
|
+
"number for processing. Expect 5-7 business days for completion."
|
|
287
|
+
),
|
|
288
|
+
"source": "support_docs/refund_policy.md",
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"content": (
|
|
292
|
+
"How to get a refund: If you have a premium subscription, you can "
|
|
293
|
+
"request a refund within 30 days of purchase. Email support@example.com "
|
|
294
|
+
"with your order number. Refunds take 5-7 business days."
|
|
295
|
+
),
|
|
296
|
+
"source": "support_docs/refund_policy.md",
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"content": (
|
|
300
|
+
"Pricing: Premium subscriptions cost $29.99/month or $299/year. "
|
|
301
|
+
"Enterprise plans start at $99/month. Contact sales for custom pricing."
|
|
302
|
+
),
|
|
303
|
+
"source": "support_docs/pricing.md",
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
"memory": [
|
|
307
|
+
(
|
|
308
|
+
"User previously asked about premium subscription pricing on 2024-01-15. "
|
|
309
|
+
"User was interested in the annual plan. Refund policy was mentioned briefly."
|
|
310
|
+
),
|
|
311
|
+
(
|
|
312
|
+
"User asked about the refund policy for premium subscriptions. "
|
|
313
|
+
"Support documents indicate 30-day refund window. Contact support@example.com."
|
|
314
|
+
),
|
|
315
|
+
],
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
cli()
|