contextos-memory-runtime 1.0.0rc2__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 (93) hide show
  1. contextos/__init__.py +3 -0
  2. contextos/__main__.py +6 -0
  3. contextos/api/__init__.py +1 -0
  4. contextos/api/routes/__init__.py +1 -0
  5. contextos/api/routes/desktop.py +322 -0
  6. contextos/api/routes/ingest.py +17 -0
  7. contextos/api/routes/memories.py +84 -0
  8. contextos/api/routes/models.py +81 -0
  9. contextos/api/routes/retrieval.py +89 -0
  10. contextos/api/routes/system.py +216 -0
  11. contextos/api/server.py +195 -0
  12. contextos/benchmarks/__init__.py +1 -0
  13. contextos/benchmarks/compilation.py +245 -0
  14. contextos/benchmarks/connectors.py +423 -0
  15. contextos/benchmarks/explainability.py +103 -0
  16. contextos/benchmarks/final.py +406 -0
  17. contextos/benchmarks/graph.py +310 -0
  18. contextos/benchmarks/graph_adversarial.py +525 -0
  19. contextos/benchmarks/mcp.py +324 -0
  20. contextos/benchmarks/model_routing.py +203 -0
  21. contextos/benchmarks/optimization.py +305 -0
  22. contextos/benchmarks/rescue_integration.py +127 -0
  23. contextos/benchmarks/retrieval.py +266 -0
  24. contextos/benchmarks/temporal.py +377 -0
  25. contextos/benchmarks/temporal_hotpath.py +76 -0
  26. contextos/benchmarks/terminal.py +62 -0
  27. contextos/cli/__init__.py +1 -0
  28. contextos/cli/app.py +932 -0
  29. contextos/cli/dashboard.py +174 -0
  30. contextos/cli/formatters.py +299 -0
  31. contextos/config/__init__.py +1 -0
  32. contextos/config/settings.py +160 -0
  33. contextos/connectors/__init__.py +6 -0
  34. contextos/connectors/fake.py +11 -0
  35. contextos/connectors/json_import.py +125 -0
  36. contextos/connectors/local_files.py +102 -0
  37. contextos/connectors/manager.py +293 -0
  38. contextos/connectors/models.py +62 -0
  39. contextos/connectors/protocols.py +11 -0
  40. contextos/core/__init__.py +103 -0
  41. contextos/core/enums.py +489 -0
  42. contextos/core/exceptions.py +293 -0
  43. contextos/core/models.py +1147 -0
  44. contextos/core/protocols.py +549 -0
  45. contextos/daemon/__init__.py +1 -0
  46. contextos/daemon/manager.py +510 -0
  47. contextos/daemon/state.py +127 -0
  48. contextos/daemon/wiring.py +296 -0
  49. contextos/demo.py +217 -0
  50. contextos/embedding/__init__.py +1 -0
  51. contextos/embedding/deterministic.py +76 -0
  52. contextos/embedding/sentence_transformers.py +80 -0
  53. contextos/mcp/__init__.py +5 -0
  54. contextos/mcp/server.py +269 -0
  55. contextos/providers/__init__.py +13 -0
  56. contextos/providers/fake.py +217 -0
  57. contextos/providers/ollama.py +297 -0
  58. contextos/providers/openai_compatible.py +337 -0
  59. contextos/services/__init__.py +1 -0
  60. contextos/services/compilation.py +535 -0
  61. contextos/services/explainability.py +553 -0
  62. contextos/services/extraction.py +311 -0
  63. contextos/services/graph.py +524 -0
  64. contextos/services/graph_retrieval.py +143 -0
  65. contextos/services/ingestion.py +143 -0
  66. contextos/services/inspection.py +174 -0
  67. contextos/services/memory.py +291 -0
  68. contextos/services/model_service.py +409 -0
  69. contextos/services/optimization.py +426 -0
  70. contextos/services/privacy.py +331 -0
  71. contextos/services/retrieval.py +302 -0
  72. contextos/services/retrieval_index.py +88 -0
  73. contextos/services/router.py +302 -0
  74. contextos/services/secret_scanner.py +207 -0
  75. contextos/services/telemetry_query.py +102 -0
  76. contextos/services/temporal.py +500 -0
  77. contextos/services/token_counter.py +222 -0
  78. contextos/storage/__init__.py +1 -0
  79. contextos/storage/connector_repo.py +67 -0
  80. contextos/storage/database.py +497 -0
  81. contextos/storage/event_repo.py +137 -0
  82. contextos/storage/graph_repo.py +228 -0
  83. contextos/storage/lexical/__init__.py +1 -0
  84. contextos/storage/lexical/bm25.py +134 -0
  85. contextos/storage/memory_repo.py +589 -0
  86. contextos/storage/relation_repo.py +80 -0
  87. contextos/storage/telemetry_repo.py +481 -0
  88. contextos/storage/vector/__init__.py +1 -0
  89. contextos/storage/vector/in_memory.py +162 -0
  90. contextos_memory_runtime-1.0.0rc2.dist-info/METADATA +143 -0
  91. contextos_memory_runtime-1.0.0rc2.dist-info/RECORD +93 -0
  92. contextos_memory_runtime-1.0.0rc2.dist-info/WHEEL +4 -0
  93. contextos_memory_runtime-1.0.0rc2.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,174 @@
