context-engineering-core 0.12.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.
- context_engine/__init__.py +15 -0
- context_engine/__main__.py +3 -0
- context_engine/_sqlite.py +19 -0
- context_engine/_windows_security.py +125 -0
- context_engine/_windows_storage.py +110 -0
- context_engine/adapters.py +148 -0
- context_engine/answers.py +90 -0
- context_engine/budget.py +93 -0
- context_engine/cli.py +222 -0
- context_engine/config.py +193 -0
- context_engine/context.py +116 -0
- context_engine/demo.py +98 -0
- context_engine/errors.py +91 -0
- context_engine/evaluation/__init__.py +1 -0
- context_engine/evaluation/commands.py +90 -0
- context_engine/evaluation/corpus.py +247 -0
- context_engine/evaluation/data/freeze.json +26 -0
- context_engine/evaluation/data/ground_truth.json +287 -0
- context_engine/evaluation/data/pin_retention.json +8 -0
- context_engine/evaluation/data/protocol.json +122 -0
- context_engine/evaluation/data/scenario.json +1226 -0
- context_engine/evaluation/data/summary_fixture.json +7 -0
- context_engine/evaluation/execution.py +538 -0
- context_engine/evaluation/fixtures.py +32 -0
- context_engine/evaluation/grading.py +62 -0
- context_engine/evaluation/harness.py +165 -0
- context_engine/evaluation/heldout/__init__.py +260 -0
- context_engine/evaluation/heldout/__main__.py +40 -0
- context_engine/evaluation/heldout/data/ground_truth.json +18 -0
- context_engine/evaluation/heldout/data/manifest.json +9 -0
- context_engine/evaluation/heldout/data/protocol.json +14 -0
- context_engine/evaluation/heldout/data/scenarios.json +18 -0
- context_engine/evaluation/heldout/data/scoring_manifest.json +4 -0
- context_engine/evaluation/heldout/data/scoring_protocol.json +12 -0
- context_engine/evaluation/heldout/scoring.py +176 -0
- context_engine/evaluation/protocol.py +264 -0
- context_engine/evaluation/reporting.py +294 -0
- context_engine/evaluation/results.py +331 -0
- context_engine/evaluation/run_commands.py +146 -0
- context_engine/evaluation/strategies.py +261 -0
- context_engine/inspection.py +266 -0
- context_engine/layers/__init__.py +18 -0
- context_engine/layers/cap.py +99 -0
- context_engine/layers/common.py +162 -0
- context_engine/layers/pin.py +68 -0
- context_engine/layers/retrieve.py +233 -0
- context_engine/layers/summarize.py +202 -0
- context_engine/layers/window.py +29 -0
- context_engine/mcp_server.py +267 -0
- context_engine/memory/__init__.py +5 -0
- context_engine/memory/codec.py +91 -0
- context_engine/memory/demo.py +90 -0
- context_engine/memory/store.py +770 -0
- context_engine/models.py +384 -0
- context_engine/pipeline.py +383 -0
- context_engine/providers/__init__.py +1 -0
- context_engine/providers/client.py +301 -0
- context_engine/providers/commands.py +138 -0
- context_engine/providers/contracts.py +324 -0
- context_engine/providers/credentials.py +66 -0
- context_engine/providers/memory.py +112 -0
- context_engine/providers/store.py +408 -0
- context_engine/providers/transport.py +195 -0
- context_engine/providers/windows_credentials.py +23 -0
- context_engine/py.typed +0 -0
- context_engine/service/__init__.py +1 -0
- context_engine/service/__main__.py +68 -0
- context_engine/service/app.py +404 -0
- context_engine/service/auth.py +132 -0
- context_engine/service/client.py +52 -0
- context_engine/service/control.py +134 -0
- context_engine/service/demo.py +151 -0
- context_engine/service/integrations.py +28 -0
- context_engine/service/schemas.py +59 -0
- context_engine/service/worker.py +59 -0
- context_engine/service/workers.py +156 -0
- context_engine/summaries.py +196 -0
- context_engine/tokens.py +171 -0
- context_engine/work.py +46 -0
- context_engineering_core-0.12.0.dist-info/METADATA +203 -0
- context_engineering_core-0.12.0.dist-info/RECORD +84 -0
- context_engineering_core-0.12.0.dist-info/WHEEL +4 -0
- context_engineering_core-0.12.0.dist-info/entry_points.txt +3 -0
- context_engineering_core-0.12.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""CLI composition for synthetic or explicitly authorized benchmark execution and model swap."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..config import TokenizerConfig
|
|
7
|
+
from ..errors import BenchmarkError
|
|
8
|
+
from ..providers.client import ProviderClient
|
|
9
|
+
from ..providers.contracts import Completion, GenerationConfig, PriceCard, QuotaPolicy, Usage
|
|
10
|
+
from ..providers.credentials import load_api_key, private_bytes
|
|
11
|
+
from ..providers.store import RuntimeStore
|
|
12
|
+
from ..tokens import TiktokenCounter
|
|
13
|
+
from .corpus import load_bundle, strict_json
|
|
14
|
+
from .execution import (
|
|
15
|
+
RunJournal,
|
|
16
|
+
execute,
|
|
17
|
+
execution_profile,
|
|
18
|
+
execution_snapshot,
|
|
19
|
+
load_execution,
|
|
20
|
+
save_execution,
|
|
21
|
+
)
|
|
22
|
+
from .protocol import load_freeze, make_manifest
|
|
23
|
+
from .reporting import export_report, scorecard
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OfflineBenchmarkTransport:
|
|
27
|
+
namespace = "c06-synthetic-unknown-v1"
|
|
28
|
+
|
|
29
|
+
async def send(self, payload, api_key, timeout):
|
|
30
|
+
# Deliberately no corpus, expected answer, fact ID or grading access.
|
|
31
|
+
return Completion(
|
|
32
|
+
payload["model"], "synthetic-response", "UNKNOWN", "stop", Usage(40, 5, 0, 2)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def add_commands(commands):
|
|
37
|
+
for name in ("benchmark-run", "model-swap"):
|
|
38
|
+
parser = commands.add_parser(name, help="Run/resume frozen benchmark; offline by default")
|
|
39
|
+
parser.add_argument("--run-dir", type=Path, required=True)
|
|
40
|
+
parser.add_argument("--mode", choices=("offline", "live"), default="offline")
|
|
41
|
+
parser.add_argument("--allow-live", action="store_true")
|
|
42
|
+
parser.add_argument("--live-config", type=Path)
|
|
43
|
+
parser.add_argument("--env-file", type=Path, help="Explicit private GROQ_API_KEY file")
|
|
44
|
+
parser.add_argument("--max-calls", type=int, default=744)
|
|
45
|
+
parser.add_argument("--snapshot", type=Path, required=True)
|
|
46
|
+
parser.add_argument("--fact", action="append")
|
|
47
|
+
parser.add_argument("--budget", type=int, action="append")
|
|
48
|
+
parser.add_argument("--variant", action="append")
|
|
49
|
+
if name == "benchmark-run":
|
|
50
|
+
parser.add_argument("--model", action="append")
|
|
51
|
+
parser = commands.add_parser(
|
|
52
|
+
"benchmark-report", help="Regenerate validated reports without inference"
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument("--snapshot", type=Path, required=True)
|
|
55
|
+
parser.add_argument("--output-dir", type=Path, required=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_command(args):
|
|
59
|
+
bundle, frozen = load_bundle(), load_freeze()
|
|
60
|
+
if args.command == "benchmark-report":
|
|
61
|
+
payload = load_execution(args.snapshot, bundle, frozen)
|
|
62
|
+
card = scorecard(bundle, frozen, payload)
|
|
63
|
+
proof = export_report(args.output_dir, card)
|
|
64
|
+
return {"status": card["validity"]["status"], "report": proof, "inference_calls": 0}
|
|
65
|
+
models = bundle.get("protocol")["models"] if args.command == "model-swap" else args.model
|
|
66
|
+
manifest = make_manifest(
|
|
67
|
+
bundle, frozen, facts=args.fact, models=models, budgets=args.budget, variants=args.variant
|
|
68
|
+
)
|
|
69
|
+
if args.snapshot.exists():
|
|
70
|
+
raise BenchmarkError("Snapshot output exists; choose a new immutable export path")
|
|
71
|
+
if type(args.max_calls) is not int or not 0 <= args.max_calls <= 744:
|
|
72
|
+
raise BenchmarkError("Invalid execution batch size")
|
|
73
|
+
if args.mode == "live":
|
|
74
|
+
if not args.allow_live or bundle.get("protocol")["approval"] != "owner_approved":
|
|
75
|
+
raise BenchmarkError(
|
|
76
|
+
"Live execution requires explicit authorization and owner-approved registration"
|
|
77
|
+
)
|
|
78
|
+
if not args.live_config:
|
|
79
|
+
raise BenchmarkError("Live execution requires a private configuration and GROQ_API_KEY")
|
|
80
|
+
key = load_api_key(args.env_file)
|
|
81
|
+
try:
|
|
82
|
+
config = strict_json(private_bytes(args.live_config, 65536).decode(), maximum=65536)
|
|
83
|
+
if set(config) != {"quota", "prices", "max_run_cost_microusd", "ledger_path"}:
|
|
84
|
+
raise BenchmarkError("Unexpected live configuration fields")
|
|
85
|
+
quota = QuotaPolicy(**config["quota"])
|
|
86
|
+
if quota.daily_budget_microusd <= 0 and quota.billing_mode != "free_tier":
|
|
87
|
+
raise BenchmarkError("A positive daily spending cap is required")
|
|
88
|
+
prices = {
|
|
89
|
+
model: PriceCard(**config["prices"][model]) for model in manifest.data["models"]
|
|
90
|
+
}
|
|
91
|
+
for card in prices.values():
|
|
92
|
+
quota.validate_prices(card)
|
|
93
|
+
maximum = config["max_run_cost_microusd"]
|
|
94
|
+
if type(maximum) is not int or not 0 <= maximum <= 10**12:
|
|
95
|
+
raise BenchmarkError("Invalid run spending cap")
|
|
96
|
+
if (quota.billing_mode == "free_tier") != (maximum == 0):
|
|
97
|
+
raise BenchmarkError("Free-tier mode requires exactly zero run spending cap")
|
|
98
|
+
ledger_path = Path(config["ledger_path"])
|
|
99
|
+
if not ledger_path.is_absolute():
|
|
100
|
+
raise BenchmarkError(
|
|
101
|
+
"Live account ledger path must be absolute and shared across runs"
|
|
102
|
+
)
|
|
103
|
+
except (OSError, KeyError, TypeError, ValueError):
|
|
104
|
+
raise BenchmarkError("Invalid private live configuration") from None
|
|
105
|
+
maximum = config["max_run_cost_microusd"]
|
|
106
|
+
else:
|
|
107
|
+
if args.env_file is not None:
|
|
108
|
+
raise BenchmarkError("Explicit credential files are only used in live mode")
|
|
109
|
+
quota = QuotaPolicy(
|
|
110
|
+
"synthetic-benchmark", 100000000, rpm=10000, tpm=100000000, rpd=10000, tpd=100000000
|
|
111
|
+
)
|
|
112
|
+
prices = {
|
|
113
|
+
m: PriceCard("synthetic-v1", 1000000, 500000, 2000000) for m in manifest.data["models"]
|
|
114
|
+
}
|
|
115
|
+
ledger_path = args.run_dir / "provider.sqlite"
|
|
116
|
+
key, maximum = "synthetic-not-a-credential", 100000000
|
|
117
|
+
store = RuntimeStore(ledger_path)
|
|
118
|
+
clients = {
|
|
119
|
+
model: ProviderClient(
|
|
120
|
+
store=store,
|
|
121
|
+
quota=quota,
|
|
122
|
+
prices=prices[model],
|
|
123
|
+
api_key=key,
|
|
124
|
+
generation=GenerationConfig(model=model, **manifest.data["protocol"]["generation"]),
|
|
125
|
+
counter=TiktokenCounter(TokenizerConfig(**manifest.data["protocol"]["tokenizer"])),
|
|
126
|
+
transport=OfflineBenchmarkTransport() if args.mode == "offline" else None,
|
|
127
|
+
)
|
|
128
|
+
for model in manifest.data["models"]
|
|
129
|
+
}
|
|
130
|
+
profile = execution_profile(manifest, clients, mode=args.mode, max_run_cost_microusd=maximum)
|
|
131
|
+
journal = RunJournal(args.run_dir / "run.sqlite", manifest, profile)
|
|
132
|
+
result = asyncio.run(
|
|
133
|
+
execute(
|
|
134
|
+
bundle, frozen, journal, clients, max_calls=args.max_calls, allow_live=args.allow_live
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
payload = execution_snapshot(journal)
|
|
138
|
+
save_execution(args.snapshot, payload)
|
|
139
|
+
return {
|
|
140
|
+
**result,
|
|
141
|
+
"execution_id": journal.execution_id,
|
|
142
|
+
"mode": args.mode,
|
|
143
|
+
"real_inference_calls": 0 if args.mode == "offline" else None,
|
|
144
|
+
"snapshot": str(args.snapshot),
|
|
145
|
+
"quality_acceptance": "NOT_EVALUATED",
|
|
146
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Offline baseline comparison on the frozen 31-fact corpus. No inference is performed.
|
|
2
|
+
|
|
3
|
+
Strategies (same frozen corpus, tokenizer and per-budget allowance):
|
|
4
|
+
|
|
5
|
+
- full_history: the raw chronological request; non-fit when over the allowance.
|
|
6
|
+
- recent_window: WINDOW only.
|
|
7
|
+
- summary_window: WINDOW + a generated extractive summary of omitted turns.
|
|
8
|
+
- retrieval_window: WINDOW + BM25 retrieval of omitted originals.
|
|
9
|
+
- combined: CAP + PIN + RETRIEVE + WINDOW + generated summary (SDK defaults, which
|
|
10
|
+
since 0.11.0 include `recover_capped_window=True`: RETRIEVE restores original
|
|
11
|
+
evidence that CAP removed from WINDOW turns).
|
|
12
|
+
- combined_no_recover: combined with `recover_capped_window=False`, the default
|
|
13
|
+
before 0.11.0 and the frozen-protocol setting.
|
|
14
|
+
|
|
15
|
+
Summary and retrieval strategies receive the same optional allocation (the frozen
|
|
16
|
+
retrieval + summary reserves), so they compete for equal space. Ground truth is
|
|
17
|
+
used only after assembly, for scoring. Answer correctness needs model inference
|
|
18
|
+
and is reported NOT_EVALUATED here; this measures what context reaches the model.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import statistics
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
from ..config import BudgetConfig, CapConfig, RetrievalConfig, TokenizerConfig
|
|
25
|
+
from ..layers.summarize import PrefixSummaryPolicy
|
|
26
|
+
from ..models import BlockKind, ChatRequest, Message, Role
|
|
27
|
+
from ..pipeline import AssemblyOptions, assemble_context
|
|
28
|
+
from ..summaries import ExtractiveSummarizer, update_summary
|
|
29
|
+
from ..tokens import TiktokenCounter
|
|
30
|
+
from .corpus import Bundle, aliases
|
|
31
|
+
from .grading import present
|
|
32
|
+
from .harness import evidence_locations
|
|
33
|
+
|
|
34
|
+
SUMMARY_POLICY_VERSION = "extractive-v1"
|
|
35
|
+
STRATEGIES = {
|
|
36
|
+
"recent_window": AssemblyOptions(cap=False, pins=False, retrieve=False, summarize=False),
|
|
37
|
+
"summary_window": AssemblyOptions(cap=False, pins=False, retrieve=False, summarize=True),
|
|
38
|
+
"retrieval_window": AssemblyOptions(cap=False, pins=False, retrieve=True, summarize=False),
|
|
39
|
+
"combined": AssemblyOptions(),
|
|
40
|
+
"combined_no_recover": AssemblyOptions(),
|
|
41
|
+
}
|
|
42
|
+
NO_RECOVER = {"combined_no_recover"}
|
|
43
|
+
ORDER = ("full_history", *STRATEGIES)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _budget(protocol: dict, allowance: int, strategy: str) -> BudgetConfig:
|
|
47
|
+
reserves = protocol["budget"]
|
|
48
|
+
optional = reserves["retrieval_reserve"] + reserves["summary_reserve"]
|
|
49
|
+
settings = dict(reserves)
|
|
50
|
+
if strategy == "summary_window":
|
|
51
|
+
settings.update(retrieval_reserve=0, summary_reserve=optional)
|
|
52
|
+
elif strategy == "retrieval_window": # Same optional space, all for evidence.
|
|
53
|
+
settings.update(retrieval_reserve=optional, summary_reserve=0)
|
|
54
|
+
return BudgetConfig(
|
|
55
|
+
input_cap=allowance,
|
|
56
|
+
completion_reservation=protocol["generation"]["max_completion_tokens"],
|
|
57
|
+
**settings,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _percentile(values: list[float], fraction: float) -> float | None:
|
|
62
|
+
if not values:
|
|
63
|
+
return None
|
|
64
|
+
ordered = sorted(values)
|
|
65
|
+
return ordered[min(len(ordered) - 1, round(fraction * (len(ordered) - 1)))]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _assemble(job, protocol, counter, fact, allowance, strategy):
|
|
69
|
+
"""Assemble one strategy's request; returns (result, summary source tokens)."""
|
|
70
|
+
history = job.arguments["history"]
|
|
71
|
+
budget = _budget(protocol, allowance, strategy)
|
|
72
|
+
options = STRATEGIES[strategy]
|
|
73
|
+
arguments = dict(
|
|
74
|
+
system=job.arguments["system"],
|
|
75
|
+
at=job.arguments["at"],
|
|
76
|
+
token_counter=counter,
|
|
77
|
+
cap_config=CapConfig(**protocol["cap"]),
|
|
78
|
+
retrieval_config=RetrievalConfig(
|
|
79
|
+
**protocol["retrieval"], recover_capped_window=strategy not in NO_RECOVER
|
|
80
|
+
),
|
|
81
|
+
options=options,
|
|
82
|
+
)
|
|
83
|
+
pins = job.arguments["pinned_facts"]
|
|
84
|
+
result = assemble_context(history, fact["question"], pins, budget, **arguments)
|
|
85
|
+
summary_tokens = 0
|
|
86
|
+
if options.summarize:
|
|
87
|
+
reserve = budget.summary_reserve
|
|
88
|
+
snapshot = update_summary(
|
|
89
|
+
history,
|
|
90
|
+
result.diagnostics.kept_turn_ids,
|
|
91
|
+
ExtractiveSummarizer(max_characters=max(1, reserve * 3)),
|
|
92
|
+
scope=pins.scope,
|
|
93
|
+
policy_version=SUMMARY_POLICY_VERSION,
|
|
94
|
+
created_at=job.arguments["at"],
|
|
95
|
+
min_new_turns=1,
|
|
96
|
+
)
|
|
97
|
+
if snapshot is not None:
|
|
98
|
+
covered = set(snapshot.covered_turn_ids)
|
|
99
|
+
summary_tokens = sum(
|
|
100
|
+
counter.count_text(m.content)
|
|
101
|
+
for t in history
|
|
102
|
+
if t.turn_id in covered
|
|
103
|
+
for m in t.messages
|
|
104
|
+
)
|
|
105
|
+
result = assemble_context(
|
|
106
|
+
history,
|
|
107
|
+
fact["question"],
|
|
108
|
+
pins,
|
|
109
|
+
budget,
|
|
110
|
+
summary_policy=PrefixSummaryPolicy(snapshot),
|
|
111
|
+
**arguments,
|
|
112
|
+
)
|
|
113
|
+
return result, summary_tokens
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _probe(bundle, protocol, counter, fact, allowance, strategy) -> dict:
|
|
117
|
+
job = bundle.job(fact["question"])
|
|
118
|
+
history = job.arguments["history"]
|
|
119
|
+
values = aliases(fact)
|
|
120
|
+
raw = counter.count_request(
|
|
121
|
+
ChatRequest(
|
|
122
|
+
(
|
|
123
|
+
Message("raw:system", Role.SYSTEM, job.arguments["system"]),
|
|
124
|
+
*(m for turn in history for m in turn.messages),
|
|
125
|
+
Message("raw:question", Role.USER, fact["question"]),
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
base = {"fact_id": fact["fact_id"], "zone": fact["zone"], "raw_tokens": raw.estimated_tokens}
|
|
130
|
+
if strategy == "full_history":
|
|
131
|
+
fits = raw.estimated_tokens <= allowance
|
|
132
|
+
return {
|
|
133
|
+
**base,
|
|
134
|
+
"fits": fits,
|
|
135
|
+
"input_tokens": raw.estimated_tokens if fits else None,
|
|
136
|
+
# Oversized raw input cannot be sent, so it retains nothing usable.
|
|
137
|
+
"fact_present": fits,
|
|
138
|
+
"evidence_at_source": fits,
|
|
139
|
+
"fact_in_summary": False,
|
|
140
|
+
"retrieved_chunks": 0,
|
|
141
|
+
"source_chunks": 0,
|
|
142
|
+
"source_retrieved": False,
|
|
143
|
+
"summary_source_tokens": 0,
|
|
144
|
+
"latency_ms": 0.0,
|
|
145
|
+
}
|
|
146
|
+
started = time.perf_counter()
|
|
147
|
+
result, summary_tokens = _assemble(job, protocol, counter, fact, allowance, strategy)
|
|
148
|
+
latency = (time.perf_counter() - started) * 1000
|
|
149
|
+
source = fact["source_turn_id"]
|
|
150
|
+
chunks = [b for b in result.blocks if b.kind == BlockKind.RETRIEVED and b.content]
|
|
151
|
+
sources = [s for b in chunks for s in b.sources]
|
|
152
|
+
summary = next((b for b in result.blocks if b.kind == BlockKind.SUMMARY), None)
|
|
153
|
+
return {
|
|
154
|
+
**base,
|
|
155
|
+
"fits": True,
|
|
156
|
+
"input_tokens": result.diagnostics.estimate.estimated_tokens,
|
|
157
|
+
"fact_present": any(present(m.content, values) for m in result.request.messages),
|
|
158
|
+
"evidence_at_source": bool(evidence_locations(result, fact, history)),
|
|
159
|
+
"fact_in_summary": bool(summary and present(summary.content, values)),
|
|
160
|
+
"retrieved_chunks": len(result.diagnostics.retrieved_chunk_ids),
|
|
161
|
+
"source_chunks": sum(1 for s in sources if s.turn_id == source),
|
|
162
|
+
"source_retrieved": source in result.diagnostics.retrieved_turn_ids,
|
|
163
|
+
"summary_source_tokens": summary_tokens,
|
|
164
|
+
"latency_ms": latency,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _aggregate(probes: list[dict]) -> dict:
|
|
169
|
+
count = len(probes)
|
|
170
|
+
fit = [p for p in probes if p["fits"]]
|
|
171
|
+
retrieving = [p for p in fit if p["retrieved_chunks"]]
|
|
172
|
+
reductions = [1 - p["input_tokens"] / p["raw_tokens"] for p in fit]
|
|
173
|
+
latencies = [p["latency_ms"] for p in fit]
|
|
174
|
+
by_zone: dict[str, list[int]] = {}
|
|
175
|
+
for p in probes:
|
|
176
|
+
tally = by_zone.setdefault(p["zone"], [0, 0])
|
|
177
|
+
tally[0] += p["evidence_at_source"]
|
|
178
|
+
tally[1] += 1
|
|
179
|
+
return {
|
|
180
|
+
"probes": count,
|
|
181
|
+
"fit": len(fit),
|
|
182
|
+
"fact_present": sum(p["fact_present"] for p in probes),
|
|
183
|
+
"evidence_at_source": sum(p["evidence_at_source"] for p in probes),
|
|
184
|
+
"fact_in_summary": sum(p["fact_in_summary"] for p in probes),
|
|
185
|
+
"evidence_by_zone": {zone: f"{hit}/{total}" for zone, (hit, total) in by_zone.items()},
|
|
186
|
+
"median_input_tokens": statistics.median(p["input_tokens"] for p in fit) if fit else None,
|
|
187
|
+
"median_raw_tokens": statistics.median(p["raw_tokens"] for p in probes),
|
|
188
|
+
"median_token_reduction": round(statistics.median(reductions), 4) if fit else None,
|
|
189
|
+
# Chunk precision: share of retrieved chunks that come from the fact's source turn.
|
|
190
|
+
"retrieval_precision": (
|
|
191
|
+
round(
|
|
192
|
+
statistics.mean(p["source_chunks"] / p["retrieved_chunks"] for p in retrieving), 4
|
|
193
|
+
)
|
|
194
|
+
if retrieving
|
|
195
|
+
else None
|
|
196
|
+
),
|
|
197
|
+
# Turn recall: share of probes whose source turn was retrieved at all.
|
|
198
|
+
"retrieval_recall": (
|
|
199
|
+
round(sum(p["source_retrieved"] for p in fit) / len(fit), 4) if retrieving else None
|
|
200
|
+
),
|
|
201
|
+
"summary_source_tokens_total": sum(p["summary_source_tokens"] for p in probes),
|
|
202
|
+
"median_latency_ms": round(statistics.median(latencies), 3) if latencies else None,
|
|
203
|
+
"p95_latency_ms": round(_percentile(latencies, 0.95), 3) if latencies else None,
|
|
204
|
+
"answer_correct": "NOT_EVALUATED",
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def assemble_strategy(bundle: Bundle, fact: dict, allowance: int, strategy: str):
|
|
209
|
+
"""The exact request a strategy sends for one frozen fact (for bounded live runs).
|
|
210
|
+
|
|
211
|
+
`full_history` is not assembled; it either fits raw or is a non-fit.
|
|
212
|
+
"""
|
|
213
|
+
if strategy not in STRATEGIES:
|
|
214
|
+
raise ValueError("Unknown or non-assembled strategy")
|
|
215
|
+
protocol = bundle.get("protocol")
|
|
216
|
+
counter = TiktokenCounter(TokenizerConfig(**protocol["tokenizer"]))
|
|
217
|
+
job = bundle.job(fact["question"])
|
|
218
|
+
result, _ = _assemble(job, protocol, counter, fact, allowance, strategy)
|
|
219
|
+
return result, _budget(protocol, allowance, strategy)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def compare_strategies(
|
|
223
|
+
bundle: Bundle, budgets: tuple[int, ...] | None = None, *, include_probes: bool = False
|
|
224
|
+
) -> dict:
|
|
225
|
+
"""Run every strategy on every frozen fact at each budget. Deterministic except latency."""
|
|
226
|
+
protocol = bundle.get("protocol")
|
|
227
|
+
budgets = tuple(protocol["budgets"]) if budgets is None else tuple(budgets)
|
|
228
|
+
if not budgets or any(type(b) is not int or b <= 0 for b in budgets):
|
|
229
|
+
raise ValueError("Budgets must be positive integers")
|
|
230
|
+
counter = TiktokenCounter(TokenizerConfig(**protocol["tokenizer"]))
|
|
231
|
+
results = []
|
|
232
|
+
for allowance in budgets:
|
|
233
|
+
for strategy in ORDER:
|
|
234
|
+
probes = [
|
|
235
|
+
_probe(bundle, protocol, counter, fact, allowance, strategy)
|
|
236
|
+
for fact in bundle.facts
|
|
237
|
+
]
|
|
238
|
+
entry = {"budget": allowance, "strategy": strategy, **_aggregate(probes)}
|
|
239
|
+
if include_probes:
|
|
240
|
+
entry["probe_results"] = [
|
|
241
|
+
{**p, "latency_ms": round(p["latency_ms"], 3)} for p in probes
|
|
242
|
+
]
|
|
243
|
+
results.append(entry)
|
|
244
|
+
return {
|
|
245
|
+
"status": "COMPLETE",
|
|
246
|
+
"mode": "offline_strategy_comparison",
|
|
247
|
+
"inference_calls": 0,
|
|
248
|
+
"corpus": {"scenario_id": protocol["scenario_id"], "facts": len(bundle.facts)},
|
|
249
|
+
"fixture_hashes": bundle.hashes,
|
|
250
|
+
"summary_policy": SUMMARY_POLICY_VERSION,
|
|
251
|
+
"results": results,
|
|
252
|
+
"limitations": [
|
|
253
|
+
"Synthetic single-scenario corpus authored with the engine; not real-world quality.",
|
|
254
|
+
"Context retention only: answer correctness and faithfulness need model inference.",
|
|
255
|
+
"Token counts are local estimates, not provider-billed tokens.",
|
|
256
|
+
"Latency is local wall time and varies by machine.",
|
|
257
|
+
],
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
__all__ = ["NO_RECOVER", "ORDER", "STRATEGIES", "assemble_strategy", "compare_strategies"]
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Bounded JSON input and content-opt-in offline inspection. No provider calls."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, replace
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .config import (
|
|
9
|
+
MAX_INSPECTION_BYTES,
|
|
10
|
+
MAX_INSPECTION_TURNS,
|
|
11
|
+
BudgetConfig,
|
|
12
|
+
CapConfig,
|
|
13
|
+
RetrievalConfig,
|
|
14
|
+
Settings,
|
|
15
|
+
TokenizerConfig,
|
|
16
|
+
)
|
|
17
|
+
from .errors import InspectionError
|
|
18
|
+
from .layers.summarize import FrozenSummaryPolicy, SummarySnapshot
|
|
19
|
+
from .models import KeyedPins, Message, Pin, Scope, ToolCall, ToolDefinition, Turn, canonical_json
|
|
20
|
+
from .pipeline import assemble_context
|
|
21
|
+
from .tokens import TiktokenCounter
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _object(value, allowed, required=()):
|
|
25
|
+
if not isinstance(value, dict) or set(value) - set(allowed) or set(required) - set(value):
|
|
26
|
+
raise InspectionError("Input object contains missing or unsupported fields")
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _items(value):
|
|
31
|
+
if not isinstance(value, list):
|
|
32
|
+
raise InspectionError("Expected a JSON array")
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _time(value):
|
|
37
|
+
if not isinstance(value, str):
|
|
38
|
+
raise InspectionError("Expected an ISO timestamp with timezone")
|
|
39
|
+
try:
|
|
40
|
+
return datetime.fromisoformat(value)
|
|
41
|
+
except ValueError:
|
|
42
|
+
raise InspectionError("Invalid ISO timestamp") from None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _config(cls, value, default):
|
|
46
|
+
_object(value, cls.__dataclass_fields__)
|
|
47
|
+
return replace(default, **value)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class InspectionJob:
|
|
52
|
+
arguments: dict
|
|
53
|
+
tokenizer: TokenizerConfig
|
|
54
|
+
synthetic: bool = False
|
|
55
|
+
|
|
56
|
+
def run(self, *, input_cap: int | None = None):
|
|
57
|
+
arguments = dict(self.arguments)
|
|
58
|
+
if input_cap is not None:
|
|
59
|
+
arguments["budget"] = replace(arguments["budget"], input_cap=input_cap)
|
|
60
|
+
return assemble_context(**arguments, token_counter=TiktokenCounter(self.tokenizer))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_job(payload: dict, settings: Settings) -> InspectionJob:
|
|
64
|
+
_object(
|
|
65
|
+
payload,
|
|
66
|
+
{
|
|
67
|
+
"schema_version",
|
|
68
|
+
"scope",
|
|
69
|
+
"system",
|
|
70
|
+
"question",
|
|
71
|
+
"at",
|
|
72
|
+
"history",
|
|
73
|
+
"pins",
|
|
74
|
+
"tools",
|
|
75
|
+
"budget",
|
|
76
|
+
"cap",
|
|
77
|
+
"retrieval",
|
|
78
|
+
"tokenizer",
|
|
79
|
+
"summary",
|
|
80
|
+
},
|
|
81
|
+
{"schema_version", "scope", "system", "question", "at", "history"},
|
|
82
|
+
)
|
|
83
|
+
if type(payload["schema_version"]) is not int or payload["schema_version"] != 1:
|
|
84
|
+
raise InspectionError("Unsupported inspection schema version")
|
|
85
|
+
scope = Scope(
|
|
86
|
+
**_object(payload["scope"], {"tenant_id", "session_id"}, {"tenant_id", "session_id"})
|
|
87
|
+
)
|
|
88
|
+
at = _time(payload["at"])
|
|
89
|
+
history = []
|
|
90
|
+
raw_history = _items(payload["history"])
|
|
91
|
+
if len(raw_history) > MAX_INSPECTION_TURNS:
|
|
92
|
+
raise InspectionError(
|
|
93
|
+
"Inspection history exceeds the turn limit", maximum=MAX_INSPECTION_TURNS
|
|
94
|
+
)
|
|
95
|
+
for item in raw_history:
|
|
96
|
+
item = _object(
|
|
97
|
+
item, {"turn_id", "revision", "timestamp", "messages"}, {"turn_id", "messages"}
|
|
98
|
+
)
|
|
99
|
+
messages = []
|
|
100
|
+
for message in _items(item["messages"]):
|
|
101
|
+
message = dict(
|
|
102
|
+
_object(
|
|
103
|
+
message,
|
|
104
|
+
{"message_id", "role", "content", "tool_call_id", "tool_calls"},
|
|
105
|
+
{"message_id", "role", "content"},
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
calls = []
|
|
109
|
+
for call in _items(message.pop("tool_calls", [])):
|
|
110
|
+
calls.append(
|
|
111
|
+
ToolCall(
|
|
112
|
+
**_object(
|
|
113
|
+
call,
|
|
114
|
+
{"call_id", "name", "arguments_json"},
|
|
115
|
+
{"call_id", "name", "arguments_json"},
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
messages.append(Message(**message, tool_calls=tuple(calls)))
|
|
120
|
+
history.append(
|
|
121
|
+
Turn(
|
|
122
|
+
item["turn_id"],
|
|
123
|
+
scope,
|
|
124
|
+
tuple(messages),
|
|
125
|
+
item.get("revision", 1),
|
|
126
|
+
_time(item["timestamp"]) if "timestamp" in item else at,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
pins = []
|
|
130
|
+
for pin in _items(payload.get("pins", [])):
|
|
131
|
+
pin = dict(
|
|
132
|
+
_object(
|
|
133
|
+
pin,
|
|
134
|
+
{"key", "value", "origin", "revision", "effective_at", "expires_at"},
|
|
135
|
+
{"key", "value", "origin"},
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
effective = _time(pin.pop("effective_at")) if "effective_at" in pin else at
|
|
139
|
+
expires = _time(pin.pop("expires_at")) if pin.get("expires_at") is not None else None
|
|
140
|
+
pin.pop("expires_at", None)
|
|
141
|
+
pins.append(Pin(scope=scope, **pin, effective_at=effective, expires_at=expires))
|
|
142
|
+
tools = []
|
|
143
|
+
for tool in _items(payload.get("tools", [])):
|
|
144
|
+
tool = _object(tool, {"name", "description", "parameters"}, {"name", "parameters"})
|
|
145
|
+
tools.append(
|
|
146
|
+
ToolDefinition(
|
|
147
|
+
tool["name"], tool.get("description", ""), canonical_json(tool["parameters"])
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
policy = None
|
|
151
|
+
if payload.get("summary") is not None:
|
|
152
|
+
raw = _object(
|
|
153
|
+
payload["summary"],
|
|
154
|
+
{"content", "covered_turn_ids", "policy_version", "created_at", "expires_at"},
|
|
155
|
+
{"content", "covered_turn_ids", "policy_version", "created_at"},
|
|
156
|
+
)
|
|
157
|
+
ids = _items(raw["covered_turn_ids"])
|
|
158
|
+
if any(not isinstance(identifier, str) for identifier in ids) or len(set(ids)) != len(ids):
|
|
159
|
+
raise InspectionError("Summary turn IDs must be unique strings")
|
|
160
|
+
turns = {t.turn_id: t for t in history}
|
|
161
|
+
if set(ids) - turns.keys():
|
|
162
|
+
raise InspectionError("Summary refers to missing history")
|
|
163
|
+
policy = FrozenSummaryPolicy(
|
|
164
|
+
SummarySnapshot.from_history(
|
|
165
|
+
scope=scope,
|
|
166
|
+
content=raw["content"],
|
|
167
|
+
history=tuple(turns[i] for i in ids),
|
|
168
|
+
policy_version=raw["policy_version"],
|
|
169
|
+
created_at=_time(raw["created_at"]),
|
|
170
|
+
expires_at=_time(raw["expires_at"]) if raw.get("expires_at") is not None else None,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
question = payload["question"]
|
|
174
|
+
if not isinstance(question, str):
|
|
175
|
+
raise InspectionError("Question must be a string")
|
|
176
|
+
return InspectionJob(
|
|
177
|
+
{
|
|
178
|
+
"history": tuple(history),
|
|
179
|
+
"question": question,
|
|
180
|
+
"pinned_facts": KeyedPins(scope, tuple(pins)),
|
|
181
|
+
"system": payload["system"],
|
|
182
|
+
"at": at,
|
|
183
|
+
"tools": tuple(tools),
|
|
184
|
+
"summary_policy": policy,
|
|
185
|
+
"budget": _config(BudgetConfig, payload.get("budget", {}), settings.budget),
|
|
186
|
+
"cap_config": _config(CapConfig, payload.get("cap", {}), settings.cap),
|
|
187
|
+
"retrieval_config": _config(
|
|
188
|
+
RetrievalConfig, payload.get("retrieval", {}), settings.retrieval
|
|
189
|
+
),
|
|
190
|
+
},
|
|
191
|
+
_config(TokenizerConfig, payload.get("tokenizer", {}), settings.tokenizer),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def load_job(path: Path, settings: Settings) -> InspectionJob:
|
|
196
|
+
def unique(pairs):
|
|
197
|
+
result = {}
|
|
198
|
+
for key, value in pairs:
|
|
199
|
+
if key in result:
|
|
200
|
+
raise InspectionError("Duplicate JSON keys are not allowed")
|
|
201
|
+
result[key] = value
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
def reject_constant(_):
|
|
205
|
+
raise InspectionError("JSON numeric constants must be finite")
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
with path.open("rb") as handle:
|
|
209
|
+
raw = handle.read(MAX_INSPECTION_BYTES + 1)
|
|
210
|
+
if len(raw) > MAX_INSPECTION_BYTES:
|
|
211
|
+
raise InspectionError(
|
|
212
|
+
"Inspection file exceeds the byte limit", maximum=MAX_INSPECTION_BYTES
|
|
213
|
+
)
|
|
214
|
+
payload = json.loads(
|
|
215
|
+
raw.decode("utf-8"), object_pairs_hook=unique, parse_constant=reject_constant
|
|
216
|
+
)
|
|
217
|
+
except OSError:
|
|
218
|
+
raise InspectionError("Cannot read inspection input") from None
|
|
219
|
+
except (UnicodeError, ValueError, RecursionError):
|
|
220
|
+
raise InspectionError("Inspection input must be valid UTF-8 JSON") from None
|
|
221
|
+
try:
|
|
222
|
+
return parse_job(payload, settings)
|
|
223
|
+
except (TypeError, KeyError, OverflowError, RecursionError):
|
|
224
|
+
raise InspectionError("Invalid inspection input structure") from None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def demo_job(settings: Settings) -> InspectionJob:
|
|
228
|
+
from .demo import demo_context
|
|
229
|
+
|
|
230
|
+
class DemoPolicy:
|
|
231
|
+
def get_summary(self, history, scope, at):
|
|
232
|
+
return SummarySnapshot.from_history(
|
|
233
|
+
scope=scope,
|
|
234
|
+
content="Earlier discussion concerned an incident and routine telemetry.",
|
|
235
|
+
history=history,
|
|
236
|
+
policy_version="synthetic-demo-v1",
|
|
237
|
+
created_at=at,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
context, budget, at = demo_context()
|
|
241
|
+
return InspectionJob(
|
|
242
|
+
{
|
|
243
|
+
"history": context.original_turns,
|
|
244
|
+
"question": context.question,
|
|
245
|
+
"pinned_facts": context.pins,
|
|
246
|
+
"budget": budget,
|
|
247
|
+
"at": at,
|
|
248
|
+
"system": "Use the supplied evidence to answer.",
|
|
249
|
+
"summary_policy": DemoPolicy(),
|
|
250
|
+
},
|
|
251
|
+
settings.tokenizer,
|
|
252
|
+
synthetic=True,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def save_inspection(path: Path, result: dict) -> None:
|
|
257
|
+
"""Explicit export never overwrites an existing file."""
|
|
258
|
+
try:
|
|
259
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
260
|
+
with path.open("x", encoding="utf-8") as handle:
|
|
261
|
+
json.dump(result, handle, ensure_ascii=False, indent=2)
|
|
262
|
+
handle.write("\n")
|
|
263
|
+
except FileExistsError:
|
|
264
|
+
raise InspectionError("Output already exists; choose a new path") from None
|
|
265
|
+
except (OSError, ValueError):
|
|
266
|
+
raise InspectionError("Cannot write inspection output") from None
|