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,613 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""Novel context relay strategies — beyond push and pull (§21).
|
|
4
|
+
|
|
5
|
+
Three fundamentally different approaches to providing context to LLMs:
|
|
6
|
+
|
|
7
|
+
§21.1 REFLEXIVE DISPATCH — Verify-then-Refine
|
|
8
|
+
Generate first with zero context (pure parametric knowledge).
|
|
9
|
+
CRP analyzes the output against its knowledge base, identifying
|
|
10
|
+
factual errors, unsupported claims, and missing information.
|
|
11
|
+
A targeted correction payload is assembled and sent back with
|
|
12
|
+
the model's own output — the model refines with surgical
|
|
13
|
+
precision instead of drowning in pre-loaded context.
|
|
14
|
+
|
|
15
|
+
Analogy: A fact-checker reviews your first draft and hands
|
|
16
|
+
you a marked-up copy with corrections and sources.
|
|
17
|
+
|
|
18
|
+
§21.2 PROGRESSIVE DISCLOSURE — Index → Detail on Demand
|
|
19
|
+
Instead of full facts, send a compact CONTEXT INDEX — one-line
|
|
20
|
+
summaries of every available fact, grouped by topic. The model
|
|
21
|
+
sees WHAT knowledge exists without the full payload. It generates
|
|
22
|
+
with awareness of available resources, and CRP detects which
|
|
23
|
+
indexed items were referenced. A second pass provides full
|
|
24
|
+
detail for only the referenced items.
|
|
25
|
+
|
|
26
|
+
Analogy: A library card catalog — you browse titles first,
|
|
27
|
+
then check out only the books you need.
|
|
28
|
+
|
|
29
|
+
§21.3 STREAM-AUGMENTED GENERATION — Real-time Context Injection
|
|
30
|
+
Stream tokens from the LLM. Buffer into sentences. After each
|
|
31
|
+
sentence, CRP runs real-time fact-matching against the knowledge
|
|
32
|
+
base. When relevant facts are found, generation is paused, the
|
|
33
|
+
partial output + injected context are sent as a new prompt, and
|
|
34
|
+
the model continues from where it left off — now informed.
|
|
35
|
+
|
|
36
|
+
Analogy: A research assistant who watches you write and slides
|
|
37
|
+
relevant papers across the desk exactly when you need them.
|
|
38
|
+
|
|
39
|
+
Each strategy has fundamentally different characteristics:
|
|
40
|
+
- Reflexive: Best for ACCURACY — catches and corrects hallucinations
|
|
41
|
+
- Progressive: Best for EFFICIENCY — minimal token usage, max coverage
|
|
42
|
+
- Stream-aug: Best for COHERENCE — context arrives at point-of-need
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import json
|
|
48
|
+
import logging
|
|
49
|
+
import re
|
|
50
|
+
from dataclasses import dataclass, field
|
|
51
|
+
from typing import Any, Callable, TYPE_CHECKING
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from crp.extraction.types import Fact
|
|
55
|
+
from crp.state.warm_store import WarmStateStore
|
|
56
|
+
from crp.ckf.fabric import ContextualKnowledgeFabric
|
|
57
|
+
|
|
58
|
+
logger = logging.getLogger("crp.relay_strategies")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
62
|
+
# §21.1 REFLEXIVE DISPATCH — Verify-then-Refine
|
|
63
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class FactCorrection:
|
|
67
|
+
"""A correction identified by comparing output against knowledge base."""
|
|
68
|
+
claim_text: str # What the model said
|
|
69
|
+
matching_fact: str # What the knowledge base says
|
|
70
|
+
fact_id: str # Traceable fact ID
|
|
71
|
+
confidence: float # How confident CRP is in the match
|
|
72
|
+
correction_type: str # "contradiction", "unsupported", "partial", "enrichment"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class ReflexiveAnalysis:
|
|
77
|
+
"""Result of analyzing model output against CRP knowledge base."""
|
|
78
|
+
corrections: list[FactCorrection] = field(default_factory=list)
|
|
79
|
+
unsupported_claims: list[str] = field(default_factory=list)
|
|
80
|
+
enrichment_facts: list[str] = field(default_factory=list)
|
|
81
|
+
coverage_score: float = 0.0 # 0-1: how much of the output is KB-supported
|
|
82
|
+
claims_checked: int = 0
|
|
83
|
+
claims_supported: int = 0
|
|
84
|
+
claims_contradicted: int = 0
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def needs_refinement(self) -> bool:
|
|
88
|
+
"""Whether the output needs a refinement pass."""
|
|
89
|
+
return (
|
|
90
|
+
len(self.corrections) > 0
|
|
91
|
+
or len(self.unsupported_claims) > 2
|
|
92
|
+
or self.coverage_score < 0.3
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def analyze_output_against_kb(
|
|
97
|
+
output: str,
|
|
98
|
+
warm_store: WarmStateStore,
|
|
99
|
+
count_tokens: Callable[[str], int],
|
|
100
|
+
embed_fn: Callable[[str], list[float]] | None = None,
|
|
101
|
+
) -> ReflexiveAnalysis:
|
|
102
|
+
"""Compare LLM output against CRP knowledge base.
|
|
103
|
+
|
|
104
|
+
Extracts claims/sentences from the output and cross-references
|
|
105
|
+
each against the WarmStore facts. Identifies:
|
|
106
|
+
- Supported claims (fact backing exists)
|
|
107
|
+
- Contradicted claims (conflicting evidence)
|
|
108
|
+
- Unsupported claims (no evidence either way)
|
|
109
|
+
- Enrichment opportunities (related facts not mentioned)
|
|
110
|
+
"""
|
|
111
|
+
analysis = ReflexiveAnalysis()
|
|
112
|
+
|
|
113
|
+
# Split output into sentences (claim units)
|
|
114
|
+
sentences = _split_into_sentences(output)
|
|
115
|
+
if not sentences:
|
|
116
|
+
return analysis
|
|
117
|
+
|
|
118
|
+
# Get all ranked facts
|
|
119
|
+
all_facts = warm_store.get_ranked_facts(limit=100)
|
|
120
|
+
if not all_facts:
|
|
121
|
+
# No knowledge base — everything is unsupported
|
|
122
|
+
analysis.unsupported_claims = sentences[:10]
|
|
123
|
+
analysis.claims_checked = len(sentences)
|
|
124
|
+
return analysis
|
|
125
|
+
|
|
126
|
+
# Build a simple term index over facts for fast matching
|
|
127
|
+
fact_index: dict[str, list[Any]] = {}
|
|
128
|
+
for f in all_facts:
|
|
129
|
+
for term in _extract_key_terms(f.text):
|
|
130
|
+
fact_index.setdefault(term, []).append(f)
|
|
131
|
+
|
|
132
|
+
supported_count = 0
|
|
133
|
+
for sentence in sentences:
|
|
134
|
+
sentence_stripped = sentence.strip()
|
|
135
|
+
if len(sentence_stripped) < 15: # Skip trivial fragments
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
analysis.claims_checked += 1
|
|
139
|
+
sentence_terms = _extract_key_terms(sentence_stripped)
|
|
140
|
+
|
|
141
|
+
# Find facts with overlapping terms
|
|
142
|
+
candidate_facts: dict[str, int] = {} # fact_id → overlap count
|
|
143
|
+
for term in sentence_terms:
|
|
144
|
+
for f in fact_index.get(term, []):
|
|
145
|
+
candidate_facts[f.id] = candidate_facts.get(f.id, 0) + 1
|
|
146
|
+
|
|
147
|
+
if not candidate_facts:
|
|
148
|
+
analysis.unsupported_claims.append(sentence_stripped[:200])
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
# Score candidates
|
|
152
|
+
best_id = max(candidate_facts, key=candidate_facts.get)
|
|
153
|
+
best_overlap = candidate_facts[best_id]
|
|
154
|
+
best_fact = next(f for f in all_facts if f.id == best_id)
|
|
155
|
+
|
|
156
|
+
overlap_ratio = best_overlap / max(len(sentence_terms), 1)
|
|
157
|
+
|
|
158
|
+
if overlap_ratio >= 0.4:
|
|
159
|
+
# Well-supported claim
|
|
160
|
+
supported_count += 1
|
|
161
|
+
|
|
162
|
+
# Check for contradiction: if the fact says something
|
|
163
|
+
# quantitatively different, flag it
|
|
164
|
+
if _detect_contradiction(sentence_stripped, best_fact.text):
|
|
165
|
+
analysis.corrections.append(FactCorrection(
|
|
166
|
+
claim_text=sentence_stripped[:200],
|
|
167
|
+
matching_fact=best_fact.text,
|
|
168
|
+
fact_id=best_fact.id,
|
|
169
|
+
confidence=best_fact.confidence,
|
|
170
|
+
correction_type="contradiction",
|
|
171
|
+
))
|
|
172
|
+
analysis.claims_contradicted += 1
|
|
173
|
+
else:
|
|
174
|
+
analysis.claims_supported += 1
|
|
175
|
+
elif overlap_ratio >= 0.15:
|
|
176
|
+
# Partial match — could be enriched
|
|
177
|
+
analysis.corrections.append(FactCorrection(
|
|
178
|
+
claim_text=sentence_stripped[:200],
|
|
179
|
+
matching_fact=best_fact.text,
|
|
180
|
+
fact_id=best_fact.id,
|
|
181
|
+
confidence=best_fact.confidence,
|
|
182
|
+
correction_type="enrichment",
|
|
183
|
+
))
|
|
184
|
+
else:
|
|
185
|
+
analysis.unsupported_claims.append(sentence_stripped[:200])
|
|
186
|
+
|
|
187
|
+
# Find enrichment facts: high-confidence facts NOT referenced in output
|
|
188
|
+
output_lower = output.lower()
|
|
189
|
+
for f in all_facts[:20]: # Top-ranked, high-confidence
|
|
190
|
+
if f.confidence >= 0.8:
|
|
191
|
+
fact_terms = _extract_key_terms(f.text)
|
|
192
|
+
# If most fact terms don't appear in output, it's enrichment
|
|
193
|
+
hits = sum(1 for t in fact_terms if t in output_lower)
|
|
194
|
+
if hits < len(fact_terms) * 0.3:
|
|
195
|
+
analysis.enrichment_facts.append(f.text)
|
|
196
|
+
if len(analysis.enrichment_facts) >= 5:
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
analysis.coverage_score = (
|
|
200
|
+
analysis.claims_supported / max(analysis.claims_checked, 1)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
logger.info(
|
|
204
|
+
"Reflexive analysis: %d claims checked, %d supported, %d contradicted, "
|
|
205
|
+
"%d unsupported, %d corrections, coverage=%.2f",
|
|
206
|
+
analysis.claims_checked, analysis.claims_supported,
|
|
207
|
+
analysis.claims_contradicted, len(analysis.unsupported_claims),
|
|
208
|
+
len(analysis.corrections), analysis.coverage_score,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return analysis
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def build_refinement_prompt(
|
|
215
|
+
original_output: str,
|
|
216
|
+
analysis: ReflexiveAnalysis,
|
|
217
|
+
count_tokens: Callable[[str], int],
|
|
218
|
+
max_correction_tokens: int = 3000,
|
|
219
|
+
) -> str:
|
|
220
|
+
"""Build the refinement prompt from reflexive analysis.
|
|
221
|
+
|
|
222
|
+
Returns a structured correction payload that tells the model
|
|
223
|
+
exactly what to fix, with evidence.
|
|
224
|
+
"""
|
|
225
|
+
parts: list[str] = []
|
|
226
|
+
token_budget = max_correction_tokens
|
|
227
|
+
|
|
228
|
+
parts.append("=== FACT-CHECK RESULTS ===")
|
|
229
|
+
parts.append(f"Coverage: {analysis.coverage_score:.0%} of your claims are supported by verified evidence.")
|
|
230
|
+
parts.append(f"Corrections needed: {len(analysis.corrections)}")
|
|
231
|
+
parts.append("")
|
|
232
|
+
|
|
233
|
+
# Corrections (most important)
|
|
234
|
+
if analysis.corrections:
|
|
235
|
+
parts.append("--- CORRECTIONS (verified evidence differs from your output) ---")
|
|
236
|
+
for c in analysis.corrections:
|
|
237
|
+
entry = (
|
|
238
|
+
f"• YOUR CLAIM: \"{c.claim_text}\"\n"
|
|
239
|
+
f" VERIFIED FACT: \"{c.matching_fact}\" (confidence: {c.confidence:.0%})\n"
|
|
240
|
+
f" TYPE: {c.correction_type}"
|
|
241
|
+
)
|
|
242
|
+
entry_tokens = count_tokens(entry)
|
|
243
|
+
if entry_tokens > token_budget:
|
|
244
|
+
break
|
|
245
|
+
token_budget -= entry_tokens
|
|
246
|
+
parts.append(entry)
|
|
247
|
+
parts.append("")
|
|
248
|
+
|
|
249
|
+
# Enrichments (additional facts the model missed)
|
|
250
|
+
if analysis.enrichment_facts and token_budget > 200:
|
|
251
|
+
parts.append("--- ADDITIONAL VERIFIED FACTS (consider incorporating) ---")
|
|
252
|
+
for ef in analysis.enrichment_facts:
|
|
253
|
+
entry = f"• {ef}"
|
|
254
|
+
entry_tokens = count_tokens(entry)
|
|
255
|
+
if entry_tokens > token_budget:
|
|
256
|
+
break
|
|
257
|
+
token_budget -= entry_tokens
|
|
258
|
+
parts.append(entry)
|
|
259
|
+
parts.append("")
|
|
260
|
+
|
|
261
|
+
parts.append("=== END FACT-CHECK RESULTS ===")
|
|
262
|
+
parts.append("")
|
|
263
|
+
parts.append(
|
|
264
|
+
"Please revise your response incorporating the corrections above. "
|
|
265
|
+
"Keep the same structure and style, but fix factual errors and "
|
|
266
|
+
"incorporate the additional verified facts where relevant."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return "\n".join(parts)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
273
|
+
# §21.2 PROGRESSIVE DISCLOSURE — Index → Detail on Demand
|
|
274
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
275
|
+
|
|
276
|
+
@dataclass
|
|
277
|
+
class ContextIndex:
|
|
278
|
+
"""A compact index of available knowledge — titles/summaries only."""
|
|
279
|
+
entries: list[ContextIndexEntry] = field(default_factory=list)
|
|
280
|
+
total_facts: int = 0
|
|
281
|
+
index_tokens: int = 0
|
|
282
|
+
|
|
283
|
+
def to_text(self) -> str:
|
|
284
|
+
"""Render the index as a compact text block for the LLM."""
|
|
285
|
+
if not self.entries:
|
|
286
|
+
return ""
|
|
287
|
+
lines = ["=== AVAILABLE CONTEXT INDEX ==="]
|
|
288
|
+
lines.append(f"({self.total_facts} verified facts available. "
|
|
289
|
+
"Reference items by [ID] to receive full details.)")
|
|
290
|
+
lines.append("")
|
|
291
|
+
for e in self.entries:
|
|
292
|
+
lines.append(f"[{e.ref_id}] {e.summary} (confidence: {e.confidence:.0%})")
|
|
293
|
+
lines.append("")
|
|
294
|
+
lines.append("=== END INDEX ===")
|
|
295
|
+
return "\n".join(lines)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@dataclass
|
|
299
|
+
class ContextIndexEntry:
|
|
300
|
+
"""One entry in the context index."""
|
|
301
|
+
ref_id: str # Short reference ID like "F1", "F2"
|
|
302
|
+
fact_id: str # Real CRP fact ID (for lookup)
|
|
303
|
+
summary: str # One-line summary (compressed)
|
|
304
|
+
confidence: float # Fact confidence
|
|
305
|
+
full_text: str # Full text (not sent until referenced)
|
|
306
|
+
tokens: int # Token cost of full text
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def build_context_index(
|
|
310
|
+
warm_store: WarmStateStore,
|
|
311
|
+
count_tokens: Callable[[str], int],
|
|
312
|
+
max_entries: int = 50,
|
|
313
|
+
max_index_tokens: int = 1500,
|
|
314
|
+
) -> ContextIndex:
|
|
315
|
+
"""Build a compact context index from WarmStore facts.
|
|
316
|
+
|
|
317
|
+
Each entry is compressed to a one-line summary (~15 tokens)
|
|
318
|
+
instead of the full fact text (~50-200 tokens). This gives
|
|
319
|
+
the LLM awareness of ALL available knowledge at ~10% of the
|
|
320
|
+
token cost of sending full facts.
|
|
321
|
+
"""
|
|
322
|
+
ranked_facts = warm_store.get_ranked_facts(limit=max_entries)
|
|
323
|
+
index = ContextIndex(total_facts=warm_store.fact_count)
|
|
324
|
+
|
|
325
|
+
token_budget = max_index_tokens
|
|
326
|
+
for i, f in enumerate(ranked_facts):
|
|
327
|
+
# Compress fact to one-line summary
|
|
328
|
+
summary = _compress_fact_to_summary(f.text)
|
|
329
|
+
ref_id = f"F{i+1}"
|
|
330
|
+
|
|
331
|
+
entry_line = f"[{ref_id}] {summary} (confidence: {f.confidence:.0%})"
|
|
332
|
+
entry_tokens = count_tokens(entry_line)
|
|
333
|
+
|
|
334
|
+
if entry_tokens > token_budget:
|
|
335
|
+
break
|
|
336
|
+
token_budget -= entry_tokens
|
|
337
|
+
|
|
338
|
+
index.entries.append(ContextIndexEntry(
|
|
339
|
+
ref_id=ref_id,
|
|
340
|
+
fact_id=f.id,
|
|
341
|
+
summary=summary,
|
|
342
|
+
confidence=f.confidence,
|
|
343
|
+
full_text=f.text,
|
|
344
|
+
tokens=count_tokens(f.text),
|
|
345
|
+
))
|
|
346
|
+
|
|
347
|
+
index.index_tokens = max_index_tokens - token_budget
|
|
348
|
+
|
|
349
|
+
logger.info(
|
|
350
|
+
"Progressive index: %d entries, %d tokens (from %d total facts)",
|
|
351
|
+
len(index.entries), index.index_tokens, index.total_facts,
|
|
352
|
+
)
|
|
353
|
+
return index
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def detect_index_references(
|
|
357
|
+
output: str,
|
|
358
|
+
index: ContextIndex,
|
|
359
|
+
) -> list[ContextIndexEntry]:
|
|
360
|
+
"""Detect which index entries were referenced in the output.
|
|
361
|
+
|
|
362
|
+
The LLM may reference entries by [F1], [F2] etc., or by
|
|
363
|
+
mentioning key terms from the summary. Both are detected.
|
|
364
|
+
"""
|
|
365
|
+
referenced: list[ContextIndexEntry] = []
|
|
366
|
+
output_lower = output.lower()
|
|
367
|
+
|
|
368
|
+
for entry in index.entries:
|
|
369
|
+
# Check for explicit reference [F1], [F2], etc.
|
|
370
|
+
if f"[{entry.ref_id}]" in output or f"[{entry.ref_id.lower()}]" in output_lower:
|
|
371
|
+
referenced.append(entry)
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
# Check for key term overlap with summary
|
|
375
|
+
summary_terms = _extract_key_terms(entry.summary)
|
|
376
|
+
if summary_terms:
|
|
377
|
+
hits = sum(1 for t in summary_terms if t in output_lower)
|
|
378
|
+
if hits >= max(2, len(summary_terms) * 0.5):
|
|
379
|
+
referenced.append(entry)
|
|
380
|
+
|
|
381
|
+
logger.info(
|
|
382
|
+
"Progressive disclosure: %d of %d index entries referenced",
|
|
383
|
+
len(referenced), len(index.entries),
|
|
384
|
+
)
|
|
385
|
+
return referenced
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def build_detail_injection(
|
|
389
|
+
referenced_entries: list[ContextIndexEntry],
|
|
390
|
+
count_tokens: Callable[[str], int],
|
|
391
|
+
max_tokens: int = 3000,
|
|
392
|
+
) -> str:
|
|
393
|
+
"""Build the detail payload for referenced index entries.
|
|
394
|
+
|
|
395
|
+
Only the entries the model actually referenced get expanded
|
|
396
|
+
to full text. This is maximally efficient — no wasted context.
|
|
397
|
+
"""
|
|
398
|
+
if not referenced_entries:
|
|
399
|
+
return ""
|
|
400
|
+
|
|
401
|
+
parts: list[str] = ["=== EXPANDED CONTEXT (you referenced these) ==="]
|
|
402
|
+
token_budget = max_tokens
|
|
403
|
+
|
|
404
|
+
for entry in referenced_entries:
|
|
405
|
+
detail = f"[{entry.ref_id}] {entry.full_text}"
|
|
406
|
+
detail_tokens = count_tokens(detail)
|
|
407
|
+
if detail_tokens > token_budget:
|
|
408
|
+
break
|
|
409
|
+
token_budget -= detail_tokens
|
|
410
|
+
parts.append(detail)
|
|
411
|
+
|
|
412
|
+
parts.append("=== END EXPANDED CONTEXT ===")
|
|
413
|
+
return "\n".join(parts)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
417
|
+
# §21.3 STREAM-AUGMENTED — Real-time Context Injection
|
|
418
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
419
|
+
|
|
420
|
+
@dataclass
|
|
421
|
+
class AugmentationEvent:
|
|
422
|
+
"""Record of a real-time context injection during streaming."""
|
|
423
|
+
sentence_index: int # Which sentence triggered it
|
|
424
|
+
trigger_text: str # The sentence that triggered injection
|
|
425
|
+
facts_injected: int # How many facts were injected
|
|
426
|
+
injection_tokens: int # Token cost of injection
|
|
427
|
+
resumption_point: str # The text sent as resumption anchor
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@dataclass
|
|
431
|
+
class StreamAugmentationState:
|
|
432
|
+
"""Tracks state during stream-augmented generation."""
|
|
433
|
+
sentence_buffer: str = ""
|
|
434
|
+
sentences_completed: int = 0
|
|
435
|
+
augmentation_events: list[AugmentationEvent] = field(default_factory=list)
|
|
436
|
+
total_injections: int = 0
|
|
437
|
+
total_injection_tokens: int = 0
|
|
438
|
+
accumulated_output: str = ""
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def should_check(self) -> bool:
|
|
442
|
+
"""Check after every N sentences to avoid excessive overhead."""
|
|
443
|
+
return self.sentences_completed % 2 == 0 # Every 2 sentences
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def find_relevant_facts_for_sentence(
|
|
447
|
+
sentence: str,
|
|
448
|
+
warm_store: WarmStateStore,
|
|
449
|
+
already_injected: set[str],
|
|
450
|
+
count_tokens: Callable[[str], int],
|
|
451
|
+
max_facts: int = 3,
|
|
452
|
+
max_tokens: int = 500,
|
|
453
|
+
) -> list[tuple[str, str]]:
|
|
454
|
+
"""Find WarmStore facts relevant to a sentence that haven't been injected yet.
|
|
455
|
+
|
|
456
|
+
Returns [(fact_id, fact_text), ...] — only NEW facts not already served.
|
|
457
|
+
"""
|
|
458
|
+
sentence_terms = _extract_key_terms(sentence)
|
|
459
|
+
if not sentence_terms:
|
|
460
|
+
return []
|
|
461
|
+
|
|
462
|
+
ranked = warm_store.get_ranked_facts(limit=50)
|
|
463
|
+
scored: list[tuple[float, Any]] = []
|
|
464
|
+
|
|
465
|
+
for f in ranked:
|
|
466
|
+
if f.id in already_injected:
|
|
467
|
+
continue
|
|
468
|
+
fact_terms = _extract_key_terms(f.text)
|
|
469
|
+
overlap = sum(1 for t in sentence_terms if t in set(fact_terms))
|
|
470
|
+
if overlap >= max(1, len(sentence_terms) // 4):
|
|
471
|
+
score = overlap / max(len(sentence_terms), 1) * f.confidence
|
|
472
|
+
scored.append((score, f))
|
|
473
|
+
|
|
474
|
+
scored.sort(key=lambda x: -x[0])
|
|
475
|
+
|
|
476
|
+
results: list[tuple[str, str]] = []
|
|
477
|
+
token_budget = max_tokens
|
|
478
|
+
for _, f in scored[:max_facts]:
|
|
479
|
+
ft = count_tokens(f.text)
|
|
480
|
+
if ft > token_budget:
|
|
481
|
+
break
|
|
482
|
+
token_budget -= ft
|
|
483
|
+
results.append((f.id, f.text))
|
|
484
|
+
|
|
485
|
+
return results
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def build_augmented_continuation(
|
|
489
|
+
system_prompt: str,
|
|
490
|
+
partial_output: str,
|
|
491
|
+
injected_facts: list[tuple[str, str]],
|
|
492
|
+
task_input: str,
|
|
493
|
+
) -> list[dict[str, str]]:
|
|
494
|
+
"""Build the message sequence for resuming after a stream augmentation.
|
|
495
|
+
|
|
496
|
+
The model receives:
|
|
497
|
+
1. System prompt (unchanged)
|
|
498
|
+
2. Original task
|
|
499
|
+
3. Its own partial output so far (as assistant message)
|
|
500
|
+
4. Injected context (as system/user interjection)
|
|
501
|
+
5. Instruction to continue from where it left off
|
|
502
|
+
"""
|
|
503
|
+
facts_text = "\n".join(f"• {text}" for _, text in injected_facts)
|
|
504
|
+
|
|
505
|
+
messages: list[dict[str, str]] = [
|
|
506
|
+
{"role": "system", "content": system_prompt},
|
|
507
|
+
{"role": "user", "content": task_input},
|
|
508
|
+
{"role": "assistant", "content": partial_output},
|
|
509
|
+
{
|
|
510
|
+
"role": "user",
|
|
511
|
+
"content": (
|
|
512
|
+
f"[CONTEXT INJECTION — verified facts relevant to what you just wrote]\n"
|
|
513
|
+
f"{facts_text}\n"
|
|
514
|
+
f"[END INJECTION]\n\n"
|
|
515
|
+
f"Continue writing from exactly where you left off. Do not repeat "
|
|
516
|
+
f"what you already wrote. Incorporate the above facts naturally."
|
|
517
|
+
),
|
|
518
|
+
},
|
|
519
|
+
]
|
|
520
|
+
return messages
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
524
|
+
# Shared utilities
|
|
525
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
526
|
+
|
|
527
|
+
# Stop words for term extraction (minimal set for speed)
|
|
528
|
+
_STOP_WORDS: frozenset[str] = frozenset({
|
|
529
|
+
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
|
|
530
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
531
|
+
"should", "may", "might", "shall", "can", "to", "of", "in", "for",
|
|
532
|
+
"on", "with", "at", "by", "from", "as", "into", "through", "during",
|
|
533
|
+
"before", "after", "above", "below", "between", "and", "but", "or",
|
|
534
|
+
"not", "no", "so", "if", "than", "then", "that", "this", "these",
|
|
535
|
+
"those", "it", "its", "he", "she", "they", "we", "you", "i", "my",
|
|
536
|
+
"his", "her", "their", "our", "your", "which", "what", "who", "whom",
|
|
537
|
+
"how", "when", "where", "why", "all", "each", "every", "both",
|
|
538
|
+
"few", "more", "most", "other", "some", "such", "only", "very",
|
|
539
|
+
"also", "just", "about", "up", "out", "over", "own", "same",
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _extract_key_terms(text: str) -> list[str]:
|
|
544
|
+
"""Extract significant terms from text (lowercased, stop-words removed)."""
|
|
545
|
+
words = re.findall(r'[a-zA-Z0-9]+', text.lower())
|
|
546
|
+
return [w for w in words if w not in _STOP_WORDS and len(w) > 2]
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _split_into_sentences(text: str) -> list[str]:
|
|
550
|
+
"""Split text into sentences using regex (handles common cases)."""
|
|
551
|
+
# Split on sentence-ending punctuation followed by space or newline
|
|
552
|
+
sentences = re.split(r'(?<=[.!?])\s+', text)
|
|
553
|
+
# Also split on double newlines (paragraph boundaries)
|
|
554
|
+
result: list[str] = []
|
|
555
|
+
for s in sentences:
|
|
556
|
+
parts = s.split("\n\n")
|
|
557
|
+
result.extend(p.strip() for p in parts if p.strip())
|
|
558
|
+
return result
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _compress_fact_to_summary(text: str) -> str:
|
|
562
|
+
"""Compress a fact to a one-line summary (~10-20 words max).
|
|
563
|
+
|
|
564
|
+
Strategy: take the first clause/sentence, truncate at ~80 chars.
|
|
565
|
+
"""
|
|
566
|
+
# Take first sentence
|
|
567
|
+
match = re.match(r'^([^.!?]+[.!?])', text)
|
|
568
|
+
first_sentence = match.group(1) if match else text
|
|
569
|
+
|
|
570
|
+
if len(first_sentence) <= 80:
|
|
571
|
+
return first_sentence
|
|
572
|
+
|
|
573
|
+
# Truncate at word boundary near 80 chars
|
|
574
|
+
truncated = first_sentence[:80]
|
|
575
|
+
last_space = truncated.rfind(" ")
|
|
576
|
+
if last_space > 40:
|
|
577
|
+
truncated = truncated[:last_space]
|
|
578
|
+
return truncated + "..."
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _detect_contradiction(claim: str, fact: str) -> bool:
|
|
582
|
+
"""Simple heuristic contradiction detection.
|
|
583
|
+
|
|
584
|
+
Checks for numeric disagreements and negation patterns.
|
|
585
|
+
Not perfect — serves as a first-pass filter.
|
|
586
|
+
"""
|
|
587
|
+
# Extract numbers from both
|
|
588
|
+
claim_numbers = set(re.findall(r'\b\d+(?:\.\d+)?\b', claim))
|
|
589
|
+
fact_numbers = set(re.findall(r'\b\d+(?:\.\d+)?\b', fact))
|
|
590
|
+
|
|
591
|
+
# If both mention numbers but they differ significantly, likely contradiction
|
|
592
|
+
if claim_numbers and fact_numbers:
|
|
593
|
+
shared_terms = _extract_key_terms(claim)
|
|
594
|
+
fact_terms = set(_extract_key_terms(fact))
|
|
595
|
+
topic_overlap = sum(1 for t in shared_terms if t in fact_terms)
|
|
596
|
+
if topic_overlap >= 2 and claim_numbers != fact_numbers:
|
|
597
|
+
# Same topic, different numbers — likely contradiction
|
|
598
|
+
return True
|
|
599
|
+
|
|
600
|
+
# Check negation patterns
|
|
601
|
+
claim_lower = claim.lower()
|
|
602
|
+
fact_lower = fact.lower()
|
|
603
|
+
negation_pairs = [
|
|
604
|
+
("not ", ""), ("never ", "always "), ("false", "true"),
|
|
605
|
+
("incorrect", "correct"), ("wrong", "right"),
|
|
606
|
+
]
|
|
607
|
+
for neg, pos in negation_pairs:
|
|
608
|
+
if neg in claim_lower and pos in fact_lower:
|
|
609
|
+
return True
|
|
610
|
+
if pos in claim_lower and neg in fact_lower:
|
|
611
|
+
return True
|
|
612
|
+
|
|
613
|
+
return False
|