Agent gateway + observer — autonomous internal operation
Three new systemd services: - lakehouse-agent (:3700) — REST gateway wrapping all lakehouse tools. Clean JSON in/out, no protocol complexity. 9 endpoints: /search, /sql, /match, /worker/:id, /ask, /log, /playbooks, /profile/:id, /vram - lakehouse-observer — watches operations, logs to lakehouse, asks local model to diagnose failure patterns, consolidates successful patterns into playbooks every 5 cycles - Stdio MCP transport preserved for Claude Code integration AGENT_INSTRUCTIONS.md: complete operating manual for sub-agents. Rules: never hallucinate, SQL first for structured questions, hybrid for matching, log every success, check playbooks before complex tasks. Observer loop: observed() wrapper timestamps + persists every gateway call → error analyzer reads failures + asks LLM for diagnosis → playbook consolidator groups successes by endpoint pattern All three designed for zero human intervention — agents operate, observer watches, playbooks accumulate, iteration happens internally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+115
-5
@@ -15,9 +15,12 @@
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import { z } from "zod";
|
||||
|
||||
const BASE = process.env.LAKEHOUSE_URL || "http://localhost:3100";
|
||||
const PORT = parseInt(process.env.MCP_PORT || "3700");
|
||||
const MODE = process.env.MCP_TRANSPORT || "http"; // "stdio" or "http"
|
||||
|
||||
async function api(method: string, path: string, body?: any) {
|
||||
const resp = await fetch(`${BASE}${path}`, {
|
||||
@@ -176,12 +179,119 @@ server.resource("lakehouse://datasets", "lakehouse://datasets", async (uri) => {
|
||||
return { contents: [{ uri: uri.href, mimeType: "text/plain", text }] };
|
||||
});
|
||||
|
||||
// Start
|
||||
// ─── Dual mode: stdio (Claude Code) or HTTP (internal agents) ───
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error(`Lakehouse MCP server started → ${BASE}`);
|
||||
console.error("Tools: search_workers, query_sql, match_contract, get_worker, rag_question, log_success, get_playbooks, swap_profile, vram_status");
|
||||
if (MODE === "stdio") {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error(`Lakehouse MCP (stdio) → ${BASE}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// HTTP mode — a REST gateway that internal agents call directly.
|
||||
// No MCP protocol complexity for consumers — just POST JSON, get JSON.
|
||||
// The MCP tool definitions above are reused for the stdio path; this
|
||||
// HTTP path wraps the same lakehouse API with agent-friendly routing.
|
||||
|
||||
Bun.serve({
|
||||
port: PORT,
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
const json = async () => req.method === "POST" ? await req.json() : {};
|
||||
const ok = (data: any) => Response.json(data);
|
||||
const err = (msg: string, status = 400) => Response.json({ error: msg }, { status });
|
||||
|
||||
try {
|
||||
// Health
|
||||
if (url.pathname === "/health") return ok({ status: "ok", lakehouse: BASE, tools: 9 });
|
||||
|
||||
// Tool: hybrid search
|
||||
if (url.pathname === "/search") {
|
||||
const b = await json();
|
||||
return ok(await api("POST", "/vectors/hybrid", {
|
||||
question: b.question, index_name: b.index || "ethereal_workers_v1",
|
||||
sql_filter: b.sql_filter, filter_dataset: b.dataset || "ethereal_workers",
|
||||
id_column: b.id_column || "worker_id", top_k: b.top_k || 5, generate: b.generate !== false,
|
||||
}));
|
||||
}
|
||||
|
||||
// Tool: SQL
|
||||
if (url.pathname === "/sql") {
|
||||
const b = await json();
|
||||
return ok(await api("POST", "/query/sql", { sql: b.sql }));
|
||||
}
|
||||
|
||||
// Tool: match contract
|
||||
if (url.pathname === "/match") {
|
||||
const b = await json();
|
||||
let filter = `role = '${b.role}' AND state = '${b.state}' AND reliability >= ${b.min_reliability || 0.7}`;
|
||||
if (b.city) filter += ` AND city = '${b.city}'`;
|
||||
return ok(await api("POST", "/vectors/hybrid", {
|
||||
question: `Best ${b.role} workers with relevant skills`,
|
||||
index_name: b.index || "ethereal_workers_v1", sql_filter: filter,
|
||||
filter_dataset: b.dataset || "ethereal_workers",
|
||||
id_column: "worker_id", top_k: (b.headcount || 5) * 2, generate: false,
|
||||
}));
|
||||
}
|
||||
|
||||
// Tool: get worker
|
||||
if (url.pathname.startsWith("/worker/")) {
|
||||
const id = url.pathname.split("/")[2];
|
||||
return ok(await api("POST", "/query/sql", { sql: `SELECT * FROM ethereal_workers WHERE worker_id = ${id}` }));
|
||||
}
|
||||
|
||||
// Tool: RAG
|
||||
if (url.pathname === "/ask") {
|
||||
const b = await json();
|
||||
return ok(await api("POST", "/vectors/rag", { index_name: b.index || "ethereal_workers_v1", question: b.question, top_k: b.top_k || 5 }));
|
||||
}
|
||||
|
||||
// Tool: log success
|
||||
if (url.pathname === "/log") {
|
||||
const b = await json();
|
||||
const csv = `timestamp,operation,approach,result,context\n"${new Date().toISOString()}","${(b.operation||"").replace(/"/g,'""')}","${(b.approach||"").replace(/"/g,'""')}","${(b.result||"").replace(/"/g,'""')}","${(b.context||"").replace(/"/g,'""')}"`;
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob([csv], { type: "text/csv" }), "playbook.csv");
|
||||
const r = await fetch(`${BASE}/ingest/file?name=successful_playbooks`, { method: "POST", body: form });
|
||||
return ok({ logged: true, response: await r.text() });
|
||||
}
|
||||
|
||||
// Tool: get playbooks
|
||||
if (url.pathname === "/playbooks") {
|
||||
const kw = url.searchParams.get("keyword");
|
||||
const limit = url.searchParams.get("limit") || "10";
|
||||
let sql = `SELECT * FROM successful_playbooks ORDER BY timestamp DESC LIMIT ${limit}`;
|
||||
if (kw) sql = `SELECT * FROM successful_playbooks WHERE operation LIKE '%${kw}%' OR approach LIKE '%${kw}%' ORDER BY timestamp DESC LIMIT ${limit}`;
|
||||
const r = await api("POST", "/query/sql", { sql });
|
||||
return ok(r.error ? { playbooks: [], note: "No playbooks yet" } : { playbooks: r.rows });
|
||||
}
|
||||
|
||||
// Tool: swap profile
|
||||
if (url.pathname.startsWith("/profile/")) {
|
||||
const id = url.pathname.split("/")[2];
|
||||
return ok(await api("POST", `/vectors/profile/${id}/activate`));
|
||||
}
|
||||
|
||||
// Tool: VRAM
|
||||
if (url.pathname === "/vram") return ok(await api("GET", "/ai/vram"));
|
||||
|
||||
// Pass-through to lakehouse for anything else
|
||||
if (url.pathname.startsWith("/api/")) {
|
||||
const path = url.pathname.replace("/api", "");
|
||||
const body = req.method !== "GET" ? await req.text() : undefined;
|
||||
const r = await fetch(`${BASE}${path}`, { method: req.method, headers: { "Content-Type": "application/json" }, body });
|
||||
return new Response(await r.text(), { status: r.status, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
|
||||
return err("Unknown path. Available: /health /search /sql /match /worker/:id /ask /log /playbooks /profile/:id /vram /api/*", 404);
|
||||
} catch (e: any) {
|
||||
return err(e.message || String(e), 500);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
console.error(`Lakehouse Agent Gateway :${PORT} → ${BASE}`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
Reference in New Issue
Block a user