Spaces:
Sleeping
Sleeping
File size: 10,903 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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | """agent-generator integration — emit a Matrix Context memory layer.
When `agent-generator` is invoked with `--context-provider matrix-context`, it
calls :func:`emit_template` to obtain the files that wire Matrix Context into the
generated project. Two variants are supported, selected by the ``mcp`` flag:
* **in-process** (default): a local :class:`~matrix_context.ContextManager` backed
by a default SQLite store, with ``build_pack`` called *before* each model call
and ``remember`` called *after* each turn, so the generated agent actually
accumulates and uses memory.
* **MCP** (``mcp=True``): instead of an in-process client, emit an MCP server
launch configuration pointing at ``matrix-context serve --transport stdio`` so
the agent reaches the same engine over the protocol.
The emitter is framework-aware for the ``crewai``, ``langgraph`` and ``react``
targets — the core ``ContextManager`` wiring is identical, only the call-site
shape (memory hook, graph node, plain tool) differs.
This is the proof that the engine is usable from generated code: the emitted
client must import :class:`ContextManager`, reference a SQLite path, and call
``build_pack``. The unit test under ``tests/unit/test_agent_generator.py``
asserts exactly that.
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass, field
from typing import Dict, Optional
FRAMEWORKS = ("react", "crewai", "langgraph")
# How the engine should be reached from generated code.
IN_PROCESS = "in_process"
MCP = "mcp"
@dataclass
class EmittedTemplate:
"""The result of :func:`emit_template`.
``files`` maps a relative path to file content; ``entrypoint`` names the
primary client module so the caller can wire imports. ``code`` / ``config``
are convenience views over the primary client and the MCP/launch config.
"""
framework: str
variant: str # IN_PROCESS | MCP
slug: str
scopes: Dict[str, str]
files: Dict[str, str] = field(default_factory=dict)
entrypoint: str = "matrix_memory.py"
@property
def code(self) -> str:
return self.files.get(self.entrypoint, "")
@property
def config(self) -> str:
return self.files.get("mcp.json", "")
def _slug(text: str, fallback: str = "agent") -> str:
s = re.sub(r"[^a-z0-9]+", "-", (text or "").lower()).strip("-")
s = "-".join(s.split("-")[:4]) # keep it short
return s or fallback
def _default_scopes(slug: str, purpose: str) -> Dict[str, str]:
"""Example scopes appropriate to the agent's purpose.
Profile is always-injectable identity; semantic/episodic are recalled by
routing. A policy scope is added when the purpose hints at governance.
"""
base = f"/{slug}"
scopes = {
"profile": f"{base}/profile",
"semantic": f"{base}/knowledge",
"episodic": f"{base}/history",
}
if re.search(r"govern|policy|complian|audit|secure", purpose or "", re.I):
scopes["policy"] = f"{base}/policy"
return scopes
# --------------------------------------------------------------------------- #
# In-process client (shared core + framework-specific call sites)
# --------------------------------------------------------------------------- #
def _client_module(slug: str, purpose: str, scopes: Dict[str, str],
store_path: str, max_tokens: int, framework: str) -> str:
seed = []
for expert, scope in scopes.items():
if expert == "profile":
seed.append(
f' ctx.remember("purpose: {purpose}", expert="profile", '
f'scope=SCOPES["profile"], importance=0.9)')
seed_block = "\n".join(seed) or " pass"
framework_note = {
"crewai": "Wire `build_context` into a CrewAI Task's context and call "
"`record_turn` from a step/`task_callback`.",
"langgraph": "Use `build_context` inside a node before the model call "
"and `record_turn` in the node that closes the turn.",
"react": "Expose `build_context` and `record_turn` as plain tools the "
"ReAct loop can call.",
}[framework]
scopes_literal = json.dumps(scopes, indent=4).replace("null", "None")
return f'''"""Matrix Context memory layer for `{slug}` ({framework}).
Generated by agent-generator (--context-provider matrix-context). {framework_note}
The two calls that matter:
* `build_context(query)` -> run BEFORE each model call (routes + budgets memory)
* `record_turn(user, agent)` -> run AFTER each turn (remember what happened)
"""
from __future__ import annotations
from matrix_context import ContextManager
# Default local SQLite store — the source of truth, vectors are an accelerator.
STORE_PATH = "{store_path}"
MAX_TOKENS = {max_tokens}
# Example scopes appropriate to this agent's purpose.
SCOPES = {scopes_literal}
ctx = ContextManager.create("{slug}", path=STORE_PATH)
def bootstrap() -> None:
"""Seed always-injectable profile facts (idempotent enough for a demo)."""
{seed_block}
def build_context(query: str, max_tokens: int = MAX_TOKENS) -> str:
"""Route + retrieve + budget memory into a compact prompt block.
Call this BEFORE every model call and prepend the result to the prompt.
"""
pack = ctx.build_pack(query, max_tokens=max_tokens)
return pack.to_prompt()
def record_turn(user_message: str, agent_message: str,
importance: float = 0.5) -> None:
"""Remember the turn AFTER it happens, so memory accumulates across calls."""
ctx.remember(user_message, expert="episodic",
scope=SCOPES["episodic"], importance=importance)
ctx.remember(agent_message, expert="semantic",
scope=SCOPES["semantic"], importance=importance)
def explain(query: str) -> str:
"""Inspect why the engine selected what it did (every choice is explainable)."""
return ctx.inspect(query, max_tokens=MAX_TOKENS)
if __name__ == "__main__":
bootstrap()
record_turn("I prefer concise answers.", "Understood — I will be concise.")
print(build_context("what does the user prefer?"))
'''
# --------------------------------------------------------------------------- #
# MCP variant (launch config + thin client)
# --------------------------------------------------------------------------- #
def _mcp_config(slug: str, store_path: str) -> str:
cfg = {
"mcpServers": {
"matrix-context": {
"command": "matrix-context",
"args": ["serve", "--transport", "stdio"],
"env": {
"MATRIX_CONTEXT_NAME": slug,
"MATRIX_CONTEXT_PATH": store_path,
},
}
}
}
return json.dumps(cfg, indent=2)
def _mcp_client_module(slug: str, purpose: str, scopes: Dict[str, str],
store_path: str, max_tokens: int, framework: str) -> str:
scopes_literal = json.dumps(scopes, indent=4).replace("null", "None")
return f'''"""Matrix Context (MCP) memory layer for `{slug}` ({framework}).
Generated by agent-generator (--context-provider matrix-context --mcp).
This variant does NOT embed the engine in-process. It launches the standards
compliant server via `matrix-context serve --transport stdio` (see mcp.json) and
talks to it over MCP. The two tools that matter are `build_pack` (before a model
call) and `remember` (after a turn). A local fallback `ContextManager` keeps the
generated project runnable offline before the MCP host is attached.
"""
from __future__ import annotations
from matrix_context import ContextManager
# The MCP server is configured in mcp.json -> `matrix-context serve --transport stdio`.
STORE_PATH = "{store_path}"
MAX_TOKENS = {max_tokens}
SCOPES = {scopes_literal}
# Offline fallback so the project runs before an MCP host wires the server in.
_local = ContextManager.create("{slug}", path=STORE_PATH)
def build_context(query: str, max_tokens: int = MAX_TOKENS) -> str:
"""Before each model call: ask the MCP `build_pack` tool (local fallback here)."""
return _local.build_pack(query, max_tokens=max_tokens).to_prompt()
def record_turn(user_message: str, agent_message: str) -> None:
"""After each turn: call the MCP `remember` tool (local fallback here)."""
_local.remember(user_message, expert="episodic", scope=SCOPES["episodic"])
_local.remember(agent_message, expert="semantic", scope=SCOPES["semantic"])
'''
def _readme(slug: str, variant: str, framework: str) -> str:
how = ("Launch the MCP server with `matrix-context serve --transport stdio` "
"(configured in `mcp.json`)."
if variant == MCP else
"The memory layer runs in-process against a local SQLite store.")
return (f"# {slug} — Matrix Context memory\n\n"
f"Framework: **{framework}** · Variant: **{variant}**\n\n{how}\n\n"
"- `build_context(query)` before every model call\n"
"- `record_turn(user, agent)` after every turn\n")
def emit_template(purpose: str = "", framework: str = "react", *,
mcp: bool = False, scopes: Optional[Dict[str, str]] = None,
store_path: Optional[str] = None, name: Optional[str] = None,
max_tokens: int = 256) -> EmittedTemplate:
"""Emit the Matrix Context client code + config for a generated project.
Parameters
----------
purpose: natural-language description of the agent (drives example scopes).
framework: one of ``react`` | ``crewai`` | ``langgraph``.
mcp: emit the MCP server launch config instead of an in-process client.
scopes: override the example scopes (expert -> scope path).
store_path: SQLite path for the default local store.
name: project/agent name (defaults to a slug of ``purpose``).
max_tokens: per-turn pack budget (compact-injection discipline).
"""
framework = (framework or "react").lower()
if framework not in FRAMEWORKS:
raise ValueError(f"unknown framework: {framework!r}; "
f"expected one of {FRAMEWORKS}")
slug = _slug(name or purpose)
scopes = scopes or _default_scopes(slug, purpose)
store_path = store_path or f"./{slug}.matrix-context.db"
variant = MCP if mcp else IN_PROCESS
files: Dict[str, str] = {}
if mcp:
files["matrix_memory.py"] = _mcp_client_module(
slug, purpose, scopes, store_path, max_tokens, framework)
files["mcp.json"] = _mcp_config(slug, store_path)
else:
files["matrix_memory.py"] = _client_module(
slug, purpose, scopes, store_path, max_tokens, framework)
files["MATRIX_CONTEXT.md"] = _readme(slug, variant, framework)
return EmittedTemplate(framework=framework, variant=variant, slug=slug,
scopes=scopes, files=files)
|