File size: 6,417 Bytes
ce45eb0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""Matrix Context — frontend / control-plane server.

A self-contained, Hugging-Face-deployable launcher that serves:
  * the static control-plane UI in ``frontend/app/`` (Overview, Inspector,
    Builder, Memory, Experts, Routing, Benchmarks, Standard, Settings), and
  * the live MoC Contract v1 API under ``/v1`` — by reusing the published
    backend's ``RestService`` / ``dispatch``.

This is **additive and non-destructive**: it imports ``matrix_context`` and the
existing REST app; it does not modify them. A small demo memory set is seeded on
startup so the Space is useful immediately.

    python frontend/server.py            # local  -> http://127.0.0.1:7860
    PORT=7860 HOST=0.0.0.0 python ...     # container / Hugging Face Space
"""
from __future__ import annotations

import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse

from matrix_context import ContextManager
from matrix_context.serve.rest.app import RestService, dispatch

APP_DIR = Path(__file__).parent / "app"
HOST = os.environ.get("HOST", "127.0.0.1")
PORT = int(os.environ.get("PORT", "7860"))
DB_PATH = os.environ.get("MATRIX_CONTEXT_PATH", ":memory:")

_CONTENT_TYPES = {".html": "text/html; charset=utf-8",
                  ".js": "application/javascript; charset=utf-8",
                  ".css": "text/css; charset=utf-8",
                  ".svg": "image/svg+xml", ".json": "application/json",
                  ".png": "image/png", ".ico": "image/x-icon"}

# Demo seed — a small, typed, multi-scope memory set so every view is populated.
SEED = [
    ("Decision: use SQLite as the default storage backend; vectors accelerate, SQL is the source of truth.",
     "semantic", "project:matrix-context", 0.95, ["backend", "sqlite", "type:decision"]),
    ("Matrix Context exposes both a Python SDK and an MCP server so agents and operators share one memory plane.",
     "semantic", "project:matrix-context", 0.9, ["mcp", "sdk", "type:decision"]),
    ("Policy: the agent must never store API keys, tokens, or PII in durable memory.",
     "policy", "project:matrix-context", 0.95, ["security", "pii", "type:rule"]),
    ("Decision: defer Milvus support to v2; pgvector lands in v1.",
     "episodic", "project:matrix-context", 0.8, ["milvus", "roadmap", "type:decision"]),
    ("The user prefers local-first tools over cloud services by default.",
     "profile", "user:42", 0.9, ["preference", "type:preference"]),
    ("The user prefers concise, direct answers with code first.",
     "profile", "user:42", 0.8, ["preference", "tone", "type:preference"]),
    ("The whitepaper reports that routed typed context cuts distractors at equal recall.",
     "document", "project:matrix-context", 0.7, ["whitepaper", "type:document"]),
    ("Decision: ship the REST API and Context Console before the MCP server.",
     "semantic", "project:acme", 0.85, ["rest", "console", "type:decision"]),
    ("Policy in project:acme — audit-log every write to profile or policy memory.",
     "policy", "project:acme", 0.88, ["audit", "governance", "type:rule"]),
    ("In the last session we agreed the demo should run on Hugging Face Spaces.",
     "episodic", "project:acme", 0.65, ["session", "demo", "type:episode"]),
]


def build_service() -> RestService:
    mgr = ContextManager.create("matrix-context-demo", path=DB_PATH)
    if not mgr.store.all_items():
        for content, expert, scope, importance, tags in SEED:
            mgr.remember(content, expert=expert, scope=scope,
                         importance=importance, tags=tags)
    return RestService(mgr)


def _static(path: str):
    """Resolve a static asset under APP_DIR (path-traversal safe)."""
    rel = path.lstrip("/") or "index.html"
    target = (APP_DIR / rel).resolve()
    if APP_DIR.resolve() not in target.parents and target != APP_DIR.resolve():
        return None
    if not target.is_file():
        return None
    ctype = _CONTENT_TYPES.get(target.suffix, "application/octet-stream")
    return target.read_bytes(), ctype


def make_handler(service: RestService):
    class Handler(BaseHTTPRequestHandler):
        def log_message(self, *a):
            pass

        def _send_json(self, status, payload):
            data = json.dumps(payload).encode()
            self.send_response(status)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(data)))
            self.end_headers()
            self.wfile.write(data)

        def _send_bytes(self, data, ctype, status=200):
            self.send_response(status)
            self.send_header("Content-Type", ctype)
            self.send_header("Content-Length", str(len(data)))
            self.end_headers()
            self.wfile.write(data)

        def _read_body(self):
            n = int(self.headers.get("Content-Length") or 0)
            if not n:
                return {}
            try:
                obj = json.loads(self.rfile.read(n) or b"{}")
                return obj if isinstance(obj, dict) else {}
            except json.JSONDecodeError:
                return {}

        def do_GET(self):
            p = urlparse(self.path)
            if p.path.startswith("/v1"):
                st, payload = dispatch(service, "GET", p.path, query=parse_qs(p.query))
                self._send_json(st, payload)
                return
            asset = _static("index.html" if p.path == "/" else p.path)
            if asset:
                self._send_bytes(*asset)
            else:
                self._send_json(404, {"error": f"not found: {p.path}"})

        def do_POST(self):
            p = urlparse(self.path)
            if p.path.startswith("/v1"):
                st, payload = dispatch(service, "POST", p.path, body=self._read_body())
                self._send_json(st, payload)
            else:
                self._send_json(404, {"error": f"not found: {p.path}"})

    return Handler


def main() -> int:
    service = build_service()
    httpd = HTTPServer((HOST, PORT), make_handler(service))
    n = len(service.m.store.all_items())
    print(f"Matrix Context control plane on http://{HOST}:{PORT}  ({n} seeded items)")
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        httpd.server_close()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())