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,540 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""Tool-mediated context relay — pull-based architecture (§20).
|
|
4
|
+
|
|
5
|
+
Instead of pre-loading ALL context into the prompt (push model), CRP
|
|
6
|
+
exposes its knowledge stores as callable tools. The LLM requests
|
|
7
|
+
context on demand during generation, consuming only what it needs.
|
|
8
|
+
|
|
9
|
+
This is fundamentally different from RAG/prompt injection:
|
|
10
|
+
- **Push (old)**: Pre-compute all context → stuff into prompt → generate
|
|
11
|
+
- **Pull (new)**: Give LLM task + tools → LLM decides what it needs → CRP serves on demand
|
|
12
|
+
|
|
13
|
+
The module provides:
|
|
14
|
+
1. Tool definitions in OpenAI-compatible format
|
|
15
|
+
2. A ContextToolExecutor that routes tool calls to CKF/WarmStore
|
|
16
|
+
3. Integration with the iterative dispatch loop in the orchestrator
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Any, Callable, TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from crp.ckf.fabric import ContextualKnowledgeFabric
|
|
28
|
+
from crp.state.warm_store import WarmStateStore
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("crp.context_tools")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── Tool call data structures ──────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ToolCall:
|
|
37
|
+
"""A single tool call extracted from an LLM response."""
|
|
38
|
+
id: str
|
|
39
|
+
name: str
|
|
40
|
+
arguments: dict[str, Any]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ToolResult:
|
|
45
|
+
"""Result of executing a tool call."""
|
|
46
|
+
tool_call_id: str
|
|
47
|
+
name: str
|
|
48
|
+
content: str
|
|
49
|
+
tokens_used: int = 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── Tool definitions (OpenAI function-calling format) ──────────────────
|
|
53
|
+
|
|
54
|
+
CRP_CONTEXT_TOOLS: list[dict[str, Any]] = [
|
|
55
|
+
{
|
|
56
|
+
"type": "function",
|
|
57
|
+
"function": {
|
|
58
|
+
"name": "crp_retrieve_context",
|
|
59
|
+
"description": (
|
|
60
|
+
"Search for verified context and facts relevant to a query. "
|
|
61
|
+
"Returns ranked facts from the CRP knowledge base. Use this "
|
|
62
|
+
"when you need specific information, data, or evidence to "
|
|
63
|
+
"support your response."
|
|
64
|
+
),
|
|
65
|
+
"parameters": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"properties": {
|
|
68
|
+
"query": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": (
|
|
71
|
+
"What context you need. Be specific about the "
|
|
72
|
+
"topic, concept, or claim you want facts about."
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
"max_results": {
|
|
76
|
+
"type": "integer",
|
|
77
|
+
"description": "Maximum number of facts to return.",
|
|
78
|
+
"default": 5,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
"required": ["query"],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"type": "function",
|
|
87
|
+
"function": {
|
|
88
|
+
"name": "crp_get_document_structure",
|
|
89
|
+
"description": (
|
|
90
|
+
"Get the current document structure and progress: what sections "
|
|
91
|
+
"have been written, what remains, the document outline and map. "
|
|
92
|
+
"Use this to understand the overall state of the document and "
|
|
93
|
+
"plan your continuation."
|
|
94
|
+
),
|
|
95
|
+
"parameters": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"properties": {},
|
|
98
|
+
"required": [],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"type": "function",
|
|
104
|
+
"function": {
|
|
105
|
+
"name": "crp_check_facts",
|
|
106
|
+
"description": (
|
|
107
|
+
"Verify a factual claim against the verified knowledge base. "
|
|
108
|
+
"Returns matching or contradicting facts. Use this to ensure "
|
|
109
|
+
"accuracy before making factual statements."
|
|
110
|
+
),
|
|
111
|
+
"parameters": {
|
|
112
|
+
"type": "object",
|
|
113
|
+
"properties": {
|
|
114
|
+
"claim": {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"description": "The factual claim to verify.",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
"required": ["claim"],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"type": "function",
|
|
125
|
+
"function": {
|
|
126
|
+
"name": "crp_get_related_facts",
|
|
127
|
+
"description": (
|
|
128
|
+
"Given a fact ID or topic, retrieve related facts via graph "
|
|
129
|
+
"traversal. Use this to explore connections between concepts "
|
|
130
|
+
"and find supporting evidence."
|
|
131
|
+
),
|
|
132
|
+
"parameters": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"properties": {
|
|
135
|
+
"topic": {
|
|
136
|
+
"type": "string",
|
|
137
|
+
"description": "Topic or concept to find related facts for.",
|
|
138
|
+
},
|
|
139
|
+
"max_hops": {
|
|
140
|
+
"type": "integer",
|
|
141
|
+
"description": "How many relationship hops to traverse (1-3).",
|
|
142
|
+
"default": 2,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
"required": ["topic"],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"type": "function",
|
|
151
|
+
"function": {
|
|
152
|
+
"name": "crp_get_continuation_state",
|
|
153
|
+
"description": (
|
|
154
|
+
"Get the continuation state: what has been completed, what "
|
|
155
|
+
"gaps remain, requirement coverage, and directives for what "
|
|
156
|
+
"to write next. Use this to understand your mission and what "
|
|
157
|
+
"the document still needs."
|
|
158
|
+
),
|
|
159
|
+
"parameters": {
|
|
160
|
+
"type": "object",
|
|
161
|
+
"properties": {},
|
|
162
|
+
"required": [],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ── Tool name set for validation ───────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
CRP_TOOL_NAMES: frozenset[str] = frozenset(
|
|
172
|
+
t["function"]["name"] for t in CRP_CONTEXT_TOOLS
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ── Context Tool Executor ──────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
class ContextToolExecutor:
|
|
179
|
+
"""Routes tool calls to CRP subsystems and returns results.
|
|
180
|
+
|
|
181
|
+
Wired to:
|
|
182
|
+
- WarmStateStore: ranked facts, structural state, fact lookup
|
|
183
|
+
- CKF: semantic/graph retrieval, community queries
|
|
184
|
+
- Embedding function: for semantic search
|
|
185
|
+
- Continuation state: gap analysis, directives
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
# Safety: cap tokens returned per tool call to prevent context overflow
|
|
189
|
+
MAX_RESULT_TOKENS: int = 2000
|
|
190
|
+
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
warm_store: WarmStateStore,
|
|
194
|
+
ckf: ContextualKnowledgeFabric,
|
|
195
|
+
count_tokens: Callable[[str], int],
|
|
196
|
+
embed_fn: Callable[[str], list[float]] | None = None,
|
|
197
|
+
continuation_state: dict[str, Any] | None = None,
|
|
198
|
+
) -> None:
|
|
199
|
+
self._warm_store = warm_store
|
|
200
|
+
self._ckf = ckf
|
|
201
|
+
self._count_tokens = count_tokens
|
|
202
|
+
self._embed_fn = embed_fn
|
|
203
|
+
self._continuation_state = continuation_state or {}
|
|
204
|
+
self._calls_executed: int = 0
|
|
205
|
+
self._total_tokens_served: int = 0
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def calls_executed(self) -> int:
|
|
209
|
+
return self._calls_executed
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def total_tokens_served(self) -> int:
|
|
213
|
+
return self._total_tokens_served
|
|
214
|
+
|
|
215
|
+
def update_continuation_state(self, state: dict[str, Any]) -> None:
|
|
216
|
+
"""Update continuation state (called between windows)."""
|
|
217
|
+
self._continuation_state = state
|
|
218
|
+
|
|
219
|
+
def execute(self, tool_call: ToolCall) -> ToolResult:
|
|
220
|
+
"""Execute a single tool call and return the result."""
|
|
221
|
+
self._calls_executed += 1
|
|
222
|
+
|
|
223
|
+
handler = {
|
|
224
|
+
"crp_retrieve_context": self._handle_retrieve_context,
|
|
225
|
+
"crp_get_document_structure": self._handle_get_document_structure,
|
|
226
|
+
"crp_check_facts": self._handle_check_facts,
|
|
227
|
+
"crp_get_related_facts": self._handle_get_related_facts,
|
|
228
|
+
"crp_get_continuation_state": self._handle_get_continuation_state,
|
|
229
|
+
}.get(tool_call.name)
|
|
230
|
+
|
|
231
|
+
if handler is None:
|
|
232
|
+
logger.warning("Unknown tool call: %s", tool_call.name)
|
|
233
|
+
content = json.dumps({"error": f"Unknown tool: {tool_call.name}"})
|
|
234
|
+
return ToolResult(
|
|
235
|
+
tool_call_id=tool_call.id,
|
|
236
|
+
name=tool_call.name,
|
|
237
|
+
content=content,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
content = handler(tool_call.arguments)
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
logger.error("Tool %s failed: %s", tool_call.name, exc)
|
|
244
|
+
content = json.dumps({"error": f"Tool execution failed: {exc}"})
|
|
245
|
+
|
|
246
|
+
tokens = self._count_tokens(content)
|
|
247
|
+
self._total_tokens_served += tokens
|
|
248
|
+
|
|
249
|
+
logger.info(
|
|
250
|
+
"Tool %s executed: %d tokens served (total: %d across %d calls)",
|
|
251
|
+
tool_call.name, tokens, self._total_tokens_served, self._calls_executed,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return ToolResult(
|
|
255
|
+
tool_call_id=tool_call.id,
|
|
256
|
+
name=tool_call.name,
|
|
257
|
+
content=content,
|
|
258
|
+
tokens_used=tokens,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def execute_batch(self, tool_calls: list[ToolCall]) -> list[ToolResult]:
|
|
262
|
+
"""Execute multiple tool calls and return results."""
|
|
263
|
+
return [self.execute(tc) for tc in tool_calls]
|
|
264
|
+
|
|
265
|
+
# ── Individual tool handlers ───────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
def _handle_retrieve_context(self, args: dict[str, Any]) -> str:
|
|
268
|
+
"""Search WarmStore + CKF for relevant facts."""
|
|
269
|
+
query = args.get("query", "")
|
|
270
|
+
max_results = min(args.get("max_results", 5), 20) # Cap at 20
|
|
271
|
+
|
|
272
|
+
if not query:
|
|
273
|
+
return json.dumps({"facts": [], "note": "Empty query"})
|
|
274
|
+
|
|
275
|
+
facts_out: list[dict[str, str]] = []
|
|
276
|
+
token_budget = self.MAX_RESULT_TOKENS
|
|
277
|
+
|
|
278
|
+
# Layer 1: WarmStore ranked facts filtered by text relevance
|
|
279
|
+
ranked = self._warm_store.get_ranked_facts(limit=max_results * 3)
|
|
280
|
+
query_lower = query.lower()
|
|
281
|
+
query_terms = set(query_lower.split())
|
|
282
|
+
|
|
283
|
+
# Score facts by term overlap with query
|
|
284
|
+
scored: list[tuple[float, Any]] = []
|
|
285
|
+
for f in ranked:
|
|
286
|
+
text_lower = f.text.lower()
|
|
287
|
+
term_hits = sum(1 for t in query_terms if t in text_lower)
|
|
288
|
+
if term_hits > 0 or query_lower in text_lower:
|
|
289
|
+
score = term_hits / max(len(query_terms), 1)
|
|
290
|
+
if query_lower in text_lower:
|
|
291
|
+
score += 0.5
|
|
292
|
+
scored.append((score, f))
|
|
293
|
+
|
|
294
|
+
scored.sort(key=lambda x: -x[0])
|
|
295
|
+
|
|
296
|
+
for _, f in scored[:max_results]:
|
|
297
|
+
fact_text = f.text
|
|
298
|
+
fact_tokens = self._count_tokens(fact_text)
|
|
299
|
+
if fact_tokens > token_budget:
|
|
300
|
+
break
|
|
301
|
+
token_budget -= fact_tokens
|
|
302
|
+
facts_out.append({
|
|
303
|
+
"id": f.id,
|
|
304
|
+
"text": fact_text,
|
|
305
|
+
"confidence": str(round(f.confidence, 2)),
|
|
306
|
+
"source": f.source_window_id or "unknown",
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
# Layer 2: CKF semantic retrieval if embedding function available
|
|
310
|
+
if len(facts_out) < max_results and self._embed_fn and token_budget > 100:
|
|
311
|
+
try:
|
|
312
|
+
query_emb = self._embed_fn(query)
|
|
313
|
+
seed_ids = {f["id"] for f in facts_out}
|
|
314
|
+
ckf_result = self._ckf.retrieve(
|
|
315
|
+
query_embedding=query_emb,
|
|
316
|
+
seed_ids=seed_ids,
|
|
317
|
+
topic=query[:100],
|
|
318
|
+
budget=max_results - len(facts_out),
|
|
319
|
+
)
|
|
320
|
+
seen_ids = {f["id"] for f in facts_out}
|
|
321
|
+
for cf in ckf_result.facts:
|
|
322
|
+
if cf.id in seen_ids:
|
|
323
|
+
continue
|
|
324
|
+
fact_tokens = self._count_tokens(cf.text)
|
|
325
|
+
if fact_tokens > token_budget:
|
|
326
|
+
break
|
|
327
|
+
token_budget -= fact_tokens
|
|
328
|
+
facts_out.append({
|
|
329
|
+
"id": cf.id,
|
|
330
|
+
"text": cf.text,
|
|
331
|
+
"confidence": str(round(cf.confidence, 2)),
|
|
332
|
+
"source": getattr(cf, "source_window_id", "ckf"),
|
|
333
|
+
})
|
|
334
|
+
if len(facts_out) >= max_results:
|
|
335
|
+
break
|
|
336
|
+
except Exception as exc:
|
|
337
|
+
logger.debug("CKF semantic retrieval skipped: %s", exc)
|
|
338
|
+
|
|
339
|
+
return json.dumps({
|
|
340
|
+
"facts": facts_out,
|
|
341
|
+
"total_available": self._warm_store.fact_count,
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
def _handle_get_document_structure(self, args: dict[str, Any]) -> str:
|
|
345
|
+
"""Return structural state: document map, outline, progress."""
|
|
346
|
+
ss = self._warm_store.structural_state
|
|
347
|
+
structure: dict[str, Any] = {}
|
|
348
|
+
|
|
349
|
+
if hasattr(ss, "to_dict"):
|
|
350
|
+
ss_dict = ss.to_dict()
|
|
351
|
+
structure["document_map"] = ss_dict.get("document_map", "")
|
|
352
|
+
structure["outline"] = ss_dict.get("outline", "")
|
|
353
|
+
structure["sections_completed"] = ss_dict.get("sections_completed", [])
|
|
354
|
+
structure["current_section"] = ss_dict.get("current_section", "")
|
|
355
|
+
structure["word_count"] = ss_dict.get("word_count", 0)
|
|
356
|
+
else:
|
|
357
|
+
# Minimal fallback
|
|
358
|
+
structure["note"] = "Structural state not yet populated"
|
|
359
|
+
|
|
360
|
+
# Add critical state info
|
|
361
|
+
cs = self._warm_store.critical_state
|
|
362
|
+
if hasattr(cs, "to_dict"):
|
|
363
|
+
cs_dict = cs.to_dict()
|
|
364
|
+
structure["goal"] = cs_dict.get("goal", "")
|
|
365
|
+
structure["phase"] = cs_dict.get("phase", "")
|
|
366
|
+
structure["constraints"] = cs_dict.get("constraints", [])
|
|
367
|
+
|
|
368
|
+
# Truncate to token budget
|
|
369
|
+
result = json.dumps(structure)
|
|
370
|
+
tokens = self._count_tokens(result)
|
|
371
|
+
if tokens > self.MAX_RESULT_TOKENS:
|
|
372
|
+
# Truncate the document_map if it's too large
|
|
373
|
+
if "document_map" in structure and structure["document_map"]:
|
|
374
|
+
structure["document_map"] = structure["document_map"][:500] + "..."
|
|
375
|
+
result = json.dumps(structure)
|
|
376
|
+
|
|
377
|
+
return result
|
|
378
|
+
|
|
379
|
+
def _handle_check_facts(self, args: dict[str, Any]) -> str:
|
|
380
|
+
"""Verify a claim against the knowledge base."""
|
|
381
|
+
claim = args.get("claim", "")
|
|
382
|
+
if not claim:
|
|
383
|
+
return json.dumps({"matching": [], "note": "Empty claim"})
|
|
384
|
+
|
|
385
|
+
claim_lower = claim.lower()
|
|
386
|
+
claim_terms = set(claim_lower.split())
|
|
387
|
+
ranked = self._warm_store.get_ranked_facts(limit=50)
|
|
388
|
+
|
|
389
|
+
matching: list[dict[str, str]] = []
|
|
390
|
+
token_budget = self.MAX_RESULT_TOKENS
|
|
391
|
+
|
|
392
|
+
for f in ranked:
|
|
393
|
+
text_lower = f.text.lower()
|
|
394
|
+
# Check for term overlap
|
|
395
|
+
overlap = sum(1 for t in claim_terms if t in text_lower)
|
|
396
|
+
if overlap >= max(1, len(claim_terms) // 3):
|
|
397
|
+
fact_tokens = self._count_tokens(f.text)
|
|
398
|
+
if fact_tokens > token_budget:
|
|
399
|
+
break
|
|
400
|
+
token_budget -= fact_tokens
|
|
401
|
+
matching.append({
|
|
402
|
+
"id": f.id,
|
|
403
|
+
"text": f.text,
|
|
404
|
+
"confidence": str(round(f.confidence, 2)),
|
|
405
|
+
"relevance": "high" if overlap > len(claim_terms) // 2 else "partial",
|
|
406
|
+
})
|
|
407
|
+
if len(matching) >= 5:
|
|
408
|
+
break
|
|
409
|
+
|
|
410
|
+
return json.dumps({
|
|
411
|
+
"matching_facts": matching,
|
|
412
|
+
"facts_searched": min(len(ranked), 50),
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
def _handle_get_related_facts(self, args: dict[str, Any]) -> str:
|
|
416
|
+
"""Graph traversal to find related facts."""
|
|
417
|
+
topic = args.get("topic", "")
|
|
418
|
+
max_hops = min(args.get("max_hops", 2), 3)
|
|
419
|
+
|
|
420
|
+
if not topic:
|
|
421
|
+
return json.dumps({"related": [], "note": "Empty topic"})
|
|
422
|
+
|
|
423
|
+
# Find seed facts matching the topic
|
|
424
|
+
ranked = self._warm_store.get_ranked_facts(limit=30)
|
|
425
|
+
topic_lower = topic.lower()
|
|
426
|
+
seed_ids: set[str] = set()
|
|
427
|
+
|
|
428
|
+
for f in ranked:
|
|
429
|
+
if topic_lower in f.text.lower():
|
|
430
|
+
seed_ids.add(f.id)
|
|
431
|
+
if len(seed_ids) >= 3:
|
|
432
|
+
break
|
|
433
|
+
|
|
434
|
+
if not seed_ids:
|
|
435
|
+
return json.dumps({
|
|
436
|
+
"related": [],
|
|
437
|
+
"note": f"No seed facts found for topic: {topic}",
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
# Graph walk from seeds
|
|
441
|
+
try:
|
|
442
|
+
walk_result = self._ckf.graph_walk(
|
|
443
|
+
seed_ids=seed_ids,
|
|
444
|
+
max_hops=max_hops,
|
|
445
|
+
max_results=10,
|
|
446
|
+
)
|
|
447
|
+
related: list[dict[str, str]] = []
|
|
448
|
+
token_budget = self.MAX_RESULT_TOKENS
|
|
449
|
+
|
|
450
|
+
for fact in walk_result.facts:
|
|
451
|
+
fact_tokens = self._count_tokens(fact.text)
|
|
452
|
+
if fact_tokens > token_budget:
|
|
453
|
+
break
|
|
454
|
+
token_budget -= fact_tokens
|
|
455
|
+
related.append({
|
|
456
|
+
"id": fact.id,
|
|
457
|
+
"text": fact.text,
|
|
458
|
+
"confidence": str(round(fact.confidence, 2)),
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
return json.dumps({
|
|
462
|
+
"related": related,
|
|
463
|
+
"seed_count": len(seed_ids),
|
|
464
|
+
"hops": max_hops,
|
|
465
|
+
})
|
|
466
|
+
except Exception as exc:
|
|
467
|
+
logger.debug("Graph walk failed: %s", exc)
|
|
468
|
+
return json.dumps({
|
|
469
|
+
"related": [],
|
|
470
|
+
"note": f"Graph traversal unavailable: {exc}",
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
def _handle_get_continuation_state(self, args: dict[str, Any]) -> str:
|
|
474
|
+
"""Return continuation state: gaps, directives, progress."""
|
|
475
|
+
if not self._continuation_state:
|
|
476
|
+
return json.dumps({
|
|
477
|
+
"status": "initial",
|
|
478
|
+
"note": "No continuation state yet — this is the first window.",
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
result = json.dumps(self._continuation_state)
|
|
482
|
+
tokens = self._count_tokens(result)
|
|
483
|
+
if tokens > self.MAX_RESULT_TOKENS:
|
|
484
|
+
# Truncate large fields
|
|
485
|
+
state = dict(self._continuation_state)
|
|
486
|
+
for key in ("last_output_summary", "document_map", "full_directive"):
|
|
487
|
+
if key in state and isinstance(state[key], str):
|
|
488
|
+
state[key] = state[key][:300] + "..."
|
|
489
|
+
result = json.dumps(state)
|
|
490
|
+
|
|
491
|
+
return result
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# ── Helper: build tool-aware system prompt ─────────────────────────────
|
|
495
|
+
|
|
496
|
+
def build_tool_system_prompt(original_system: str, fact_count: int) -> str:
|
|
497
|
+
"""Augment the system prompt with context-tool usage guidance.
|
|
498
|
+
|
|
499
|
+
Tells the LLM that it has access to CRP context tools and should
|
|
500
|
+
use them to retrieve verified information instead of relying on
|
|
501
|
+
parametric knowledge alone.
|
|
502
|
+
"""
|
|
503
|
+
tool_guidance = (
|
|
504
|
+
"\n\n--- Context Access Protocol ---\n"
|
|
505
|
+
"You have access to a verified knowledge base managed by the "
|
|
506
|
+
"Context Relay Protocol (CRP). Instead of guessing or relying "
|
|
507
|
+
"solely on your training data, use the provided tools to:\n"
|
|
508
|
+
"- Retrieve verified facts relevant to your response\n"
|
|
509
|
+
"- Check claims against the knowledge base before stating them\n"
|
|
510
|
+
"- Get the document structure and continuation state\n"
|
|
511
|
+
"- Explore related concepts via knowledge graph traversal\n"
|
|
512
|
+
f"\nThe knowledge base currently contains {fact_count} verified facts.\n"
|
|
513
|
+
"Call tools as needed during your response. Each tool call "
|
|
514
|
+
"returns targeted context — more efficient than searching "
|
|
515
|
+
"everything at once.\n"
|
|
516
|
+
"--- End Context Access Protocol ---"
|
|
517
|
+
)
|
|
518
|
+
return original_system + tool_guidance
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# ── Helper: convert tool results to messages ───────────────────────────
|
|
522
|
+
|
|
523
|
+
def tool_results_to_messages(
|
|
524
|
+
assistant_message: dict[str, Any],
|
|
525
|
+
results: list[ToolResult],
|
|
526
|
+
) -> list[dict[str, Any]]:
|
|
527
|
+
"""Build the message sequence for a tool call round-trip.
|
|
528
|
+
|
|
529
|
+
Returns [assistant_msg_with_tool_calls, tool_result_1, tool_result_2, ...].
|
|
530
|
+
This is the OpenAI-compatible format for continuing generation
|
|
531
|
+
after tool calls.
|
|
532
|
+
"""
|
|
533
|
+
messages: list[dict[str, Any]] = [assistant_message]
|
|
534
|
+
for r in results:
|
|
535
|
+
messages.append({
|
|
536
|
+
"role": "tool",
|
|
537
|
+
"tool_call_id": r.tool_call_id,
|
|
538
|
+
"content": r.content,
|
|
539
|
+
})
|
|
540
|
+
return messages
|