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
crp/core/facilitator.py
ADDED
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""CRP Facilitator — LLM-in-the-Loop cognitive engine (§22).
|
|
4
|
+
|
|
5
|
+
╔═══════════════════════════════════════════════════════════════════╗
|
|
6
|
+
║ PARADIGM INVERSION ║
|
|
7
|
+
║ ║
|
|
8
|
+
║ Traditional: User → LLM → Text ║
|
|
9
|
+
║ RAG: User → Context + LLM → Text ║
|
|
10
|
+
║ Agentic: LLM (controller) → Tools → LLM → Output ║
|
|
11
|
+
║ ║
|
|
12
|
+
║ CRP §22: CRP (orchestrator) → LLM (cognitive engine) ║
|
|
13
|
+
║ → CRP (acts on reasoning) → LLM (generates) ║
|
|
14
|
+
║ → CRP (evaluates via LLM) → CRP (curates) ║
|
|
15
|
+
║ ║
|
|
16
|
+
║ The LLM is INSIDE CRP. CRP is the system. ║
|
|
17
|
+
║ The LLM provides reasoning. CRP provides structure. ║
|
|
18
|
+
╚═══════════════════════════════════════════════════════════════════╝
|
|
19
|
+
|
|
20
|
+
The CRPFacilitator uses the LLM for six cognitive functions:
|
|
21
|
+
|
|
22
|
+
§22.1 TASK ANALYSIS — Parse task complexity, domain, knowledge needs
|
|
23
|
+
§22.2 STRATEGY ROUTING — Choose optimal dispatch strategy
|
|
24
|
+
§22.3 FACT SYNTHESIS — Merge/compress/derive new knowledge from facts
|
|
25
|
+
§22.4 OUTPUT EVALUATION — Assess output quality against task intent
|
|
26
|
+
§22.5 MEMORY CURATION — Decide what knowledge to keep/merge/discard
|
|
27
|
+
§22.6 EXECUTION PLANNING — Decompose complex tasks into sub-tasks
|
|
28
|
+
|
|
29
|
+
The facilitator wraps each cognitive call in a structured prompt
|
|
30
|
+
with constrained JSON output. CRP interprets the JSON and acts
|
|
31
|
+
on it — the LLM never touches CRP's internal state directly.
|
|
32
|
+
|
|
33
|
+
Inspired by:
|
|
34
|
+
- Xi et al., "The Rise and Potential of LLM-Based Agents" (2023):
|
|
35
|
+
Agent = Brain(LLM) + Perception + Action
|
|
36
|
+
- Park et al., "Generative Agents" (2023):
|
|
37
|
+
Observation → Planning → Reflection cognitive loop
|
|
38
|
+
- Packer et al., "MemGPT" (2023):
|
|
39
|
+
LLM manages its own memory via structured interrupts
|
|
40
|
+
|
|
41
|
+
CRP differs from all three: the LLM is NOT the controller.
|
|
42
|
+
CRP IS the controller. The LLM is the reasoning module.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import json
|
|
48
|
+
import logging
|
|
49
|
+
import re
|
|
50
|
+
import time
|
|
51
|
+
from dataclasses import dataclass, field
|
|
52
|
+
from typing import Any, Callable, TYPE_CHECKING
|
|
53
|
+
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from crp.extraction.types import Fact
|
|
56
|
+
from crp.providers.base import LLMProvider
|
|
57
|
+
from crp.state.warm_store import WarmStateStore
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger("crp.facilitator")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
63
|
+
# Cognitive output data structures
|
|
64
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class TaskAnalysis:
|
|
68
|
+
"""LLM's analysis of a task — §22.1."""
|
|
69
|
+
complexity: str = "medium" # "simple", "medium", "complex", "multi_part"
|
|
70
|
+
domain: str = "general" # Detected domain/topic
|
|
71
|
+
knowledge_needs: list[str] = field(default_factory=list) # What knowledge the task requires
|
|
72
|
+
expected_output_length: str = "medium" # "short", "medium", "long", "variable"
|
|
73
|
+
requires_factual_grounding: bool = True
|
|
74
|
+
requires_creativity: bool = False
|
|
75
|
+
requires_reasoning: bool = False
|
|
76
|
+
subtasks: list[str] = field(default_factory=list) # If multi_part
|
|
77
|
+
confidence: float = 0.8
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class StrategyDecision:
|
|
82
|
+
"""LLM's routing decision — §22.2."""
|
|
83
|
+
strategy: str = "push" # "push", "pull", "reflexive", "progressive", "stream_augmented", "agentic"
|
|
84
|
+
reasoning: str = "" # Why this strategy was chosen
|
|
85
|
+
envelope_priority: str = "balanced" # "minimal", "balanced", "maximal"
|
|
86
|
+
continuation_likelihood: str = "low" # "none", "low", "medium", "high"
|
|
87
|
+
confidence: float = 0.7
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class SynthesizedKnowledge:
|
|
92
|
+
"""LLM-synthesized knowledge from existing facts — §22.3."""
|
|
93
|
+
summary: str = "" # Coherent synthesis of input facts
|
|
94
|
+
key_insights: list[str] = field(default_factory=list) # Distilled insights
|
|
95
|
+
contradictions: list[str] = field(default_factory=list) # Detected contradictions
|
|
96
|
+
knowledge_gaps: list[str] = field(default_factory=list) # Gaps identified
|
|
97
|
+
redundant_fact_ids: list[str] = field(default_factory=list) # Facts that are redundant
|
|
98
|
+
merged_facts: list[dict[str, str]] = field(default_factory=list) # New merged facts
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class OutputEvaluation:
|
|
103
|
+
"""LLM's evaluation of CRP's output — §22.4."""
|
|
104
|
+
task_completion: float = 0.0 # 0-1: how well the output addresses the task
|
|
105
|
+
factual_accuracy: float = 0.0 # 0-1: estimated accuracy
|
|
106
|
+
coherence: float = 0.0 # 0-1: logical flow and structure
|
|
107
|
+
missing_elements: list[str] = field(default_factory=list) # What's missing
|
|
108
|
+
revision_needed: bool = False
|
|
109
|
+
revision_focus: str = "" # What to focus on in revision
|
|
110
|
+
overall_grade: str = "B" # S/A/B/C/D
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class CurationDecision:
|
|
115
|
+
"""LLM's memory curation recommendations — §22.5."""
|
|
116
|
+
promote_ids: list[str] = field(default_factory=list) # Fact IDs to boost confidence
|
|
117
|
+
demote_ids: list[str] = field(default_factory=list) # Fact IDs to reduce confidence
|
|
118
|
+
merge_groups: list[list[str]] = field(default_factory=list) # Groups of IDs to merge
|
|
119
|
+
discard_ids: list[str] = field(default_factory=list) # Fact IDs to remove
|
|
120
|
+
reasoning: str = ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class ExecutionPlan:
|
|
125
|
+
"""LLM's execution plan for complex tasks — §22.6."""
|
|
126
|
+
steps: list[PlanStep] = field(default_factory=list)
|
|
127
|
+
estimated_windows: int = 1
|
|
128
|
+
parallel_possible: bool = False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class PlanStep:
|
|
133
|
+
"""One step in an execution plan."""
|
|
134
|
+
description: str = ""
|
|
135
|
+
strategy: str = "push" # Which dispatch strategy
|
|
136
|
+
context_needs: list[str] = field(default_factory=list)
|
|
137
|
+
depends_on: list[int] = field(default_factory=list) # Step indices this depends on
|
|
138
|
+
priority: int = 1 # 1=highest
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class FacilitatorMetrics:
|
|
143
|
+
"""Telemetry for facilitator cognitive calls."""
|
|
144
|
+
task_analysis_ms: float = 0.0
|
|
145
|
+
strategy_routing_ms: float = 0.0
|
|
146
|
+
fact_synthesis_ms: float = 0.0
|
|
147
|
+
output_evaluation_ms: float = 0.0
|
|
148
|
+
memory_curation_ms: float = 0.0
|
|
149
|
+
execution_planning_ms: float = 0.0
|
|
150
|
+
total_cognitive_tokens: int = 0
|
|
151
|
+
cognitive_calls: int = 0
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def total_cognitive_ms(self) -> float:
|
|
155
|
+
return (self.task_analysis_ms + self.strategy_routing_ms +
|
|
156
|
+
self.fact_synthesis_ms + self.output_evaluation_ms +
|
|
157
|
+
self.memory_curation_ms + self.execution_planning_ms)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
161
|
+
# Core Facilitator Engine
|
|
162
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
163
|
+
|
|
164
|
+
class CRPFacilitator:
|
|
165
|
+
"""LLM-in-the-Loop cognitive engine for CRP.
|
|
166
|
+
|
|
167
|
+
The facilitator uses the LLM for CRP's internal reasoning:
|
|
168
|
+
- Task analysis (what kind of task is this?)
|
|
169
|
+
- Strategy routing (which dispatch method?)
|
|
170
|
+
- Fact synthesis (merge/compress knowledge)
|
|
171
|
+
- Output evaluation (did we actually help?)
|
|
172
|
+
- Memory curation (what to keep/discard?)
|
|
173
|
+
- Execution planning (how to handle complex tasks?)
|
|
174
|
+
|
|
175
|
+
The LLM never touches CRP state directly. It returns structured
|
|
176
|
+
JSON that CRP interprets and acts upon. This maintains CRP's
|
|
177
|
+
sovereignty over its own subsystems while leveraging the LLM's
|
|
178
|
+
reasoning capability at every decision point.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
# Maximum tokens budget for each cognitive call
|
|
182
|
+
MAX_ANALYSIS_TOKENS = 500
|
|
183
|
+
MAX_ROUTING_TOKENS = 300
|
|
184
|
+
MAX_SYNTHESIS_TOKENS = 800
|
|
185
|
+
MAX_EVALUATION_TOKENS = 500
|
|
186
|
+
MAX_CURATION_TOKENS = 500
|
|
187
|
+
MAX_PLANNING_TOKENS = 600
|
|
188
|
+
|
|
189
|
+
def __init__(
|
|
190
|
+
self,
|
|
191
|
+
provider: LLMProvider,
|
|
192
|
+
count_tokens: Callable[[str], int],
|
|
193
|
+
):
|
|
194
|
+
self._provider = provider
|
|
195
|
+
self._count_tokens = count_tokens
|
|
196
|
+
self._metrics = FacilitatorMetrics()
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def metrics(self) -> FacilitatorMetrics:
|
|
200
|
+
return self._metrics
|
|
201
|
+
|
|
202
|
+
def reset_metrics(self) -> None:
|
|
203
|
+
self._metrics = FacilitatorMetrics()
|
|
204
|
+
|
|
205
|
+
# ───────────────────────────────────────────────────────────────
|
|
206
|
+
# §22.1 TASK ANALYSIS
|
|
207
|
+
# ───────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
def analyze_task(
|
|
210
|
+
self,
|
|
211
|
+
task_input: str,
|
|
212
|
+
system_prompt: str,
|
|
213
|
+
fact_count: int = 0,
|
|
214
|
+
) -> TaskAnalysis:
|
|
215
|
+
"""Use the LLM to analyze what kind of task this is.
|
|
216
|
+
|
|
217
|
+
Instead of CRP guessing task complexity via regex, the LLM
|
|
218
|
+
actually *understands* the task semantically. This feeds
|
|
219
|
+
into every downstream decision: strategy routing, envelope
|
|
220
|
+
budget, continuation thresholds, extraction depth.
|
|
221
|
+
"""
|
|
222
|
+
prompt = _TASK_ANALYSIS_PROMPT.format(
|
|
223
|
+
system_prompt=system_prompt[:300],
|
|
224
|
+
task_input=task_input[:500],
|
|
225
|
+
fact_count=fact_count,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
start = time.monotonic_ns()
|
|
229
|
+
raw = self._cognitive_call(prompt, self.MAX_ANALYSIS_TOKENS)
|
|
230
|
+
self._metrics.task_analysis_ms = (time.monotonic_ns() - start) / 1_000_000
|
|
231
|
+
|
|
232
|
+
return _parse_task_analysis(raw)
|
|
233
|
+
|
|
234
|
+
# ───────────────────────────────────────────────────────────────
|
|
235
|
+
# §22.2 STRATEGY ROUTING
|
|
236
|
+
# ───────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
def route_strategy(
|
|
239
|
+
self,
|
|
240
|
+
analysis: TaskAnalysis,
|
|
241
|
+
fact_count: int,
|
|
242
|
+
available_strategies: list[str] | None = None,
|
|
243
|
+
) -> StrategyDecision:
|
|
244
|
+
"""Use the LLM to choose the optimal dispatch strategy.
|
|
245
|
+
|
|
246
|
+
Given the task analysis and CRP's current state (fact count,
|
|
247
|
+
available strategies), the LLM reasons about which approach
|
|
248
|
+
will produce the best output.
|
|
249
|
+
|
|
250
|
+
This replaces the hardcoded "always use push" default.
|
|
251
|
+
"""
|
|
252
|
+
strategies = available_strategies or [
|
|
253
|
+
"push", "pull", "reflexive", "progressive", "stream_augmented",
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
prompt = _STRATEGY_ROUTING_PROMPT.format(
|
|
257
|
+
complexity=analysis.complexity,
|
|
258
|
+
domain=analysis.domain,
|
|
259
|
+
knowledge_needs=", ".join(analysis.knowledge_needs[:5]),
|
|
260
|
+
output_length=analysis.expected_output_length,
|
|
261
|
+
requires_grounding=analysis.requires_factual_grounding,
|
|
262
|
+
requires_creativity=analysis.requires_creativity,
|
|
263
|
+
fact_count=fact_count,
|
|
264
|
+
available_strategies=", ".join(strategies),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
start = time.monotonic_ns()
|
|
268
|
+
raw = self._cognitive_call(prompt, self.MAX_ROUTING_TOKENS)
|
|
269
|
+
self._metrics.strategy_routing_ms = (time.monotonic_ns() - start) / 1_000_000
|
|
270
|
+
|
|
271
|
+
return _parse_strategy_decision(raw, strategies)
|
|
272
|
+
|
|
273
|
+
# ───────────────────────────────────────────────────────────────
|
|
274
|
+
# §22.3 FACT SYNTHESIS
|
|
275
|
+
# ───────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
def synthesize_facts(
|
|
278
|
+
self,
|
|
279
|
+
facts: list[tuple[str, str, float]], # [(id, text, confidence), ...]
|
|
280
|
+
task_context: str = "",
|
|
281
|
+
) -> SynthesizedKnowledge:
|
|
282
|
+
"""Use the LLM to synthesize coherent knowledge from raw facts.
|
|
283
|
+
|
|
284
|
+
Instead of CRP dumping raw facts into an envelope, the LLM:
|
|
285
|
+
- Merges overlapping facts into coherent statements
|
|
286
|
+
- Identifies contradictions
|
|
287
|
+
- Spots knowledge gaps
|
|
288
|
+
- Ranks by actual relevance to the task
|
|
289
|
+
|
|
290
|
+
This produces BETTER context than raw fact packing.
|
|
291
|
+
"""
|
|
292
|
+
if not facts:
|
|
293
|
+
return SynthesizedKnowledge()
|
|
294
|
+
|
|
295
|
+
facts_block = "\n".join(
|
|
296
|
+
f"[{fid}] (conf={conf:.0%}) {text}"
|
|
297
|
+
for fid, text, conf in facts[:30] # Cap at 30 for token budget
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
prompt = _FACT_SYNTHESIS_PROMPT.format(
|
|
301
|
+
task_context=task_context[:200],
|
|
302
|
+
facts_block=facts_block,
|
|
303
|
+
fact_count=len(facts),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
start = time.monotonic_ns()
|
|
307
|
+
raw = self._cognitive_call(prompt, self.MAX_SYNTHESIS_TOKENS)
|
|
308
|
+
self._metrics.fact_synthesis_ms = (time.monotonic_ns() - start) / 1_000_000
|
|
309
|
+
|
|
310
|
+
return _parse_synthesis(raw)
|
|
311
|
+
|
|
312
|
+
# ───────────────────────────────────────────────────────────────
|
|
313
|
+
# §22.4 OUTPUT EVALUATION
|
|
314
|
+
# ───────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
def evaluate_output(
|
|
317
|
+
self,
|
|
318
|
+
task_input: str,
|
|
319
|
+
output: str,
|
|
320
|
+
facts_used: int = 0,
|
|
321
|
+
) -> OutputEvaluation:
|
|
322
|
+
"""Use the LLM to evaluate CRP's own output.
|
|
323
|
+
|
|
324
|
+
After generation, the LLM assesses whether the output
|
|
325
|
+
actually addresses the task. This replaces the heuristic
|
|
326
|
+
quality scoring (fact count × saturation × token count).
|
|
327
|
+
|
|
328
|
+
The evaluation feeds back into:
|
|
329
|
+
- Whether to trigger continuation
|
|
330
|
+
- Whether to try a different strategy
|
|
331
|
+
- Quality tier classification
|
|
332
|
+
- Memory curation decisions
|
|
333
|
+
"""
|
|
334
|
+
prompt = _OUTPUT_EVALUATION_PROMPT.format(
|
|
335
|
+
task_input=task_input[:300],
|
|
336
|
+
output=output[:1500],
|
|
337
|
+
facts_used=facts_used,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
start = time.monotonic_ns()
|
|
341
|
+
raw = self._cognitive_call(prompt, self.MAX_EVALUATION_TOKENS)
|
|
342
|
+
self._metrics.output_evaluation_ms = (time.monotonic_ns() - start) / 1_000_000
|
|
343
|
+
|
|
344
|
+
return _parse_evaluation(raw)
|
|
345
|
+
|
|
346
|
+
# ───────────────────────────────────────────────────────────────
|
|
347
|
+
# §22.5 MEMORY CURATION
|
|
348
|
+
# ───────────────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
def curate_memory(
|
|
351
|
+
self,
|
|
352
|
+
facts: list[tuple[str, str, float, int]], # [(id, text, confidence, age), ...]
|
|
353
|
+
recent_task: str = "",
|
|
354
|
+
) -> CurationDecision:
|
|
355
|
+
"""Use the LLM to curate CRP's knowledge base.
|
|
356
|
+
|
|
357
|
+
Instead of fixed-interval curation with hardcoded rules,
|
|
358
|
+
the LLM reviews the knowledge base and makes intelligent
|
|
359
|
+
decisions about what to keep, merge, or discard.
|
|
360
|
+
|
|
361
|
+
This is the LLM managing CRP's own memory — the deepest
|
|
362
|
+
integration of LLM-as-cognitive-engine.
|
|
363
|
+
"""
|
|
364
|
+
if not facts:
|
|
365
|
+
return CurationDecision()
|
|
366
|
+
|
|
367
|
+
facts_block = "\n".join(
|
|
368
|
+
f"[{fid}] age={age}w conf={conf:.0%} | {text}"
|
|
369
|
+
for fid, text, conf, age in facts[:25]
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
prompt = _MEMORY_CURATION_PROMPT.format(
|
|
373
|
+
facts_block=facts_block,
|
|
374
|
+
fact_count=len(facts),
|
|
375
|
+
recent_task=recent_task[:200],
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
start = time.monotonic_ns()
|
|
379
|
+
raw = self._cognitive_call(prompt, self.MAX_CURATION_TOKENS)
|
|
380
|
+
self._metrics.memory_curation_ms = (time.monotonic_ns() - start) / 1_000_000
|
|
381
|
+
|
|
382
|
+
return _parse_curation(raw)
|
|
383
|
+
|
|
384
|
+
# ───────────────────────────────────────────────────────────────
|
|
385
|
+
# §22.6 EXECUTION PLANNING
|
|
386
|
+
# ───────────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
def plan_execution(
|
|
389
|
+
self,
|
|
390
|
+
analysis: TaskAnalysis,
|
|
391
|
+
fact_count: int = 0,
|
|
392
|
+
) -> ExecutionPlan:
|
|
393
|
+
"""Use the LLM to create an execution plan for complex tasks.
|
|
394
|
+
|
|
395
|
+
For multi-part or complex tasks, the LLM decomposes the task
|
|
396
|
+
into steps, each with its own dispatch strategy and context
|
|
397
|
+
needs. CRP then orchestrates the plan, running each step
|
|
398
|
+
with the optimal configuration.
|
|
399
|
+
|
|
400
|
+
This replaces the flat "one dispatch" approach with intelligent
|
|
401
|
+
multi-step orchestration.
|
|
402
|
+
"""
|
|
403
|
+
if analysis.complexity in ("simple", "medium"):
|
|
404
|
+
# Simple tasks don't need planning
|
|
405
|
+
return ExecutionPlan(
|
|
406
|
+
steps=[PlanStep(
|
|
407
|
+
description="Direct dispatch",
|
|
408
|
+
strategy="push",
|
|
409
|
+
priority=1,
|
|
410
|
+
)],
|
|
411
|
+
estimated_windows=1,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
subtasks_text = "\n".join(
|
|
415
|
+
f"- {st}" for st in analysis.subtasks
|
|
416
|
+
) if analysis.subtasks else "None identified yet"
|
|
417
|
+
|
|
418
|
+
prompt = _EXECUTION_PLANNING_PROMPT.format(
|
|
419
|
+
complexity=analysis.complexity,
|
|
420
|
+
domain=analysis.domain,
|
|
421
|
+
knowledge_needs=", ".join(analysis.knowledge_needs[:5]),
|
|
422
|
+
subtasks=subtasks_text,
|
|
423
|
+
fact_count=fact_count,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
start = time.monotonic_ns()
|
|
427
|
+
raw = self._cognitive_call(prompt, self.MAX_PLANNING_TOKENS)
|
|
428
|
+
self._metrics.execution_planning_ms = (time.monotonic_ns() - start) / 1_000_000
|
|
429
|
+
|
|
430
|
+
return _parse_execution_plan(raw)
|
|
431
|
+
|
|
432
|
+
# ───────────────────────────────────────────────────────────────
|
|
433
|
+
# Internal: structured LLM call
|
|
434
|
+
# ───────────────────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
def _cognitive_call(self, prompt: str, max_tokens: int) -> str:
|
|
437
|
+
"""Execute a single cognitive LLM call.
|
|
438
|
+
|
|
439
|
+
All cognitive calls go through this bottleneck so we can:
|
|
440
|
+
- Track token usage
|
|
441
|
+
- Enforce budget limits
|
|
442
|
+
- Log cognitive reasoning
|
|
443
|
+
- Handle failures gracefully
|
|
444
|
+
"""
|
|
445
|
+
messages = [
|
|
446
|
+
{
|
|
447
|
+
"role": "system",
|
|
448
|
+
"content": (
|
|
449
|
+
"You are the cognitive engine of CRP (Context Relay Protocol). "
|
|
450
|
+
"You make internal reasoning decisions for the system. "
|
|
451
|
+
"ALWAYS respond with valid JSON matching the requested schema. "
|
|
452
|
+
"Be concise and precise."
|
|
453
|
+
),
|
|
454
|
+
},
|
|
455
|
+
{"role": "user", "content": prompt},
|
|
456
|
+
]
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
response, _ = self._provider.generate_chat(
|
|
460
|
+
messages, max_tokens=max_tokens,
|
|
461
|
+
)
|
|
462
|
+
self._metrics.cognitive_calls += 1
|
|
463
|
+
self._metrics.total_cognitive_tokens += self._count_tokens(response)
|
|
464
|
+
return response
|
|
465
|
+
except Exception as exc:
|
|
466
|
+
logger.warning("Facilitator cognitive call failed: %s", exc)
|
|
467
|
+
return "{}" # Return empty JSON — parsers handle gracefully
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
471
|
+
# Structured prompts for cognitive calls
|
|
472
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
473
|
+
|
|
474
|
+
_TASK_ANALYSIS_PROMPT = """\
|
|
475
|
+
Analyze this task for CRP's internal routing. You are NOT generating the response — \
|
|
476
|
+
you are analyzing what KIND of task this is so CRP can configure itself optimally.
|
|
477
|
+
|
|
478
|
+
SYSTEM PROMPT (context): {system_prompt}
|
|
479
|
+
TASK INPUT: {task_input}
|
|
480
|
+
AVAILABLE FACTS: {fact_count} facts in knowledge base
|
|
481
|
+
|
|
482
|
+
Respond with JSON:
|
|
483
|
+
{{
|
|
484
|
+
"complexity": "simple|medium|complex|multi_part",
|
|
485
|
+
"domain": "<detected domain, e.g. 'software engineering', 'history', 'data analysis'>",
|
|
486
|
+
"knowledge_needs": ["<what knowledge this task requires>", ...],
|
|
487
|
+
"expected_output_length": "short|medium|long|variable",
|
|
488
|
+
"requires_factual_grounding": true/false,
|
|
489
|
+
"requires_creativity": true/false,
|
|
490
|
+
"requires_reasoning": true/false,
|
|
491
|
+
"subtasks": ["<if multi_part, list the sub-tasks>"],
|
|
492
|
+
"confidence": 0.0-1.0
|
|
493
|
+
}}"""
|
|
494
|
+
|
|
495
|
+
_STRATEGY_ROUTING_PROMPT = """\
|
|
496
|
+
Choose the optimal CRP dispatch strategy. You are CRP's routing brain.
|
|
497
|
+
|
|
498
|
+
TASK PROFILE:
|
|
499
|
+
Complexity: {complexity}
|
|
500
|
+
Domain: {domain}
|
|
501
|
+
Knowledge needs: {knowledge_needs}
|
|
502
|
+
Expected output: {output_length}
|
|
503
|
+
Needs grounding: {requires_grounding}
|
|
504
|
+
Needs creativity: {requires_creativity}
|
|
505
|
+
Facts available: {fact_count}
|
|
506
|
+
|
|
507
|
+
AVAILABLE STRATEGIES: {available_strategies}
|
|
508
|
+
- push: Pre-load ALL relevant context (envelope). Best for fact-heavy tasks with enough KB coverage.
|
|
509
|
+
- pull: LLM requests context via tools. Best when LLM knows what it needs.
|
|
510
|
+
- reflexive: Generate blind, then fact-check & refine. Best for accuracy-critical tasks.
|
|
511
|
+
- progressive: Send compact index, expand on demand. Best for broad tasks with many available facts.
|
|
512
|
+
- stream_augmented: Inject context mid-generation. Best for long-form coherent output.
|
|
513
|
+
|
|
514
|
+
Respond with JSON:
|
|
515
|
+
{{
|
|
516
|
+
"strategy": "<chosen strategy>",
|
|
517
|
+
"reasoning": "<1-2 sentences explaining why>",
|
|
518
|
+
"envelope_priority": "minimal|balanced|maximal",
|
|
519
|
+
"continuation_likelihood": "none|low|medium|high",
|
|
520
|
+
"confidence": 0.0-1.0
|
|
521
|
+
}}"""
|
|
522
|
+
|
|
523
|
+
_FACT_SYNTHESIS_PROMPT = """\
|
|
524
|
+
Synthesize these CRP knowledge base facts into coherent knowledge. \
|
|
525
|
+
You are CRP's knowledge engine — merge, deduplicate, and organize.
|
|
526
|
+
|
|
527
|
+
TASK CONTEXT: {task_context}
|
|
528
|
+
FACTS ({fact_count} total):
|
|
529
|
+
{facts_block}
|
|
530
|
+
|
|
531
|
+
Respond with JSON:
|
|
532
|
+
{{
|
|
533
|
+
"summary": "<coherent 2-3 sentence synthesis of the key knowledge>",
|
|
534
|
+
"key_insights": ["<distilled insight 1>", "<insight 2>", ...],
|
|
535
|
+
"contradictions": ["<any contradicting facts>"],
|
|
536
|
+
"knowledge_gaps": ["<what important knowledge is missing>"],
|
|
537
|
+
"redundant_fact_ids": ["<IDs of facts that are duplicates or subsumed>"],
|
|
538
|
+
"merged_facts": [{{"original_ids": ["id1", "id2"], "merged_text": "<new merged fact>"}}]
|
|
539
|
+
}}"""
|
|
540
|
+
|
|
541
|
+
_OUTPUT_EVALUATION_PROMPT = """\
|
|
542
|
+
Evaluate this CRP output. You are CRP's quality brain — assess whether the output \
|
|
543
|
+
actually addresses the task. This is NOT about style — it's about substance.
|
|
544
|
+
|
|
545
|
+
ORIGINAL TASK: {task_input}
|
|
546
|
+
CRP OUTPUT (may be truncated): {output}
|
|
547
|
+
FACTS USED: {facts_used}
|
|
548
|
+
|
|
549
|
+
Respond with JSON:
|
|
550
|
+
{{
|
|
551
|
+
"task_completion": 0.0-1.0,
|
|
552
|
+
"factual_accuracy": 0.0-1.0,
|
|
553
|
+
"coherence": 0.0-1.0,
|
|
554
|
+
"missing_elements": ["<what's missing>"],
|
|
555
|
+
"revision_needed": true/false,
|
|
556
|
+
"revision_focus": "<what to focus on if revision needed>",
|
|
557
|
+
"overall_grade": "S|A|B|C|D"
|
|
558
|
+
}}"""
|
|
559
|
+
|
|
560
|
+
_MEMORY_CURATION_PROMPT = """\
|
|
561
|
+
Review CRP's knowledge base and make curation decisions. You are CRP's memory manager — \
|
|
562
|
+
decide what to keep, merge, or discard.
|
|
563
|
+
|
|
564
|
+
RECENT TASK CONTEXT: {recent_task}
|
|
565
|
+
KNOWLEDGE BASE ({fact_count} facts):
|
|
566
|
+
{facts_block}
|
|
567
|
+
|
|
568
|
+
Respond with JSON:
|
|
569
|
+
{{
|
|
570
|
+
"promote_ids": ["<fact IDs to boost — high value, should rank higher>"],
|
|
571
|
+
"demote_ids": ["<fact IDs to reduce — low value or stale>"],
|
|
572
|
+
"merge_groups": [["<id1>", "<id2>"], ...],
|
|
573
|
+
"discard_ids": ["<fact IDs that are useless or harmful>"],
|
|
574
|
+
"reasoning": "<brief explanation of curation logic>"
|
|
575
|
+
}}"""
|
|
576
|
+
|
|
577
|
+
_EXECUTION_PLANNING_PROMPT = """\
|
|
578
|
+
Create an execution plan for this complex task. You are CRP's planner — \
|
|
579
|
+
decompose the task and assign strategies.
|
|
580
|
+
|
|
581
|
+
TASK PROFILE:
|
|
582
|
+
Complexity: {complexity}
|
|
583
|
+
Domain: {domain}
|
|
584
|
+
Knowledge needs: {knowledge_needs}
|
|
585
|
+
Identified subtasks: {subtasks}
|
|
586
|
+
Facts available: {fact_count}
|
|
587
|
+
|
|
588
|
+
Respond with JSON:
|
|
589
|
+
{{
|
|
590
|
+
"steps": [
|
|
591
|
+
{{
|
|
592
|
+
"description": "<what this step does>",
|
|
593
|
+
"strategy": "push|pull|reflexive|progressive|stream_augmented",
|
|
594
|
+
"context_needs": ["<what knowledge this step needs>"],
|
|
595
|
+
"depends_on": [<step indices this depends on, 0-indexed>],
|
|
596
|
+
"priority": 1-3
|
|
597
|
+
}}
|
|
598
|
+
],
|
|
599
|
+
"estimated_windows": <total generation windows expected>,
|
|
600
|
+
"parallel_possible": true/false
|
|
601
|
+
}}"""
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
605
|
+
# JSON parsers — extract structured data from LLM output
|
|
606
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
607
|
+
|
|
608
|
+
def _extract_json(raw: str) -> dict[str, Any]:
|
|
609
|
+
"""Extract JSON from LLM response, handling markdown fences etc."""
|
|
610
|
+
# Try direct parse first
|
|
611
|
+
text = raw.strip()
|
|
612
|
+
if text.startswith("```"):
|
|
613
|
+
# Strip markdown code fence
|
|
614
|
+
lines = text.split("\n")
|
|
615
|
+
lines = [l for l in lines if not l.strip().startswith("```")]
|
|
616
|
+
text = "\n".join(lines).strip()
|
|
617
|
+
|
|
618
|
+
try:
|
|
619
|
+
return json.loads(text)
|
|
620
|
+
except json.JSONDecodeError:
|
|
621
|
+
# Try to find JSON object in the text
|
|
622
|
+
match = re.search(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', text, re.DOTALL)
|
|
623
|
+
if match:
|
|
624
|
+
try:
|
|
625
|
+
return json.loads(match.group())
|
|
626
|
+
except json.JSONDecodeError:
|
|
627
|
+
pass
|
|
628
|
+
return {}
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _parse_task_analysis(raw: str) -> TaskAnalysis:
|
|
632
|
+
d = _extract_json(raw)
|
|
633
|
+
return TaskAnalysis(
|
|
634
|
+
complexity=d.get("complexity", "medium"),
|
|
635
|
+
domain=d.get("domain", "general"),
|
|
636
|
+
knowledge_needs=d.get("knowledge_needs", []),
|
|
637
|
+
expected_output_length=d.get("expected_output_length", "medium"),
|
|
638
|
+
requires_factual_grounding=d.get("requires_factual_grounding", True),
|
|
639
|
+
requires_creativity=d.get("requires_creativity", False),
|
|
640
|
+
requires_reasoning=d.get("requires_reasoning", False),
|
|
641
|
+
subtasks=d.get("subtasks", []),
|
|
642
|
+
confidence=float(d.get("confidence", 0.5)),
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _parse_strategy_decision(raw: str, valid_strategies: list[str]) -> StrategyDecision:
|
|
647
|
+
d = _extract_json(raw)
|
|
648
|
+
strategy = d.get("strategy", "push")
|
|
649
|
+
if strategy not in valid_strategies:
|
|
650
|
+
strategy = "push" # Safe fallback
|
|
651
|
+
return StrategyDecision(
|
|
652
|
+
strategy=strategy,
|
|
653
|
+
reasoning=d.get("reasoning", ""),
|
|
654
|
+
envelope_priority=d.get("envelope_priority", "balanced"),
|
|
655
|
+
continuation_likelihood=d.get("continuation_likelihood", "low"),
|
|
656
|
+
confidence=float(d.get("confidence", 0.5)),
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _parse_synthesis(raw: str) -> SynthesizedKnowledge:
|
|
661
|
+
d = _extract_json(raw)
|
|
662
|
+
return SynthesizedKnowledge(
|
|
663
|
+
summary=d.get("summary", ""),
|
|
664
|
+
key_insights=d.get("key_insights", []),
|
|
665
|
+
contradictions=d.get("contradictions", []),
|
|
666
|
+
knowledge_gaps=d.get("knowledge_gaps", []),
|
|
667
|
+
redundant_fact_ids=d.get("redundant_fact_ids", []),
|
|
668
|
+
merged_facts=d.get("merged_facts", []),
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _parse_evaluation(raw: str) -> OutputEvaluation:
|
|
673
|
+
d = _extract_json(raw)
|
|
674
|
+
return OutputEvaluation(
|
|
675
|
+
task_completion=float(d.get("task_completion", 0.5)),
|
|
676
|
+
factual_accuracy=float(d.get("factual_accuracy", 0.5)),
|
|
677
|
+
coherence=float(d.get("coherence", 0.5)),
|
|
678
|
+
missing_elements=d.get("missing_elements", []),
|
|
679
|
+
revision_needed=d.get("revision_needed", False),
|
|
680
|
+
revision_focus=d.get("revision_focus", ""),
|
|
681
|
+
overall_grade=d.get("overall_grade", "B"),
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _parse_curation(raw: str) -> CurationDecision:
|
|
686
|
+
d = _extract_json(raw)
|
|
687
|
+
return CurationDecision(
|
|
688
|
+
promote_ids=d.get("promote_ids", []),
|
|
689
|
+
demote_ids=d.get("demote_ids", []),
|
|
690
|
+
merge_groups=d.get("merge_groups", []),
|
|
691
|
+
discard_ids=d.get("discard_ids", []),
|
|
692
|
+
reasoning=d.get("reasoning", ""),
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _parse_execution_plan(raw: str) -> ExecutionPlan:
|
|
697
|
+
d = _extract_json(raw)
|
|
698
|
+
steps = []
|
|
699
|
+
for s in d.get("steps", []):
|
|
700
|
+
steps.append(PlanStep(
|
|
701
|
+
description=s.get("description", ""),
|
|
702
|
+
strategy=s.get("strategy", "push"),
|
|
703
|
+
context_needs=s.get("context_needs", []),
|
|
704
|
+
depends_on=s.get("depends_on", []),
|
|
705
|
+
priority=int(s.get("priority", 1)),
|
|
706
|
+
))
|
|
707
|
+
if not steps:
|
|
708
|
+
steps = [PlanStep(description="Direct dispatch", strategy="push", priority=1)]
|
|
709
|
+
return ExecutionPlan(
|
|
710
|
+
steps=steps,
|
|
711
|
+
estimated_windows=int(d.get("estimated_windows", 1)),
|
|
712
|
+
parallel_possible=d.get("parallel_possible", False),
|
|
713
|
+
)
|