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.
Files changed (153) hide show
  1. crp/__init__.py +126 -0
  2. crp/__main__.py +8 -0
  3. crp/_typing.py +27 -0
  4. crp/_version.py +5 -0
  5. crp/adapters.py +31 -0
  6. crp/advanced/__init__.py +40 -0
  7. crp/advanced/auto_ingest.py +400 -0
  8. crp/advanced/cqs.py +235 -0
  9. crp/advanced/cross_window.py +477 -0
  10. crp/advanced/curator.py +265 -0
  11. crp/advanced/feedback.py +146 -0
  12. crp/advanced/hierarchical.py +211 -0
  13. crp/advanced/meta_learning.py +401 -0
  14. crp/advanced/parallel.py +98 -0
  15. crp/advanced/review_cycle.py +329 -0
  16. crp/advanced/scale_mode.py +129 -0
  17. crp/advanced/source_grounding.py +207 -0
  18. crp/ckf/__init__.py +35 -0
  19. crp/ckf/community.py +377 -0
  20. crp/ckf/fabric.py +445 -0
  21. crp/ckf/gc.py +175 -0
  22. crp/ckf/graph_walk.py +87 -0
  23. crp/ckf/merge.py +133 -0
  24. crp/ckf/pattern_query.py +122 -0
  25. crp/ckf/pubsub.py +128 -0
  26. crp/ckf/semantic.py +207 -0
  27. crp/cli/__init__.py +7 -0
  28. crp/cli/main.py +329 -0
  29. crp/cli/sidecar.py +929 -0
  30. crp/cli/startup.py +272 -0
  31. crp/continuation/__init__.py +103 -0
  32. crp/continuation/completion.py +348 -0
  33. crp/continuation/degradation.py +157 -0
  34. crp/continuation/document_map.py +160 -0
  35. crp/continuation/flow.py +109 -0
  36. crp/continuation/gap.py +419 -0
  37. crp/continuation/manager.py +484 -0
  38. crp/continuation/quality_monitor.py +179 -0
  39. crp/continuation/stitch.py +419 -0
  40. crp/continuation/trigger.py +142 -0
  41. crp/continuation/voice.py +157 -0
  42. crp/core/__init__.py +69 -0
  43. crp/core/batch.py +77 -0
  44. crp/core/circuit_breaker.py +116 -0
  45. crp/core/config.py +377 -0
  46. crp/core/context_tools.py +540 -0
  47. crp/core/dispatch_router.py +3977 -0
  48. crp/core/errors.py +128 -0
  49. crp/core/extraction_facade.py +384 -0
  50. crp/core/facilitator.py +713 -0
  51. crp/core/idempotency.py +215 -0
  52. crp/core/orchestrator.py +1435 -0
  53. crp/core/relay_strategies.py +613 -0
  54. crp/core/security_manager.py +140 -0
  55. crp/core/session.py +134 -0
  56. crp/core/task_intent.py +36 -0
  57. crp/core/window.py +363 -0
  58. crp/envelope/__init__.py +30 -0
  59. crp/envelope/builder.py +288 -0
  60. crp/envelope/decomposer.py +236 -0
  61. crp/envelope/formatter.py +168 -0
  62. crp/envelope/packer.py +211 -0
  63. crp/envelope/reranker.py +209 -0
  64. crp/envelope/scoring.py +310 -0
  65. crp/extraction/__init__.py +45 -0
  66. crp/extraction/complexity.py +96 -0
  67. crp/extraction/contradiction.py +132 -0
  68. crp/extraction/pipeline.py +360 -0
  69. crp/extraction/quality_gate.py +237 -0
  70. crp/extraction/stage1_regex.py +173 -0
  71. crp/extraction/stage2_statistical.py +244 -0
  72. crp/extraction/stage3_gliner.py +210 -0
  73. crp/extraction/stage4_uie.py +183 -0
  74. crp/extraction/stage5_discourse.py +175 -0
  75. crp/extraction/stage6_llm.py +178 -0
  76. crp/extraction/structured_output.py +219 -0
  77. crp/extraction/types.py +299 -0
  78. crp/license_guard.py +722 -0
  79. crp/observability/__init__.py +30 -0
  80. crp/observability/audit.py +118 -0
  81. crp/observability/events.py +233 -0
  82. crp/observability/metrics.py +264 -0
  83. crp/observability/quality.py +135 -0
  84. crp/observability/structured_logging.py +81 -0
  85. crp/observability/telemetry.py +117 -0
  86. crp/provenance/__init__.py +314 -0
  87. crp/provenance/_embeddings.py +97 -0
  88. crp/provenance/_types.py +378 -0
  89. crp/provenance/attribution_scorer.py +252 -0
  90. crp/provenance/claim_detector.py +229 -0
  91. crp/provenance/contradiction_detector.py +243 -0
  92. crp/provenance/distortion_detector.py +397 -0
  93. crp/provenance/entailment_verifier.py +358 -0
  94. crp/provenance/fabrication_detector.py +203 -0
  95. crp/provenance/hallucination_scorer.py +320 -0
  96. crp/provenance/omission_analyzer.py +106 -0
  97. crp/provenance/provenance_chain.py +205 -0
  98. crp/provenance/report_generator.py +440 -0
  99. crp/providers/__init__.py +43 -0
  100. crp/providers/anthropic.py +270 -0
  101. crp/providers/base.py +135 -0
  102. crp/providers/custom.py +63 -0
  103. crp/providers/diagnostic.py +251 -0
  104. crp/providers/llamacpp.py +224 -0
  105. crp/providers/manager.py +139 -0
  106. crp/providers/ollama.py +243 -0
  107. crp/providers/openai.py +628 -0
  108. crp/providers/tokenizers.py +48 -0
  109. crp/py.typed +0 -0
  110. crp/resources/__init__.py +53 -0
  111. crp/resources/adaptive_allocator.py +525 -0
  112. crp/resources/cost_model.py +388 -0
  113. crp/resources/overhead_manager.py +217 -0
  114. crp/resources/resource_manager.py +262 -0
  115. crp/schemas/__init__.py +20 -0
  116. crp/schemas/cost-estimate.json +33 -0
  117. crp/schemas/crp-error.json +43 -0
  118. crp/schemas/envelope-preview.json +40 -0
  119. crp/schemas/persisted-state-header.json +27 -0
  120. crp/schemas/quality-report.json +94 -0
  121. crp/schemas/session-handle.json +33 -0
  122. crp/schemas/session-status.json +57 -0
  123. crp/schemas/stream-event.json +18 -0
  124. crp/schemas/task-intent.json +42 -0
  125. crp/security/__init__.py +93 -0
  126. crp/security/audit_trail.py +392 -0
  127. crp/security/binding.py +192 -0
  128. crp/security/compliance.py +813 -0
  129. crp/security/consent.py +593 -0
  130. crp/security/embedding_defense.py +161 -0
  131. crp/security/encryption.py +202 -0
  132. crp/security/injection.py +335 -0
  133. crp/security/integrity.py +267 -0
  134. crp/security/privacy.py +662 -0
  135. crp/security/quarantine.py +249 -0
  136. crp/security/rbac.py +221 -0
  137. crp/security/validation.py +164 -0
  138. crp/state/__init__.py +31 -0
  139. crp/state/cold_storage.py +258 -0
  140. crp/state/compaction.py +263 -0
  141. crp/state/critical_state.py +104 -0
  142. crp/state/event_log.py +313 -0
  143. crp/state/fact.py +189 -0
  144. crp/state/serialization.py +189 -0
  145. crp/state/session_cleanup.py +77 -0
  146. crp/state/snapshot.py +290 -0
  147. crp/state/warm_store.py +346 -0
  148. crprotocol-2.0.0.dist-info/METADATA +1295 -0
  149. crprotocol-2.0.0.dist-info/RECORD +153 -0
  150. crprotocol-2.0.0.dist-info/WHEEL +4 -0
  151. crprotocol-2.0.0.dist-info/entry_points.txt +2 -0
  152. crprotocol-2.0.0.dist-info/licenses/LICENSE.md +170 -0
  153. crprotocol-2.0.0.dist-info/licenses/NOTICE +18 -0
