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.
- contextos/__init__.py +3 -0
- contextos/__main__.py +6 -0
- contextos/api/__init__.py +1 -0
- contextos/api/routes/__init__.py +1 -0
- contextos/api/routes/desktop.py +322 -0
- contextos/api/routes/ingest.py +17 -0
- contextos/api/routes/memories.py +84 -0
- contextos/api/routes/models.py +81 -0
- contextos/api/routes/retrieval.py +89 -0
- contextos/api/routes/system.py +216 -0
- contextos/api/server.py +195 -0
- contextos/benchmarks/__init__.py +1 -0
- contextos/benchmarks/compilation.py +245 -0
- contextos/benchmarks/connectors.py +423 -0
- contextos/benchmarks/explainability.py +103 -0
- contextos/benchmarks/final.py +406 -0
- contextos/benchmarks/graph.py +310 -0
- contextos/benchmarks/graph_adversarial.py +525 -0
- contextos/benchmarks/mcp.py +324 -0
- contextos/benchmarks/model_routing.py +203 -0
- contextos/benchmarks/optimization.py +305 -0
- contextos/benchmarks/rescue_integration.py +127 -0
- contextos/benchmarks/retrieval.py +266 -0
- contextos/benchmarks/temporal.py +377 -0
- contextos/benchmarks/temporal_hotpath.py +76 -0
- contextos/benchmarks/terminal.py +62 -0
- contextos/cli/__init__.py +1 -0
- contextos/cli/app.py +932 -0
- contextos/cli/dashboard.py +174 -0
- contextos/cli/formatters.py +299 -0
- contextos/config/__init__.py +1 -0
- contextos/config/settings.py +160 -0
- contextos/connectors/__init__.py +6 -0
- contextos/connectors/fake.py +11 -0
- contextos/connectors/json_import.py +125 -0
- contextos/connectors/local_files.py +102 -0
- contextos/connectors/manager.py +293 -0
- contextos/connectors/models.py +62 -0
- contextos/connectors/protocols.py +11 -0
- contextos/core/__init__.py +103 -0
- contextos/core/enums.py +489 -0
- contextos/core/exceptions.py +293 -0
- contextos/core/models.py +1147 -0
- contextos/core/protocols.py +549 -0
- contextos/daemon/__init__.py +1 -0
- contextos/daemon/manager.py +510 -0
- contextos/daemon/state.py +127 -0
- contextos/daemon/wiring.py +296 -0
- contextos/demo.py +217 -0
- contextos/embedding/__init__.py +1 -0
- contextos/embedding/deterministic.py +76 -0
- contextos/embedding/sentence_transformers.py +80 -0
- contextos/mcp/__init__.py +5 -0
- contextos/mcp/server.py +269 -0
- contextos/providers/__init__.py +13 -0
- contextos/providers/fake.py +217 -0
- contextos/providers/ollama.py +297 -0
- contextos/providers/openai_compatible.py +337 -0
- contextos/services/__init__.py +1 -0
- contextos/services/compilation.py +535 -0
- contextos/services/explainability.py +553 -0
- contextos/services/extraction.py +311 -0
- contextos/services/graph.py +524 -0
- contextos/services/graph_retrieval.py +143 -0
- contextos/services/ingestion.py +143 -0
- contextos/services/inspection.py +174 -0
- contextos/services/memory.py +291 -0
- contextos/services/model_service.py +409 -0
- contextos/services/optimization.py +426 -0
- contextos/services/privacy.py +331 -0
- contextos/services/retrieval.py +302 -0
- contextos/services/retrieval_index.py +88 -0
- contextos/services/router.py +302 -0
- contextos/services/secret_scanner.py +207 -0
- contextos/services/telemetry_query.py +102 -0
- contextos/services/temporal.py +500 -0
- contextos/services/token_counter.py +222 -0
- contextos/storage/__init__.py +1 -0
- contextos/storage/connector_repo.py +67 -0
- contextos/storage/database.py +497 -0
- contextos/storage/event_repo.py +137 -0
- contextos/storage/graph_repo.py +228 -0
- contextos/storage/lexical/__init__.py +1 -0
- contextos/storage/lexical/bm25.py +134 -0
- contextos/storage/memory_repo.py +589 -0
- contextos/storage/relation_repo.py +80 -0
- contextos/storage/telemetry_repo.py +481 -0
- contextos/storage/vector/__init__.py +1 -0
- contextos/storage/vector/in_memory.py +162 -0
- contextos_memory_runtime-1.0.0rc2.dist-info/METADATA +143 -0
- contextos_memory_runtime-1.0.0rc2.dist-info/RECORD +93 -0
- contextos_memory_runtime-1.0.0rc2.dist-info/WHEEL +4 -0
- 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))
|