crprotocol 2.0.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.
- crp/__init__.py +126 -0
- crp/__main__.py +8 -0
- crp/_typing.py +27 -0
- crp/_version.py +5 -0
- crp/adapters.py +31 -0
- crp/advanced/__init__.py +40 -0
- crp/advanced/auto_ingest.py +400 -0
- crp/advanced/cqs.py +235 -0
- crp/advanced/cross_window.py +477 -0
- crp/advanced/curator.py +265 -0
- crp/advanced/feedback.py +146 -0
- crp/advanced/hierarchical.py +211 -0
- crp/advanced/meta_learning.py +401 -0
- crp/advanced/parallel.py +98 -0
- crp/advanced/review_cycle.py +329 -0
- crp/advanced/scale_mode.py +129 -0
- crp/advanced/source_grounding.py +207 -0
- crp/ckf/__init__.py +35 -0
- crp/ckf/community.py +377 -0
- crp/ckf/fabric.py +445 -0
- crp/ckf/gc.py +175 -0
- crp/ckf/graph_walk.py +87 -0
- crp/ckf/merge.py +133 -0
- crp/ckf/pattern_query.py +122 -0
- crp/ckf/pubsub.py +128 -0
- crp/ckf/semantic.py +207 -0
- crp/cli/__init__.py +7 -0
- crp/cli/main.py +329 -0
- crp/cli/sidecar.py +929 -0
- crp/cli/startup.py +272 -0
- crp/continuation/__init__.py +103 -0
- crp/continuation/completion.py +348 -0
- crp/continuation/degradation.py +157 -0
- crp/continuation/document_map.py +160 -0
- crp/continuation/flow.py +109 -0
- crp/continuation/gap.py +419 -0
- crp/continuation/manager.py +484 -0
- crp/continuation/quality_monitor.py +179 -0
- crp/continuation/stitch.py +419 -0
- crp/continuation/trigger.py +142 -0
- crp/continuation/voice.py +157 -0
- crp/core/__init__.py +69 -0
- crp/core/batch.py +77 -0
- crp/core/circuit_breaker.py +116 -0
- crp/core/config.py +377 -0
- crp/core/context_tools.py +540 -0
- crp/core/dispatch_router.py +3977 -0
- crp/core/errors.py +128 -0
- crp/core/extraction_facade.py +384 -0
- crp/core/facilitator.py +713 -0
- crp/core/idempotency.py +215 -0
- crp/core/orchestrator.py +1435 -0
- crp/core/relay_strategies.py +613 -0
- crp/core/security_manager.py +140 -0
- crp/core/session.py +134 -0
- crp/core/task_intent.py +36 -0
- crp/core/window.py +363 -0
- crp/envelope/__init__.py +30 -0
- crp/envelope/builder.py +288 -0
- crp/envelope/decomposer.py +236 -0
- crp/envelope/formatter.py +168 -0
- crp/envelope/packer.py +211 -0
- crp/envelope/reranker.py +209 -0
- crp/envelope/scoring.py +310 -0
- crp/extraction/__init__.py +45 -0
- crp/extraction/complexity.py +96 -0
- crp/extraction/contradiction.py +132 -0
- crp/extraction/pipeline.py +360 -0
- crp/extraction/quality_gate.py +237 -0
- crp/extraction/stage1_regex.py +173 -0
- crp/extraction/stage2_statistical.py +244 -0
- crp/extraction/stage3_gliner.py +210 -0
- crp/extraction/stage4_uie.py +183 -0
- crp/extraction/stage5_discourse.py +175 -0
- crp/extraction/stage6_llm.py +178 -0
- crp/extraction/structured_output.py +219 -0
- crp/extraction/types.py +299 -0
- crp/license_guard.py +722 -0
- crp/observability/__init__.py +30 -0
- crp/observability/audit.py +118 -0
- crp/observability/events.py +233 -0
- crp/observability/metrics.py +264 -0
- crp/observability/quality.py +135 -0
- crp/observability/structured_logging.py +81 -0
- crp/observability/telemetry.py +117 -0
- crp/provenance/__init__.py +314 -0
- crp/provenance/_embeddings.py +97 -0
- crp/provenance/_types.py +378 -0
- crp/provenance/attribution_scorer.py +252 -0
- crp/provenance/claim_detector.py +229 -0
- crp/provenance/contradiction_detector.py +243 -0
- crp/provenance/distortion_detector.py +397 -0
- crp/provenance/entailment_verifier.py +358 -0
- crp/provenance/fabrication_detector.py +203 -0
- crp/provenance/hallucination_scorer.py +320 -0
- crp/provenance/omission_analyzer.py +106 -0
- crp/provenance/provenance_chain.py +205 -0
- crp/provenance/report_generator.py +440 -0
- crp/providers/__init__.py +43 -0
- crp/providers/anthropic.py +270 -0
- crp/providers/base.py +135 -0
- crp/providers/custom.py +63 -0
- crp/providers/diagnostic.py +251 -0
- crp/providers/llamacpp.py +224 -0
- crp/providers/manager.py +139 -0
- crp/providers/ollama.py +243 -0
- crp/providers/openai.py +628 -0
- crp/providers/tokenizers.py +48 -0
- crp/py.typed +0 -0
- crp/resources/__init__.py +53 -0
- crp/resources/adaptive_allocator.py +525 -0
- crp/resources/cost_model.py +388 -0
- crp/resources/overhead_manager.py +217 -0
- crp/resources/resource_manager.py +262 -0
- crp/schemas/__init__.py +20 -0
- crp/schemas/cost-estimate.json +33 -0
- crp/schemas/crp-error.json +43 -0
- crp/schemas/envelope-preview.json +40 -0
- crp/schemas/persisted-state-header.json +27 -0
- crp/schemas/quality-report.json +94 -0
- crp/schemas/session-handle.json +33 -0
- crp/schemas/session-status.json +57 -0
- crp/schemas/stream-event.json +18 -0
- crp/schemas/task-intent.json +42 -0
- crp/security/__init__.py +93 -0
- crp/security/audit_trail.py +392 -0
- crp/security/binding.py +192 -0
- crp/security/compliance.py +813 -0
- crp/security/consent.py +593 -0
- crp/security/embedding_defense.py +161 -0
- crp/security/encryption.py +202 -0
- crp/security/injection.py +335 -0
- crp/security/integrity.py +267 -0
- crp/security/privacy.py +662 -0
- crp/security/quarantine.py +249 -0
- crp/security/rbac.py +221 -0
- crp/security/validation.py +164 -0
- crp/state/__init__.py +31 -0
- crp/state/cold_storage.py +258 -0
- crp/state/compaction.py +263 -0
- crp/state/critical_state.py +104 -0
- crp/state/event_log.py +313 -0
- crp/state/fact.py +189 -0
- crp/state/serialization.py +189 -0
- crp/state/session_cleanup.py +77 -0
- crp/state/snapshot.py +290 -0
- crp/state/warm_store.py +346 -0
- crprotocol-2.0.0.dist-info/METADATA +1295 -0
- crprotocol-2.0.0.dist-info/RECORD +153 -0
- crprotocol-2.0.0.dist-info/WHEEL +4 -0
- crprotocol-2.0.0.dist-info/entry_points.txt +2 -0
- crprotocol-2.0.0.dist-info/licenses/LICENSE.md +170 -0
- crprotocol-2.0.0.dist-info/licenses/NOTICE +18 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""Provider pricing, real-time cost tracking, and budget enforcement (§6.8).
|
|
4
|
+
|
|
5
|
+
Budget invariants:
|
|
6
|
+
- Warn at 80% of any cap.
|
|
7
|
+
- Hard stop at 100% → BudgetExhaustedError.
|
|
8
|
+
- OverheadBudget caps total overhead at 15%.
|
|
9
|
+
- Feature shedding cascade: review_tier3 → orc_steps → curation →
|
|
10
|
+
re_grounding → review_tier2 (lowest priority shed first).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from crp.core.errors import BudgetExhaustedError
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("crp.cost_model")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Constants
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
WARN_THRESHOLD_PCT = 80
|
|
30
|
+
DEFAULT_OVERHEAD_CAP = 15.0 # %
|
|
31
|
+
GRACE_PCT = 5.0 # high-priority features may exceed by 5%
|
|
32
|
+
HIGH_PRIORITY_WEIGHT = 2 # features with weight >= 2 get grace
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Provider pricing
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class ProviderPricing:
|
|
42
|
+
"""Token pricing for an LLM provider (USD per 1M tokens)."""
|
|
43
|
+
|
|
44
|
+
input_price_per_million: float = 0.0
|
|
45
|
+
output_price_per_million: float = 0.0
|
|
46
|
+
provider_name: str = "unknown"
|
|
47
|
+
|
|
48
|
+
def input_cost(self, tokens: int) -> float:
|
|
49
|
+
return tokens * self.input_price_per_million / 1_000_000
|
|
50
|
+
|
|
51
|
+
def output_cost(self, tokens: int) -> float:
|
|
52
|
+
return tokens * self.output_price_per_million / 1_000_000
|
|
53
|
+
|
|
54
|
+
def total_cost(self, input_tokens: int, output_tokens: int) -> float:
|
|
55
|
+
return self.input_cost(input_tokens) + self.output_cost(output_tokens)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Built-in pricing tables (approximate, users can override)
|
|
59
|
+
KNOWN_PRICING: dict[str, ProviderPricing] = {
|
|
60
|
+
"gpt-4o": ProviderPricing(2.50, 10.00, "openai"),
|
|
61
|
+
"gpt-4o-mini": ProviderPricing(0.15, 0.60, "openai"),
|
|
62
|
+
"gpt-4-turbo": ProviderPricing(10.00, 30.00, "openai"),
|
|
63
|
+
"claude-3-opus": ProviderPricing(15.00, 75.00, "anthropic"),
|
|
64
|
+
"claude-3-sonnet": ProviderPricing(3.00, 15.00, "anthropic"),
|
|
65
|
+
"claude-3-haiku": ProviderPricing(0.25, 1.25, "anthropic"),
|
|
66
|
+
"gemini-1.5-pro": ProviderPricing(3.50, 10.50, "google"),
|
|
67
|
+
"gemini-1.5-flash": ProviderPricing(0.075, 0.30, "google"),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Window cost record
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class WindowCost:
|
|
78
|
+
"""Cost record for a single window."""
|
|
79
|
+
|
|
80
|
+
window_id: str = ""
|
|
81
|
+
input_tokens: int = 0
|
|
82
|
+
output_tokens: int = 0
|
|
83
|
+
cost_usd: float = 0.0
|
|
84
|
+
is_overhead: bool = False
|
|
85
|
+
feature_name: str = ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Budget warning
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class BudgetWarningLevel(str, Enum):
|
|
94
|
+
"""Budget warning levels."""
|
|
95
|
+
|
|
96
|
+
NONE = "none"
|
|
97
|
+
WARN = "warn" # 80%+
|
|
98
|
+
CRITICAL = "critical" # 95%+
|
|
99
|
+
EXCEEDED = "exceeded" # 100%+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class BudgetWarning:
|
|
104
|
+
"""A budget warning emitted when approaching limits."""
|
|
105
|
+
|
|
106
|
+
cap_type: str = "" # "windows" | "input_tokens" | "output_tokens"
|
|
107
|
+
level: BudgetWarningLevel = BudgetWarningLevel.NONE
|
|
108
|
+
used: int = 0
|
|
109
|
+
limit: int = 0
|
|
110
|
+
pct_used: float = 0.0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# CostModel
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class CostModel:
|
|
119
|
+
"""Real-time cost tracking with budget enforcement (§6.8).
|
|
120
|
+
|
|
121
|
+
Tracks per-window, per-session, and cumulative costs.
|
|
122
|
+
Warns at 80%, hard-stops at 100% of user-set budgets.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
pricing: ProviderPricing | None = None,
|
|
128
|
+
max_windows: int | None = None,
|
|
129
|
+
max_input_tokens: int | None = None,
|
|
130
|
+
max_output_tokens: int | None = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
self._pricing = pricing or ProviderPricing()
|
|
133
|
+
self._max_windows = max_windows
|
|
134
|
+
self._max_input_tokens = max_input_tokens
|
|
135
|
+
self._max_output_tokens = max_output_tokens
|
|
136
|
+
|
|
137
|
+
# Running totals
|
|
138
|
+
self._windows: list[WindowCost] = []
|
|
139
|
+
self._total_input_tokens = 0
|
|
140
|
+
self._total_output_tokens = 0
|
|
141
|
+
self._total_cost_usd = 0.0
|
|
142
|
+
self._warnings: list[BudgetWarning] = []
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def pricing(self) -> ProviderPricing:
|
|
146
|
+
return self._pricing
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def total_windows(self) -> int:
|
|
150
|
+
return len(self._windows)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def total_input_tokens(self) -> int:
|
|
154
|
+
return self._total_input_tokens
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def total_output_tokens(self) -> int:
|
|
158
|
+
return self._total_output_tokens
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def total_cost_usd(self) -> float:
|
|
162
|
+
return self._total_cost_usd
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def warnings(self) -> list[BudgetWarning]:
|
|
166
|
+
return list(self._warnings)
|
|
167
|
+
|
|
168
|
+
def record_window(
|
|
169
|
+
self,
|
|
170
|
+
window_id: str,
|
|
171
|
+
input_tokens: int,
|
|
172
|
+
output_tokens: int,
|
|
173
|
+
is_overhead: bool = False,
|
|
174
|
+
feature_name: str = "",
|
|
175
|
+
) -> WindowCost:
|
|
176
|
+
"""Record a completed window's cost. Returns the WindowCost record."""
|
|
177
|
+
cost_usd = self._pricing.total_cost(input_tokens, output_tokens)
|
|
178
|
+
record = WindowCost(
|
|
179
|
+
window_id=window_id,
|
|
180
|
+
input_tokens=input_tokens,
|
|
181
|
+
output_tokens=output_tokens,
|
|
182
|
+
cost_usd=cost_usd,
|
|
183
|
+
is_overhead=is_overhead,
|
|
184
|
+
feature_name=feature_name,
|
|
185
|
+
)
|
|
186
|
+
self._windows.append(record)
|
|
187
|
+
self._total_input_tokens += input_tokens
|
|
188
|
+
self._total_output_tokens += output_tokens
|
|
189
|
+
self._total_cost_usd += cost_usd
|
|
190
|
+
return record
|
|
191
|
+
|
|
192
|
+
def check_budget(self, input_tokens: int = 0) -> list[BudgetWarning]:
|
|
193
|
+
"""Check all budget caps. Raises BudgetExhaustedError if exceeded.
|
|
194
|
+
|
|
195
|
+
Returns any warnings generated (80%+ of cap).
|
|
196
|
+
"""
|
|
197
|
+
warnings: list[BudgetWarning] = []
|
|
198
|
+
|
|
199
|
+
# Windows cap
|
|
200
|
+
if self._max_windows is not None:
|
|
201
|
+
w = self._check_cap(
|
|
202
|
+
"windows",
|
|
203
|
+
self.total_windows + 1,
|
|
204
|
+
self._max_windows,
|
|
205
|
+
)
|
|
206
|
+
if w:
|
|
207
|
+
warnings.append(w)
|
|
208
|
+
|
|
209
|
+
# Input tokens cap
|
|
210
|
+
if self._max_input_tokens is not None:
|
|
211
|
+
w = self._check_cap(
|
|
212
|
+
"input_tokens",
|
|
213
|
+
self._total_input_tokens + input_tokens,
|
|
214
|
+
self._max_input_tokens,
|
|
215
|
+
)
|
|
216
|
+
if w:
|
|
217
|
+
warnings.append(w)
|
|
218
|
+
|
|
219
|
+
# Output tokens cap (check cumulative)
|
|
220
|
+
if self._max_output_tokens is not None:
|
|
221
|
+
w = self._check_cap(
|
|
222
|
+
"output_tokens",
|
|
223
|
+
self._total_output_tokens,
|
|
224
|
+
self._max_output_tokens,
|
|
225
|
+
)
|
|
226
|
+
if w:
|
|
227
|
+
warnings.append(w)
|
|
228
|
+
|
|
229
|
+
self._warnings.extend(warnings)
|
|
230
|
+
return warnings
|
|
231
|
+
|
|
232
|
+
def _check_cap(
|
|
233
|
+
self, cap_type: str, used: int, limit: int
|
|
234
|
+
) -> BudgetWarning | None:
|
|
235
|
+
"""Check a single budget cap. Raises on exceeded."""
|
|
236
|
+
if limit <= 0:
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
pct = (used / limit) * 100
|
|
240
|
+
|
|
241
|
+
if pct >= 100:
|
|
242
|
+
raise BudgetExhaustedError(
|
|
243
|
+
f"{cap_type} budget exceeded",
|
|
244
|
+
cap_type=cap_type,
|
|
245
|
+
used=used,
|
|
246
|
+
limit=limit,
|
|
247
|
+
windows_completed=self.total_windows,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if pct >= 95:
|
|
251
|
+
level = BudgetWarningLevel.CRITICAL
|
|
252
|
+
elif pct >= WARN_THRESHOLD_PCT:
|
|
253
|
+
level = BudgetWarningLevel.WARN
|
|
254
|
+
else:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
warning = BudgetWarning(
|
|
258
|
+
cap_type=cap_type,
|
|
259
|
+
level=level,
|
|
260
|
+
used=used,
|
|
261
|
+
limit=limit,
|
|
262
|
+
pct_used=round(pct, 1),
|
|
263
|
+
)
|
|
264
|
+
logger.warning(
|
|
265
|
+
"Budget %s: %s at %.1f%% (%d / %d)",
|
|
266
|
+
level.value, cap_type, pct, used, limit,
|
|
267
|
+
)
|
|
268
|
+
return warning
|
|
269
|
+
|
|
270
|
+
def estimate(
|
|
271
|
+
self,
|
|
272
|
+
planned_dispatches: int = 1,
|
|
273
|
+
avg_input_tokens: int = 0,
|
|
274
|
+
avg_output_tokens: int = 0,
|
|
275
|
+
) -> dict[str, Any]:
|
|
276
|
+
"""Pre-flight cost estimation."""
|
|
277
|
+
total_in = planned_dispatches * avg_input_tokens
|
|
278
|
+
total_out = planned_dispatches * avg_output_tokens
|
|
279
|
+
cost = self._pricing.total_cost(total_in, total_out)
|
|
280
|
+
return {
|
|
281
|
+
"estimated_windows": planned_dispatches,
|
|
282
|
+
"estimated_input_tokens": total_in,
|
|
283
|
+
"estimated_output_tokens": total_out,
|
|
284
|
+
"estimated_cost_usd": cost if cost > 0 else None,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
def reset(self) -> None:
|
|
288
|
+
"""Reset all counters (on session start)."""
|
|
289
|
+
self._windows.clear()
|
|
290
|
+
self._total_input_tokens = 0
|
|
291
|
+
self._total_output_tokens = 0
|
|
292
|
+
self._total_cost_usd = 0.0
|
|
293
|
+
self._warnings.clear()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
# OverheadBudget (§6.9)
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@dataclass
|
|
302
|
+
class FeaturePriority:
|
|
303
|
+
"""Priority entry for overhead feature shedding."""
|
|
304
|
+
|
|
305
|
+
name: str
|
|
306
|
+
weight: int
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
FEATURE_PRIORITY: list[FeaturePriority] = [
|
|
310
|
+
FeaturePriority("review_tier3", 3), # Full review windows (most expensive)
|
|
311
|
+
FeaturePriority("orc_steps", 2), # Extra ORC reasoning steps
|
|
312
|
+
FeaturePriority("curation", 1), # Curation windows
|
|
313
|
+
FeaturePriority("re_grounding", 1), # Re-grounding windows
|
|
314
|
+
FeaturePriority("review_tier2", 0), # Binary probes (cheapest, shed last)
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class OverheadDecision(str, Enum):
|
|
319
|
+
"""Result of overhead budget check."""
|
|
320
|
+
|
|
321
|
+
ALLOW = "allow"
|
|
322
|
+
DENY = "deny"
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@dataclass
|
|
326
|
+
class OverheadBudget:
|
|
327
|
+
"""Caps total protocol overhead across all features (§6.9).
|
|
328
|
+
|
|
329
|
+
Default: 15% of productive windows.
|
|
330
|
+
Feature shedding: lowest priority shed first.
|
|
331
|
+
Never shed: extraction (always runs) and Tier 1 validation (zero LLM cost).
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
max_overhead_pct: float = DEFAULT_OVERHEAD_CAP
|
|
335
|
+
current_overhead_windows: int = 0
|
|
336
|
+
current_productive_windows: int = 0
|
|
337
|
+
shed_log: list[str] = field(default_factory=list)
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def current_ratio(self) -> float:
|
|
341
|
+
if self.current_productive_windows == 0:
|
|
342
|
+
return 0.0
|
|
343
|
+
return self.current_overhead_windows / self.current_productive_windows
|
|
344
|
+
|
|
345
|
+
def record_productive(self) -> None:
|
|
346
|
+
"""Record a productive (non-overhead) window."""
|
|
347
|
+
self.current_productive_windows += 1
|
|
348
|
+
|
|
349
|
+
def check(self, feature_name: str) -> OverheadDecision:
|
|
350
|
+
"""Check if an overhead window is allowed for the given feature.
|
|
351
|
+
|
|
352
|
+
Returns ALLOW or DENY. If denied, logs the shedding event.
|
|
353
|
+
"""
|
|
354
|
+
ratio = self.current_ratio
|
|
355
|
+
|
|
356
|
+
if ratio < self.max_overhead_pct / 100:
|
|
357
|
+
self.current_overhead_windows += 1
|
|
358
|
+
return OverheadDecision.ALLOW
|
|
359
|
+
|
|
360
|
+
# Over budget — check if feature is high-priority (gets grace)
|
|
361
|
+
weight = self._get_weight(feature_name)
|
|
362
|
+
if weight >= HIGH_PRIORITY_WEIGHT:
|
|
363
|
+
grace_limit = (self.max_overhead_pct + GRACE_PCT) / 100
|
|
364
|
+
if ratio < grace_limit:
|
|
365
|
+
self.current_overhead_windows += 1
|
|
366
|
+
return OverheadDecision.ALLOW
|
|
367
|
+
|
|
368
|
+
# Deny and log
|
|
369
|
+
msg = (
|
|
370
|
+
f"Overhead budget exceeded ({ratio:.1%} > {self.max_overhead_pct}%). "
|
|
371
|
+
f"Shedding {feature_name}."
|
|
372
|
+
)
|
|
373
|
+
logger.info(msg)
|
|
374
|
+
self.shed_log.append(msg)
|
|
375
|
+
return OverheadDecision.DENY
|
|
376
|
+
|
|
377
|
+
def reset(self) -> None:
|
|
378
|
+
"""Reset on session start."""
|
|
379
|
+
self.current_overhead_windows = 0
|
|
380
|
+
self.current_productive_windows = 0
|
|
381
|
+
self.shed_log.clear()
|
|
382
|
+
|
|
383
|
+
@staticmethod
|
|
384
|
+
def _get_weight(feature_name: str) -> int:
|
|
385
|
+
for fp in FEATURE_PRIORITY:
|
|
386
|
+
if fp.name == feature_name:
|
|
387
|
+
return fp.weight
|
|
388
|
+
return 0
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""Overhead budget manager — shedding cascade + lazy embedding (§6.12, §3.2).
|
|
4
|
+
|
|
5
|
+
OverheadBudgetManager sits on top of the existing ``OverheadBudget`` in
|
|
6
|
+
``cost_model.py`` and adds two features from the spec:
|
|
7
|
+
|
|
8
|
+
1. **Feature shedding cascade (§6.12)**
|
|
9
|
+
When overhead exceeds the cap (default 15%), features are shed in a
|
|
10
|
+
fixed cost order:
|
|
11
|
+
|
|
12
|
+
community_detection → cross_encoder → gliner → uie → discourse
|
|
13
|
+
|
|
14
|
+
Each feature can be individually disabled and later re-enabled when
|
|
15
|
+
overhead drops.
|
|
16
|
+
|
|
17
|
+
2. **Lazy embedding batch (§3.2)**
|
|
18
|
+
Instead of computing embeddings on every ``fact.created`` event,
|
|
19
|
+
the manager *defers* them and batch-processes once N facts have
|
|
20
|
+
accumulated or a flush is requested.
|
|
21
|
+
|
|
22
|
+
Design goals:
|
|
23
|
+
* Pure logic — no external deps, no I/O.
|
|
24
|
+
* Works with or without an EventEmitter.
|
|
25
|
+
* Thread-safe (uses a threading.Lock around mutable state).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
import threading
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger("crp.overhead_manager")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
39
|
+
# Feature shedding cascade (§9.6a)
|
|
40
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
41
|
+
|
|
42
|
+
# Ordered cheapest-to-most-expensive.
|
|
43
|
+
# First in the list = shed first when over budget.
|
|
44
|
+
SHEDDING_CASCADE: list[str] = [
|
|
45
|
+
"community_detection",
|
|
46
|
+
"cross_encoder",
|
|
47
|
+
"gliner",
|
|
48
|
+
"uie",
|
|
49
|
+
"discourse",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
# ML intelligence features that must NEVER be shed. These are the core
|
|
53
|
+
# extraction pipeline stages that provide CRP's analytical intelligence.
|
|
54
|
+
# Under pressure, the system throttles throughput (fewer facts per stage,
|
|
55
|
+
# larger batches) rather than disabling these capabilities.
|
|
56
|
+
PROTECTED_INTELLIGENCE: frozenset[str] = frozenset({"gliner", "uie", "discourse"})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class SheddingState:
|
|
61
|
+
"""Tracks which features are currently enabled or shed."""
|
|
62
|
+
|
|
63
|
+
enabled: dict[str, bool] = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
def __post_init__(self) -> None:
|
|
66
|
+
if not self.enabled:
|
|
67
|
+
self.enabled = {f: True for f in SHEDDING_CASCADE}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
71
|
+
# Lazy embedding batch (§9.6b)
|
|
72
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class _PendingEmbedding:
|
|
76
|
+
"""One deferred embedding request."""
|
|
77
|
+
|
|
78
|
+
fact_id: str
|
|
79
|
+
text: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
83
|
+
# OverheadBudgetManager
|
|
84
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class OverheadBudgetManager:
|
|
88
|
+
"""Orchestrates feature shedding and lazy embedding batching.
|
|
89
|
+
|
|
90
|
+
Usage::
|
|
91
|
+
|
|
92
|
+
mgr = OverheadBudgetManager(max_overhead_pct=15, batch_size=32)
|
|
93
|
+
|
|
94
|
+
# After each window, update overhead ratio:
|
|
95
|
+
mgr.update_overhead(current_pct=12.5)
|
|
96
|
+
assert mgr.is_feature_enabled("gliner")
|
|
97
|
+
|
|
98
|
+
# When overhead spikes:
|
|
99
|
+
mgr.update_overhead(current_pct=18.0)
|
|
100
|
+
assert not mgr.is_feature_enabled("community_detection") # shed first
|
|
101
|
+
|
|
102
|
+
# Lazy embedding:
|
|
103
|
+
mgr.defer_embedding("f1", "some fact text")
|
|
104
|
+
mgr.defer_embedding("f2", "another fact text")
|
|
105
|
+
batch = mgr.flush_embeddings() # returns pending, clears queue
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
max_overhead_pct: float = 15.0,
|
|
111
|
+
batch_size: int = 32,
|
|
112
|
+
) -> None:
|
|
113
|
+
self._lock = threading.Lock()
|
|
114
|
+
self._max_pct = max_overhead_pct
|
|
115
|
+
self._batch_size = batch_size
|
|
116
|
+
self._shedding = SheddingState()
|
|
117
|
+
self._pending: list[_PendingEmbedding] = []
|
|
118
|
+
self._current_pct: float = 0.0
|
|
119
|
+
self._shed_log: list[str] = []
|
|
120
|
+
|
|
121
|
+
# -- shedding cascade -------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def update_overhead(self, current_pct: float) -> list[str]:
|
|
124
|
+
"""Update the current overhead percentage and shed/restore features.
|
|
125
|
+
|
|
126
|
+
Returns a list of features that changed state (shed or restored).
|
|
127
|
+
"""
|
|
128
|
+
with self._lock:
|
|
129
|
+
self._current_pct = current_pct
|
|
130
|
+
changed: list[str] = []
|
|
131
|
+
|
|
132
|
+
if current_pct > self._max_pct:
|
|
133
|
+
# Shed features cheapest-first until (conceptually) under budget.
|
|
134
|
+
# PROTECTED_INTELLIGENCE features (gliner, uie, discourse) are
|
|
135
|
+
# never shed — they are core ML extraction intelligence.
|
|
136
|
+
for feat in SHEDDING_CASCADE:
|
|
137
|
+
if feat in PROTECTED_INTELLIGENCE:
|
|
138
|
+
continue # never shed ML intelligence
|
|
139
|
+
if self._shedding.enabled[feat]:
|
|
140
|
+
self._shedding.enabled[feat] = False
|
|
141
|
+
msg = f"Shed {feat} (overhead {current_pct:.1f}% > {self._max_pct}%)"
|
|
142
|
+
logger.info(msg)
|
|
143
|
+
self._shed_log.append(msg)
|
|
144
|
+
changed.append(feat)
|
|
145
|
+
break # shed one at a time per update call
|
|
146
|
+
else:
|
|
147
|
+
# Restore features most-expensive-first (reverse order).
|
|
148
|
+
for feat in reversed(SHEDDING_CASCADE):
|
|
149
|
+
if feat in PROTECTED_INTELLIGENCE:
|
|
150
|
+
continue # protected features are always on
|
|
151
|
+
if not self._shedding.enabled[feat]:
|
|
152
|
+
self._shedding.enabled[feat] = True
|
|
153
|
+
msg = f"Restored {feat} (overhead {current_pct:.1f}%)"
|
|
154
|
+
logger.info(msg)
|
|
155
|
+
changed.append(feat)
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
return changed
|
|
159
|
+
|
|
160
|
+
def is_feature_enabled(self, name: str) -> bool:
|
|
161
|
+
"""Check if a feature is currently enabled."""
|
|
162
|
+
with self._lock:
|
|
163
|
+
return self._shedding.enabled.get(name, True)
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def shed_log(self) -> list[str]:
|
|
167
|
+
with self._lock:
|
|
168
|
+
return list(self._shed_log)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def enabled_features(self) -> dict[str, bool]:
|
|
172
|
+
with self._lock:
|
|
173
|
+
return dict(self._shedding.enabled)
|
|
174
|
+
|
|
175
|
+
# -- lazy embedding batch ---------------------------------------------
|
|
176
|
+
|
|
177
|
+
def defer_embedding(self, fact_id: str, text: str) -> bool:
|
|
178
|
+
"""Queue a fact for deferred embedding.
|
|
179
|
+
|
|
180
|
+
Returns ``True`` if the batch is now full and should be flushed.
|
|
181
|
+
"""
|
|
182
|
+
with self._lock:
|
|
183
|
+
self._pending.append(_PendingEmbedding(fact_id=fact_id, text=text))
|
|
184
|
+
return len(self._pending) >= self._batch_size
|
|
185
|
+
|
|
186
|
+
def flush_embeddings(self) -> list[tuple[str, str]]:
|
|
187
|
+
"""Return all pending embeddings as ``(fact_id, text)`` and clear queue."""
|
|
188
|
+
with self._lock:
|
|
189
|
+
batch = [(p.fact_id, p.text) for p in self._pending]
|
|
190
|
+
self._pending.clear()
|
|
191
|
+
return batch
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def pending_embedding_count(self) -> int:
|
|
195
|
+
with self._lock:
|
|
196
|
+
return len(self._pending)
|
|
197
|
+
|
|
198
|
+
# -- introspection ----------------------------------------------------
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def current_overhead_pct(self) -> float:
|
|
202
|
+
return self._current_pct
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def max_overhead_pct(self) -> float:
|
|
206
|
+
return self._max_pct
|
|
207
|
+
|
|
208
|
+
def summary(self) -> dict[str, Any]:
|
|
209
|
+
"""Return a JSON-serializable summary of the manager state."""
|
|
210
|
+
with self._lock:
|
|
211
|
+
return {
|
|
212
|
+
"current_overhead_pct": round(self._current_pct, 2),
|
|
213
|
+
"max_overhead_pct": self._max_pct,
|
|
214
|
+
"features": dict(self._shedding.enabled),
|
|
215
|
+
"pending_embeddings": len(self._pending),
|
|
216
|
+
"shed_log": list(self._shed_log),
|
|
217
|
+
}
|