crp/cli/main.py ADDED
@@ -0,0 +1,329 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """CRP CLI entry point — ``crp init | dispatch | ingest | status | preview | serve``.
4
+
5
+ UNIX-friendly: reads stdin, writes JSON to stdout, pipe-compatible.
6
+ Requires the ``click`` package (``pip install crprotocol[cli]``).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import sys
13
+ import uuid
14
+ from typing import Any
15
+
16
+
17
+ def _require_click():
18
+ """Import click with a friendly error if missing."""
19
+ try:
20
+ import click
21
+ return click
22
+ except ImportError as exc:
23
+ raise SystemExit(
24
+ "The CRP CLI requires 'click'. Install with: pip install crprotocol[cli]"
25
+ ) from exc
26
+
27
+
28
+ # ── Session store (in-memory for CLI lifetime) ──────────────────────────
29
+
30
+ _sessions: dict[str, dict] = {}
31
+
32
+
33
+ def _get_session(session_id: str) -> dict:
34
+ """Retrieve a live session or exit with error."""
35
+ if session_id not in _sessions:
36
+ sys.stderr.write(f"Error: unknown session '{session_id}'\n")
37
+ raise SystemExit(1)
38
+ return _sessions[session_id]
39
+
40
+
41
+ # ── Click application ───────────────────────────────────────────────────
42
+
43
+ def cli() -> None:
44
+ """CRP CLI entry point — delegates to click group."""
45
+ # Fix Windows cp1252 encoding — ensure UTF-8 for Unicode output
46
+ if sys.platform == "win32":
47
+ import io
48
+ for stream_name in ("stdout", "stderr"):
49
+ stream = getattr(sys, stream_name)
50
+ if hasattr(stream, "buffer"):
51
+ wrapped = io.TextIOWrapper(
52
+ stream.buffer, encoding="utf-8", errors="replace",
53
+ )
54
+ wrapped.reconfigure(line_buffering=True)
55
+ setattr(sys, stream_name, wrapped)
56
+
57
+ click = _require_click()
58
+
59
+ # Build the Click group lazily so the module can be imported
60
+ # without click being installed (for embedded library use).
61
+ @click.group(context_settings={"help_option_names": ["-h", "--help"]})
62
+ @click.version_option(package_name="crp")
63
+ def _cli():
64
+ """CRP — Context Relay Protocol CLI."""
65
+
66
+ # ── crp init ────────────────────────────────────────────────────
67
+
68
+ @_cli.command()
69
+ @click.option("--model", default="custom", help="Model name for diagnostics.")
70
+ @click.option("--context-window", default=128_000, type=int,
71
+ help="Context window size in tokens.")
72
+ @click.option("--max-output", default=4096, type=int,
73
+ help="Max output tokens per generation.")
74
+ @click.option("--timeout", default=86400, type=int,
75
+ help="Session timeout in seconds.")
76
+ @click.option("--json", "as_json", is_flag=True, help="Output raw JSON.")
77
+ def init(model, context_window, max_output, timeout, as_json):
78
+ """Create a new CRP session and print the session ID."""
79
+ from crp.cli.startup import run_startup
80
+ from crp.providers.custom import CustomProvider
81
+
82
+ # Build a stub provider — real LLM endpoint comes via dispatch
83
+ provider = CustomProvider(
84
+ generate_fn=lambda msgs, **kw: ("", "stop"),
85
+ count_tokens_fn=lambda t: max(1, len(t) // 4),
86
+ context_size=context_window,
87
+ name=model,
88
+ max_output=max_output,
89
+ )
90
+
91
+ result, ctx = run_startup(
92
+ provider=provider,
93
+ config_overrides={"session_timeout": timeout},
94
+ skip_health=True,
95
+ )
96
+
97
+ session_id = str(uuid.uuid4())
98
+ _sessions[session_id] = {
99
+ "id": session_id,
100
+ "provider": provider,
101
+ "config": ctx.get("config"),
102
+ "emitter": ctx.get("emitter"),
103
+ "orchestrator": None, # lazy
104
+ }
105
+
106
+ if as_json:
107
+ click.echo(json.dumps({
108
+ "session_id": session_id,
109
+ "model": model,
110
+ "context_window": context_window,
111
+ "startup_ok": result.ok,
112
+ "startup_ms": round(result.total_ms, 2),
113
+ }))
114
+ else:
115
+ click.echo(session_id)
116
+
117
+ # ── crp dispatch ────────────────────────────────────────────────
118
+
119
+ @_cli.command()
120
+ @click.option("--session", "session_id", default=None, help="Session ID (auto-creates if omitted).")
121
+ @click.option("--task", required=True, help="Task input text.")
122
+ @click.option("--system", default="You are a helpful assistant.",
123
+ help="System prompt.")
124
+ @click.option("--json", "as_json", is_flag=True, help="Output JSON report.")
125
+ def dispatch(session_id, task, system, as_json):
126
+ """Dispatch a task to the LLM via CRP and print the output.
127
+
128
+ If --session is omitted a one-shot session is created automatically
129
+ so that ``crp dispatch --task "Hello"`` works without ``crp init``.
130
+ """
131
+ if session_id is None:
132
+ # Auto-init a one-shot session
133
+ from crp.cli.startup import run_startup
134
+ from crp.providers.custom import CustomProvider
135
+
136
+ provider = CustomProvider(
137
+ generate_fn=lambda msgs, **kw: ("", "stop"),
138
+ count_tokens_fn=lambda t: max(1, len(t) // 4),
139
+ context_size=128_000,
140
+ )
141
+ result, ctx = run_startup(
142
+ provider=provider, skip_health=True,
143
+ )
144
+ session_id = str(uuid.uuid4())
145
+ _sessions[session_id] = {
146
+ "id": session_id,
147
+ "provider": provider,
148
+ "config": ctx.get("config"),
149
+ "emitter": ctx.get("emitter"),
150
+ "orchestrator": None,
151
+ }
152
+ sess = _get_session(session_id)
153
+ orch = _ensure_orchestrator(sess)
154
+
155
+ output, report = orch.dispatch(system, task)
156
+
157
+ if as_json:
158
+ click.echo(json.dumps({
159
+ "session_id": report.session_id,
160
+ "window_id": report.window_id,
161
+ "output": output,
162
+ "facts_extracted": report.facts_extracted,
163
+ }))
164
+ else:
165
+ click.echo(output)
166
+
167
+ # ── crp ingest ──────────────────────────────────────────────────
168
+
169
+ @_cli.command()
170
+ @click.option("--session", "session_id", required=True, help="Session ID.")
171
+ @click.option("--label", default="stdin", help="Source label.")
172
+ @click.option("--json", "as_json", is_flag=True, help="Output JSON.")
173
+ @click.argument("file", default="-", type=click.File("r"))
174
+ def ingest(session_id, label, as_json, file):
175
+ """Ingest raw text (file or stdin) into the session."""
176
+ sess = _get_session(session_id)
177
+ orch = _ensure_orchestrator(sess)
178
+ text = file.read()
179
+ result = orch.ingest(text, source_label=label)
180
+
181
+ if as_json:
182
+ click.echo(json.dumps({
183
+ "facts_extracted": result.facts_extracted,
184
+ "source_label": result.source_label,
185
+ "fact_ids": result.fact_ids,
186
+ }))
187
+ else:
188
+ click.echo(f"Ingested {result.facts_extracted} facts from '{label}'")
189
+
190
+ # ── crp status ──────────────────────────────────────────────────
191
+
192
+ @_cli.command()
193
+ @click.option("--session", "session_id", required=True, help="Session ID.")
194
+ def status(session_id):
195
+ """Print session status as JSON."""
196
+ sess = _get_session(session_id)
197
+ orch = _ensure_orchestrator(sess)
198
+ st = orch.session_status()
199
+ click.echo(json.dumps({
200
+ "session_id": st.session_id,
201
+ "windows_completed": st.windows_completed,
202
+ "total_input_tokens": st.total_input_tokens,
203
+ "total_output_tokens": st.total_output_tokens,
204
+ "facts_in_warm_state": st.facts_in_warm_state,
205
+ "overhead_ratio": st.overhead_ratio,
206
+ }, indent=2))
207
+
208
+ # ── crp preview ─────────────────────────────────────────────────
209
+
210
+ @_cli.command()
211
+ @click.option("--session", "session_id", required=True, help="Session ID.")
212
+ @click.option("--task", required=True, help="Task input text.")
213
+ @click.option("--system", default="You are a helpful assistant.",
214
+ help="System prompt.")
215
+ def preview(session_id, task, system):
216
+ """Preview envelope without dispatching — prints JSON."""
217
+ sess = _get_session(session_id)
218
+ orch = _ensure_orchestrator(sess)
219
+ ep = orch.preview_envelope(system, task)
220
+ click.echo(json.dumps({
221
+ "total_tokens": ep.total_tokens,
222
+ "envelope_tokens": ep.envelope_tokens,
223
+ "generation_reserve": ep.generation_reserve,
224
+ "facts_included": ep.facts_included,
225
+ "facts_available": ep.facts_available,
226
+ "saturation": round(ep.saturation, 4),
227
+ }, indent=2))
228
+
229
+ # ── crp serve (HTTP sidecar for inter-LLM context sharing) ────
230
+
231
+ @_cli.command()
232
+ @click.option("--port", default=9470, type=int, help="Port number.")
233
+ @click.option("--bind-all", is_flag=True, default=False,
234
+ help="Bind to 0.0.0.0 instead of 127.0.0.1. Requires --auth-token unless --allow-unauthenticated is set.")
235
+ @click.option("--auth-token", default=None, help="Bearer token for authentication (strongly recommended).")
236
+ @click.option("--allow-unauthenticated", is_flag=True, default=False,
237
+ help="Allow running without auth token (NOT recommended for --bind-all).")
238
+ @click.option("--max-sessions", default=64, type=int, help="Maximum concurrent sessions.")
239
+ @click.option("--rate-limit", default=120, type=int, help="Max requests per IP per rate window.")
240
+ def serve(port, bind_all, auth_token, allow_unauthenticated, max_sessions, rate_limit):
241
+ """Start the CRP HTTP sidecar for inter-LLM context sharing.
242
+
243
+ The sidecar is OPTIONAL — it is never started automatically.
244
+ It exposes CRP sessions over HTTP, enabling multiple applications
245
+ and LLMs to share extracted knowledge.
246
+
247
+ \b
248
+ SECURITY:
249
+ - Binds to 127.0.0.1 (loopback) by default
250
+ - --auth-token enables bearer-token authentication
251
+ - --bind-all requires --auth-token (or explicit --allow-unauthenticated)
252
+ - Per-IP rate limiting (default: 120 req/60s)
253
+ - Session ownership: only the token that created a session can access it
254
+ - 10 MB request body limit
255
+ - No HTTPS — deploy behind a TLS reverse proxy for production
256
+
257
+ \b
258
+ ENDPOINTS (all 6 dispatch variants + knowledge sharing):
259
+ POST /sessions Create session
260
+ POST /sessions/:id/dispatch Basic dispatch
261
+ POST /sessions/:id/dispatch/tools Tool-mediated dispatch
262
+ POST /sessions/:id/dispatch/reflexive Reflexive dispatch
263
+ POST /sessions/:id/dispatch/progressive Progressive dispatch
264
+ POST /sessions/:id/dispatch/stream-augmented Stream-augmented
265
+ POST /sessions/:id/dispatch/agentic Agentic dispatch
266
+ POST /sessions/:id/ingest Ingest text
267
+ GET /sessions/:id/facts Query facts
268
+ POST /sessions/:id/facts/share SHARE facts between sessions
269
+ POST /sessions/:id/facts/feedback Boost/penalize/reject facts
270
+ POST /sessions/:id/providers Register fallback provider
271
+ POST /sessions/:id/estimate Cost estimation
272
+ GET /sessions/:id/status Session status
273
+ GET /sessions/:id/envelope Envelope preview
274
+ POST /sessions/:id/close Close session
275
+ GET /sessions List sessions (owned only)
276
+ GET /health Health check
277
+
278
+ The /facts/share endpoint is the core differentiator — it enables
279
+ two LLMs using CRP to share each other's extracted knowledge.
280
+ """
281
+ from crp.cli.sidecar import start_sidecar
282
+
283
+ host = "0.0.0.0" if bind_all else "127.0.0.1"
284
+
285
+ # Security gate: --bind-all requires auth unless explicitly waived
286
+ if bind_all and not auth_token and not allow_unauthenticated:
287
+ click.echo("ERROR: --bind-all requires --auth-token for security.", err=True)
288
+ click.echo(" Use --allow-unauthenticated to override (NOT recommended).", err=True)
289
+ raise SystemExit(1)
290
+
291
+ if bind_all and not auth_token:
292
+ click.echo("WARNING: Binding to 0.0.0.0 WITHOUT authentication!", err=True)
293
+ click.echo(" Any process on the network can access CRP sessions.", err=True)
294
+
295
+ click.echo(f"CRP sidecar starting on {host}:{port}")
296
+ click.echo(f" Auth: {'Bearer token required' if auth_token else 'DISABLED'}")
297
+ click.echo(f" Max sessions: {max_sessions}")
298
+ click.echo(f" Rate limit: {rate_limit} req/60s per IP")
299
+ click.echo()
300
+ click.echo(f" Dispatch variants: basic, tools, reflexive, progressive, stream-augmented, agentic")
301
+ click.echo(f" POST /sessions/:id/facts/share ← Inter-LLM context sharing")
302
+ click.echo(f" POST /sessions/:id/facts/feedback ← Fact confidence adjustment")
303
+ click.echo(f" GET /health")
304
+ click.echo()
305
+
306
+ server = start_sidecar(host=host, port=port, auth_token=auth_token,
307
+ max_sessions=max_sessions, rate_limit=rate_limit)
308
+ try:
309
+ server.serve_forever()
310
+ except KeyboardInterrupt:
311
+ click.echo("\nShutting down sidecar...")
312
+ server.shutdown()
313
+
314
+ _cli(standalone_mode=True)
315
+
316
+
317
+ # ── Helpers ─────────────────────────────────────────────────────────────
318
+
319
+ def _ensure_orchestrator(sess: dict) -> Any: # type: ignore[type-arg]
320
+ """Lazily build the orchestrator for this session."""
321
+ if sess["orchestrator"] is None:
322
+ from crp.core.orchestrator import CRPOrchestrator
323
+
324
+ orch = CRPOrchestrator(
325
+ provider=sess["provider"],
326
+ config=sess["config"],
327
+ )
328
+ sess["orchestrator"] = orch
329
+ return sess["orchestrator"]