1
+ """Terminal dashboard rendering with no untrusted terminal control sequences."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from rich.console import Group, RenderableType
9
+ from rich.table import Table
10
+ from rich.text import Text
11
+
12
+ _escape = re.compile(r"\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x1b\x07]*(?:\x07|\x1b\\)|.)")
13
+
14
+
15
+ def safe(value: object, limit: int = 160, allow_newlines: bool = False) -> str:
16
+ if value is None:
17
+ return ""
18
+ text = _escape.sub("", str(value))
19
+ if allow_newlines:
20
+ return "".join(ch for ch in text if ch.isprintable() or ch in ("\n", "\t"))[:limit]
21
+ return "".join(ch for ch in text if ch.isprintable())[:limit]
22
+
23
+
24
+ def _model_kind(model: dict[str, Any]) -> str:
25
+ if not model["enabled"]:
26
+ return "unavailable"
27
+ if model["simulated"]:
28
+ return "simulation"
29
+ return "local" if model["local"] else "remote"
30
+
31
+
32
+ def render_dashboard(
33
+ data: dict[str, Any], model: str | None = None, compare: bool = False
34
+ ) -> Group:
35
+ counts = data["memories"]
36
+ summary = data["summary"]
37
+ models = summary.get("by_model", {})
38
+ bases = data.get("context_measurement_bases", [])
39
+ table = Table(title="ContextOS - Live activity", show_header=False)
40
+ table.add_column("Metric", style="cyan", overflow="crop")
41
+ table.add_column("Value", overflow="crop")
42
+ table.add_row(
43
+ "Memories",
44
+ f"{counts['active']} active | {counts['historical']} historical | "
45
+ f"{counts['expired']} expired",
46
+ )
47
+ temporal = data.get("temporal")
48
+ if temporal:
49
+ table.add_row(
50
+ "Temporal",
51
+ f"{temporal['superseded']} superseded | {temporal['contradicted']} contradicted",
52
+ )
53
+ table.add_row(
54
+ "Connectors",
55
+ ", ".join(
56
+ f"{safe(c['id'])}: {safe(c['status'])} ({c.get('tracked_items', 0)} tracked)"
57
+ for c in data["connectors"]
58
+ ) or "none registered",
59
+ )
60
+ mcp = data.get("mcp")
61
+ if mcp:
62
+ table.add_row(
63
+ "MCP config", "disabled" if not mcp["enabled"] else
64
+ f"configured {safe(mcp['transport'])} | read={mcp['read']} | write={mcp['write']}"
65
+ )
66
+ table.add_row(
67
+ "Models",
68
+ ", ".join(
69
+ f"{safe(item['model'], 32)} ({_model_kind(item)})"
70
+ for item in data.get("models", [])
71
+ ) or "none discoverable",
72
+ )
73
+ table.add_row("Successful invocations", str(summary["total_invocations"]))
74
+ error_count = sum(row.get("errors", 0) for row in data.get("provider_models", []))
75
+ table.add_row("Recorded errors", str(error_count))
76
+ if (model or len(models) == 1) and len(bases) == 1 and bases[0]["source"] != "unknown":
77
+ table.add_row(
78
+ "Context basis",
79
+ f"{safe(bases[0]['source'])} | {safe(bases[0]['tokenizer'])}",
80
+ )
81
+ table.add_row("Context avoided", f"{summary['total_tokens_avoided']:,} context tokens")
82
+ table.add_row("Average reduction", f"{summary['average_reduction_ratio']:.1%} (arithmetic)")
83
+ table.add_row(
84
+ "Weighted reduction",
85
+ f"{summary['weighted_reduction_ratio']:.1%} (candidate-token weighted)",
86
+ )
87
+ elif models:
88
+ table.add_row("Reduction", "Select --model; a single known context-token basis is required")
89
+ else:
90
+ table.add_row("Reduction", "No measured invocations")
91
+ activity = Table(title="Recent invocations - metadata only")
92
+ for name in (
93
+ "Time", "Model", "Status", "Preflight", "Provider input", "Candidate -> compiled",
94
+ "Lex/Dense/Selected", "Context source", "Avoided", "Graph", "Retrieve", "Compile",
95
+ "Provider",
96
+ ):
97
+ activity.add_column(name, overflow="crop")
98
+ for row in data["recent"]:
99
+ activity.add_row(
100
+ safe(row["timestamp"], 19), safe(row["model"], 32), safe(row["status"], 20),
101
+ str(row["preflight_input_tokens"]),
102
+ f"{row['provider_input_tokens']} ["
103
+ f"{safe(row.get('provider_measurement_label', 'UNKNOWN'), 20)}]",
104
+ f"{row['candidate_context_tokens']} -> {row['compiled_context_tokens']}",
105
+ f"{row.get('lexical_candidate_count', 0)}/"
106
+ f"{row.get('dense_candidate_count', 0)}/{row.get('selected_memory_count', 0)}",
107
+ safe(row.get("context_measurement_label", "UNKNOWN"), 20),
108
+ str(row["context_tokens_avoided"]), str(row["graph_expanded_count"]),
109
+ f"{row['retrieval_ms']:.1f} ms", f"{row['compilation_ms']:.1f} ms",
110
+ f"{row.get('provider_ms', 0):.1f} ms",
111
+ )
112
+ if not data["recent"]:
113
+ activity.add_row("No activity", *([""] * 12))
114
+ sections: list[RenderableType] = [table, activity]
115
+ graph = data.get("graph")
116
+ if graph:
117
+ graph_table = Table(title="Graph projection [MEASURED]")
118
+ graph_table.add_column("Nodes")
119
+ graph_table.add_column("Edges")
120
+ graph_table.add_column("Supports")
121
+ graph_table.add_column("State")
122
+ graph_table.add_row(str(graph["nodes"]), str(graph["edges"]), str(graph["supports"]),
123
+ "dirty" if graph["dirty"] else "clean")
124
+ sections.append(graph_table)
125
+ breakdown = data.get("provider_models", [])
126
+ if compare and breakdown:
127
+ models_table = Table(title="Provider + model (successful invocations by context basis)")
128
+ for name in (
129
+ "Provider", "Model", "Runs", "Errors", "Candidate", "Compiled", "Avoided",
130
+ "Reduction", "Basis",
131
+ ):
132
+ models_table.add_column(name, overflow="crop")
133
+ for row in breakdown[:50]:
134
+ ratio = row["weighted_reduction_ratio"]
135
+ models_table.add_row(
136
+ Text(safe(row["provider"])), Text(safe(row["model"])),
137
+ str(row["invocations"]), str(row["errors"]),
138
+ str(row["candidate_context_tokens"]), str(row["compiled_context_tokens"]),
139
+ str(row["context_tokens_avoided"]),
140
+ f"{ratio:.1%}" if ratio is not None else "UNKNOWN",
141
+ Text(
142
+ safe(row["context_measurement_source"])
143
+ + " / "
144
+ + safe(row["context_tokenizer"])
145
+ ),
146
+ )
147
+ sections.append(models_table)
148
+ if (
149
+ len(breakdown) == 1
150
+ and breakdown[0]["invocations"] > 0
151
+ and breakdown[0]["context_measurement_source"] != "unknown"
152
+ ):
153
+ row = breakdown[0]
154
+ candidate = row["candidate_context_tokens"]
155
+ compiled = row["compiled_context_tokens"]
156
+ avoided = row["context_tokens_avoided"]
157
+ maximum = max(candidate, compiled, avoided, 1)
158
+ bars = Table(title="[MEASURED] Context tokens, one tokenizer basis")
159
+ bars.add_column("Stage")
160
+ bars.add_column("Scale")
161
+ bars.add_column("Tokens", justify="right")
162
+ for label, value in (
163
+ ("Candidate", candidate), ("Compiled", compiled), ("Avoided", avoided)
164
+ ):
165
+ bars.add_row(label, "#" * round(24 * value / maximum), f"{value:,}")
166
+ sections.append(bars)
167
+ sections.append(
168
+ Text(
169
+ "Context and preflight counts use the recorded target tokenizer or approximation. "
170
+ "Provider usage is separate; no cross-tokenizer totals are shown.",
171
+ style="dim",
172
+ )
173
+ )
174
+ return Group(*sections)
@@ -0,0 +1,299 @@
1
+ """Rich output formatters for the ContextOS CLI.
2
+
3
+ Centralizes all terminal formatting so CLI commands stay clean.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+ from contextos.cli.dashboard import safe
15
+
16
+ from contextos.core.enums import MemoryStatus, PrivacyLevel
17
+ from contextos.core.models import (
18
+ CompiledContext,
19
+ IngestResult,
20
+ Memory,
21
+ RetrievalResult,
22
+ ScoredMemory,
23
+ SystemStatus,
24
+ TokenStats,
25
+ )
26
+
27
+ console = Console()
28
+ error_console = Console(stderr=True)
29
+
30
+
31
+ # --- Status Colors ---
32
+
33
+ STATUS_COLORS: dict[MemoryStatus, str] = {
34
+ MemoryStatus.ACTIVE: "green",
35
+ MemoryStatus.CANDIDATE: "yellow",
36
+ MemoryStatus.SUPERSEDED: "dim",
37
+ MemoryStatus.CONTRADICTED: "red",
38
+ MemoryStatus.EXPIRED: "dim yellow",
39
+ MemoryStatus.HISTORICAL: "dim",
40
+ MemoryStatus.DELETED: "dim red",
41
+ MemoryStatus.PURGED: "dim red",
42
+ MemoryStatus.MERGED: "dim cyan",
43
+ }
44
+
45
+ PRIVACY_COLORS: dict[PrivacyLevel, str] = {
46
+ PrivacyLevel.PUBLIC: "green",
47
+ PrivacyLevel.PERSONAL: "blue",
48
+ PrivacyLevel.SENSITIVE: "yellow",
49
+ PrivacyLevel.RESTRICTED: "red",
50
+ }
51
+
52
+
53
+ def format_status(status: SystemStatus) -> None:
54
+ """Print system status."""
55
+ table = Table(title="ContextOS Status", show_header=False, box=None, padding=(0, 2))
56
+ table.add_column("Key", style="bold cyan")
57
+ table.add_column("Value")
58
+
59
+ running_text = Text("[OK] Running", style="bold green") if status.daemon_running else Text("[X] Stopped", style="bold red")
60
+ table.add_row("Status", running_text)
61
+ table.add_row("PID", str(status.pid or "-"))
62
+ table.add_row("Uptime", _format_duration(status.uptime_seconds))
63
+ table.add_row("", "")
64
+ table.add_row("Memories", f"{status.active_memories} active / {status.total_memories} total")
65
+ table.add_row("Events", str(status.total_events))
66
+ table.add_row("", "")
67
+ table.add_row("Embedding Model", status.embedding_model or "-")
68
+ table.add_row("Vector Index", f"{status.vector_index_size} vectors")
69
+ table.add_row("BM25 Index", f"{status.bm25_index_size} documents")
70
+ table.add_row("Database Size", _format_bytes(status.database_size_bytes))
71
+ table.add_row("Data Directory", status.data_directory)
72
+
73
+ console.print(table)
74
+
75
+
76
+ def format_stats(stats: TokenStats) -> None:
77
+ """Print token statistics."""
78
+ table = Table(title="Token Statistics", show_header=False, box=None, padding=(0, 2))
79
+ table.add_column("Metric", style="bold cyan")
80
+ table.add_column("Value", justify="right")
81
+
82
+ table.add_row("Total Tokens Stored", f"{stats.total_tokens_stored:,}")
83
+ table.add_row("Tokens Per Memory", f"{stats.tokens_per_memory:.1f}")
84
+ table.add_row("", "")
85
+ table.add_row("Total Compilations", f"{stats.total_compilations:,}")
86
+ table.add_row("Total Tokens Compiled", f"{stats.total_tokens_compiled:,}")
87
+ table.add_row("Total Tokens Saved", f"{stats.total_tokens_saved:,}")
88
+ table.add_row("Avg Compression Ratio", f"{stats.average_compression_ratio:.1%}")
89
+
90
+ console.print(table)
91
+
92
+
93
+ def format_memory(memory: Memory, detailed: bool = False) -> None:
94
+ """Print a single memory."""
95
+ status_color = STATUS_COLORS.get(memory.status, "white")
96
+
97
+ header = f"[bold]{memory.type.value.upper()}[/bold] | "
98
+ header += f"[{status_color}]{memory.status.value}[/{status_color}]"
99
+ header += f" | confidence: {memory.confidence:.0%}"
100
+ header += f" | importance: {memory.importance:.0%}"
101
+
102
+ panel = Panel(
103
+ Text(safe(memory.content, 10_000)),
104
+ title=header,
105
+ subtitle=f"ID: {str(memory.id)[:8]}... | {memory.token_count} tokens | {memory.created_at.strftime('%Y-%m-%d %H:%M')}",
106
+ border_style=status_color,
107
+ padding=(0, 1),
108
+ )
109
+ console.print(panel)
110
+
111
+ if detailed:
112
+ detail_table = Table(show_header=False, box=None, padding=(0, 2))
113
+ detail_table.add_column("Key", style="dim")
114
+ detail_table.add_column("Value")
115
+
116
+ detail_table.add_row("Full ID", str(memory.id))
117
+ detail_table.add_row("Source", Text(safe(f"{memory.source_type} ({memory.source_uri or ''})")))
118
+ detail_table.add_row("Privacy", Text(memory.privacy_level.value, style=PRIVACY_COLORS.get(memory.privacy_level, "white")))
119
+ detail_table.add_row("Tags", Text(safe(", ".join(memory.tags) if memory.tags else "-")))
120
+ detail_table.add_row("Access Count", str(memory.access_count))
121
+ detail_table.add_row("Last Accessed", memory.last_accessed_at.strftime('%Y-%m-%d %H:%M') if memory.last_accessed_at else "-")
122
+ detail_table.add_row("Version", str(memory.version))
123
+ if memory.expires_at:
124
+ detail_table.add_row("Expires", memory.expires_at.strftime('%Y-%m-%d %H:%M'))
125
+ if memory.superseded_by:
126
+ detail_table.add_row("Superseded By", str(memory.superseded_by)[:8] + "...")
127
+ if memory.supersedes:
128
+ detail_table.add_row("Supersedes", str(memory.supersedes)[:8] + "...")
129
+
130
+ console.print(detail_table)
131
+
132
+
133
+ def format_memory_list(memories: list[Memory]) -> None:
134
+ """Print a list of memories as a table."""
135
+ if not memories:
136
+ console.print("[dim]No memories found.[/dim]")
137
+ return
138
+
139
+ table = Table(title=f"{len(memories)} Memories")
140
+ table.add_column("ID", style="dim", width=10)
141
+ table.add_column("Content", max_width=60)
142
+ table.add_column("Type", width=12)
143
+ table.add_column("Status", width=12)
144
+ table.add_column("Conf.", width=6, justify="right")
145
+ table.add_column("Tokens", width=7, justify="right")
146
+ table.add_column("Created", width=12)
147
+
148
+ for mem in memories:
149
+ status_color = STATUS_COLORS.get(mem.status, "white")
150
+ content_preview = safe(mem.content, 57)
151
+
152
+ table.add_row(
153
+ str(mem.id)[:8] + "...",
154
+ Text(content_preview),
155
+ mem.type.value,
156
+ Text(mem.status.value, style=status_color),
157
+ f"{mem.confidence:.0%}",
158
+ str(mem.token_count),
159
+ mem.created_at.strftime("%Y-%m-%d"),
160
+ )
161
+
162
+ console.print(table)
163
+
164
+
165
+ def format_ingest_result(result: IngestResult) -> None:
166
+ """Print ingestion result."""
167
+ if result.memories_created:
168
+ console.print(f"[green][OK][/green] Created {len(result.memories_created)} memor{'y' if len(result.memories_created) == 1 else 'ies'}")
169
+ for mid in result.memories_created:
170
+ console.print(f" [dim]{str(mid)[:8]}...[/dim]")
171
+
172
+ if result.memories_merged:
173
+ console.print(f"[cyan][MERGED][/cyan] Merged into {len(result.memories_merged)} existing memor{'y' if len(result.memories_merged) == 1 else 'ies'}")
174
+
175
+ if result.secrets_detected:
176
+ if result.secrets_redacted:
177
+ console.print("[yellow][!] Secrets detected and redacted[/yellow]")
178
+ else:
179
+ console.print("[yellow][!] Secrets detected[/yellow]")
180
+
181
+ for warning in result.warnings:
182
+ console.print(f"[yellow][!] {warning}[/yellow]")
183
+
184
+
185
+ def format_retrieval_result(result: RetrievalResult, show_trace: bool = False) -> None:
186
+ """Print retrieval results."""
187
+ if not result.memories:
188
+ console.print("[dim]No memories found.[/dim]")
189
+ return
190
+
191
+ console.print(f"\nRetrieved {len(result.memories)} memories for:", Text(safe(result.query)))
192
+
193
+ for i, sm in enumerate(result.memories, 1):
194
+ score_text = f"score: {sm.final_score:.4f}"
195
+ if sm.vector_score is not None:
196
+ score_text += f" (vec: {sm.vector_score:.3f}"
197
+ if sm.bm25_score is not None:
198
+ score_text += f", bm25: {sm.bm25_score:.3f}"
199
+ if sm.vector_score is not None or sm.bm25_score is not None:
200
+ score_text += ")"
201
+
202
+ status_color = STATUS_COLORS.get(sm.memory.status, "white")
203
+ console.print(
204
+ Text(f" {i}. {safe(sm.memory.content, 10_000)}")
205
+ )
206
+ console.print(f" [dim]{score_text} | {sm.memory.type.value} | {sm.memory.token_count} tokens[/dim]")
207
+
208
+ if show_trace:
209
+ console.print("\n[bold]Pipeline Trace[/bold]")
210
+ trace_table = Table(box=None, padding=(0, 1))
211
+ trace_table.add_column("Stage", style="cyan")
212
+ trace_table.add_column("In", justify="right")
213
+ trace_table.add_column("Out", justify="right")
214
+ trace_table.add_column("Latency", justify="right")
215
+
216
+ for stage in result.trace.stages:
217
+ trace_table.add_row(
218
+ stage.stage_name,
219
+ str(stage.input_count),
220
+ str(stage.output_count),
221
+ f"{stage.latency_ms:.1f}ms",
222
+ )
223
+
224
+ trace_table.add_row(
225
+ "[bold]Total[/bold]", "", str(result.trace.total_results),
226
+ f"[bold]{result.trace.total_latency_ms:.1f}ms[/bold]",
227
+ )
228
+ console.print(trace_table)
229
+
230
+
231
+ def format_compiled_context(compiled: CompiledContext, show_context: bool = False) -> None:
232
+ """Print compilation result."""
233
+ console.print("\nContext Compilation for:", Text(safe(compiled.query)))
234
+
235
+ table = Table(show_header=False, box=None, padding=(0, 2))
236
+ table.add_column("Metric", style="cyan")
237
+ table.add_column("Value", justify="right")
238
+
239
+ table.add_row("Budget", f"{compiled.budget:,} tokens")
240
+ table.add_row("Compiled", f"{compiled.total_tokens:,} tokens")
241
+ table.add_row("Compression", f"{compiled.compression_ratio:.1%}")
242
+ table.add_row("Memories Included", f"{compiled.memories_included} / {compiled.memories_considered}")
243
+
244
+ console.print(table)
245
+
246
+ if show_context:
247
+ console.print("\n[bold]Compiled Context:[/bold]")
248
+ console.print(Panel(Text(safe(compiled.context_text, 50_000)), border_style="green"))
249
+
250
+
251
+ def format_doctor_results(results: dict) -> None:
252
+ """Print doctor diagnostic results."""
253
+ console.print(f"\n[bold]ContextOS Doctor[/bold] (v{results.get('version', '?')})\n")
254
+
255
+ for check_name, check_result in results.get("checks", {}).items():
256
+ ok = check_result.get("ok", False)
257
+ icon = "[green][OK][/green]" if ok else "[red][X][/red]"
258
+ console.print(f" {icon} {check_name}")
259
+
260
+ for key, value in check_result.items():
261
+ if key == "ok":
262
+ continue
263
+ if key == "warnings" and value:
264
+ for w in value:
265
+ console.print(f" [yellow][!] {w}[/yellow]")
266
+ elif key != "warnings":
267
+ console.print(f" [dim]{key}: {value}[/dim]")
268
+
269
+ overall = results.get("overall", False)
270
+ console.print()
271
+ if overall:
272
+ console.print("[bold green]All checks passed.[/bold green]")
273
+ else:
274
+ console.print("[bold red]Some checks failed.[/bold red]")
275
+
276
+
277
+ # --- Utilities ---
278
+
279
+
280
+ def _format_duration(seconds: float) -> str:
281
+ """Format seconds into human-readable duration."""
282
+ if seconds < 60:
283
+ return f"{seconds:.0f}s"
284
+ minutes = int(seconds // 60)
285
+ secs = int(seconds % 60)
286
+ if minutes < 60:
287
+ return f"{minutes}m {secs}s"
288
+ hours = minutes // 60
289
+ mins = minutes % 60
290
+ return f"{hours}h {mins}m"
291
+
292
+
293
+ def _format_bytes(bytes_val: int) -> str:
294
+ """Format bytes into human-readable size."""
295
+ for unit in ("B", "KB", "MB", "GB"):
296
+ if bytes_val < 1024:
297
+ return f"{bytes_val:.1f} {unit}"
298
+ bytes_val /= 1024 # type: ignore
299
+ return f"{bytes_val:.1f} TB"
@@ -0,0 +1 @@
1
+ """Config package for ContextOS."""
@@ -0,0 +1,160 @@
1
+ """Configuration management for ContextOS.
2
+
3
+ Uses Pydantic Settings for config validation with TOML file support.
4
+ Config is loaded from ~/.config/contextos/config.toml (XDG-compliant).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import platform
10
+ import re
11
+ from pathlib import Path
12
+ from typing import Literal
13
+
14
+ from pydantic import Field, field_validator, model_validator
15
+ from pydantic_settings import BaseSettings
16
+
17
+
18
+ def _default_data_dir() -> Path:
19
+ """Platform-appropriate default data directory."""
20
+ system = platform.system()
21
+ if system == "Windows":
22
+ base = Path.home() / "AppData" / "Local" / "contextos"
23
+ elif system == "Darwin":
24
+ base = Path.home() / "Library" / "Application Support" / "contextos"
25
+ else:
26
+ # Linux / other Unix — XDG
27
+ xdg_data = Path.home() / ".local" / "share"
28
+ base = xdg_data / "contextos"
29
+ return base
30
+
31
+
32
+ def _default_config_dir() -> Path:
33
+ """Platform-appropriate default config directory."""
34
+ system = platform.system()
35
+ if system == "Windows":
36
+ return Path.home() / "AppData" / "Local" / "contextos"
37
+ elif system == "Darwin":
38
+ return Path.home() / "Library" / "Application Support" / "contextos"
39
+ else:
40
+ xdg_config = Path.home() / ".config"
41
+ return xdg_config / "contextos"
42
+
43
+
44
+ class DaemonConfig(BaseSettings):
45
+ """Daemon process configuration."""
46
+ host: str = "127.0.0.1"
47
+ port: int = 52411
48
+ log_level: str = "info"
49
+ data_dir: Path = Field(default_factory=_default_data_dir)
50
+ config_dir: Path = Field(default_factory=_default_config_dir)
51
+ readiness_timeout: float = Field(default=30.0, ge=1, le=300, allow_inf_nan=False)
52
+ lock_timeout: float = Field(default=45.0, ge=1, le=600, allow_inf_nan=False)
53
+
54
+ @model_validator(mode="after")
55
+ def bounded_startup(self):
56
+ if self.lock_timeout <= self.readiness_timeout + 5:
57
+ raise ValueError("lock_timeout must exceed readiness_timeout by more than 5 seconds")
58
+ return self
59
+
60
+ @field_validator("host")
61
+ @classmethod
62
+ def loopback_only(cls, value: str) -> str:
63
+ if value not in {"127.0.0.1", "::1", "localhost"}:
64
+ raise ValueError("ContextOS daemon must bind to loopback")
65
+ return value
66
+
67
+
68
+ class EmbeddingConfig(BaseSettings):
69
+ """Embedding model configuration."""
70
+ model: str = "deterministic"
71
+ device: str = "cpu"
72
+ batch_size: int = 32
73
+
74
+
75
+ class TokenCounterConfig(BaseSettings):
76
+ """Offline approximation by default; exact tokenizers require explicit setup."""
77
+
78
+ encoding: Literal["deterministic", "cl100k_base", "o200k_base"] = "deterministic"
79
+
80
+
81
+ class RetrievalConfig(BaseSettings):
82
+ """Default retrieval parameters."""
83
+ vector_top_k: int = 20
84
+ bm25_top_k: int = 20
85
+ rrf_k: int = 60
86
+ default_budget: int = 4000
87
+
88
+
89
+ class PrivacyConfig(BaseSettings):
90
+ """Privacy and security configuration."""
91
+ secret_detection: str = "strict" # "strict", "redact", "warn"
92
+ default_privacy_level: str = "personal"
93
+ entropy_threshold: float = 4.5
94
+
95
+
96
+ class LLMConfig(BaseSettings):
97
+ """LLM provider configuration."""
98
+ provider: str = "none"
99
+ model: str = ""
100
+ api_key_env: str = ""
101
+
102
+
103
+ class MCPConfig(BaseSettings):
104
+ """Local MCP exposure; writes and destructive operations fail closed."""
105
+
106
+ enabled: bool = False
107
+ transport: str = "stdio"
108
+ allow_read: bool = True
109
+ allow_write: bool = False
110
+ allow_delete: bool = False
111
+ allow_telemetry: bool = True
112
+ max_input_chars: int = Field(default=10_000, ge=1, le=100_000)
113
+ max_search_results: int = Field(default=25, ge=1, le=200)
114
+ max_history_entries: int = Field(default=50, ge=1, le=200)
115
+ max_graph_nodes: int = Field(default=100, ge=1, le=1_000)
116
+ max_graph_edges: int = Field(default=250, ge=1, le=2_500)
117
+ max_compilation_tokens: int = Field(default=8_000, ge=1, le=32_000)
118
+
119
+
120
+ class ConnectorConfig(BaseSettings):
121
+ """Explicit local sources only; no discovered paths or credentials."""
122
+
123
+ local_files: dict[str, list[Path]] = Field(default_factory=dict)
124
+ json_imports: dict[str, Path] = Field(default_factory=dict)
125
+
126
+ @field_validator("local_files", "json_imports")
127
+ @classmethod
128
+ def valid_ids(cls, value):
129
+ if len(value) > 20 or any(not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", key) for key in value):
130
+ raise ValueError("Connector IDs must be short alphanumeric identifiers")
131
+ return value
132
+
133
+
134
+ class Settings(BaseSettings):
135
+ """Root settings for ContextOS."""
136
+ daemon: DaemonConfig = Field(default_factory=DaemonConfig)
137
+ embedding: EmbeddingConfig = Field(default_factory=EmbeddingConfig)
138
+ token_counter: TokenCounterConfig = Field(default_factory=TokenCounterConfig)
139
+ retrieval: RetrievalConfig = Field(default_factory=RetrievalConfig)
140
+ privacy: PrivacyConfig = Field(default_factory=PrivacyConfig)
141
+ llm: LLMConfig = Field(default_factory=LLMConfig)
142
+ mcp: MCPConfig = Field(default_factory=MCPConfig)
143
+ connectors: ConnectorConfig = Field(default_factory=ConnectorConfig)
144
+
145
+
146
+ def load_settings() -> Settings:
147
+ """Load settings from config file, falling back to defaults."""
148
+ config_dir = _default_config_dir()
149
+ config_file = config_dir / "config.toml"
150
+
151
+ if config_file.exists():
152
+ try:
153
+ import tomllib
154
+ with open(config_file, "rb") as f:
155
+ data = tomllib.load(f)
156
+ return Settings(**data)
157
+ except Exception as exc:
158
+ raise ValueError(f"Invalid ContextOS configuration: {config_file}") from exc
159
+
160
+ return Settings()
@@ -0,0 +1,6 @@
1
+ """Credential-free local activity connectors for ContextOS Phase 11."""
2
+
3
+ from contextos.connectors.manager import ConnectorManager
4
+ from contextos.connectors.models import ConnectorItem, ConnectorSyncResult, RetentionPolicy
5
+
6
+ __all__ = ["ConnectorItem", "ConnectorManager", "ConnectorSyncResult", "RetentionPolicy"]
@@ -0,0 +1,11 @@
1
+ """Deterministic offline connector used by tests and benchmarks."""
2
+ from __future__ import annotations
3
+ from contextos.connectors.models import ConnectorItem
4
+ class FakeConnector:
5
+ source_type="fake"
6
+ def __init__(self,connector_id:str,items:list[ConnectorItem])->None:self.connector_id,self.items=connector_id,items;self.failure:Exception|None=None
7
+ async def health(self)->bool:return self.failure is None
8
+ async def close(self)->None:pass
9
+ async def scan(self,cursor:str|None)->tuple[list[ConnectorItem],str|None]:
10
+ if self.failure: raise self.failure
11
+ return list(self.items),str(len(self.items))