| """Rivet — Kintsugi-based code assistant for the Multiverse Campus. |
| |
| Not a chat wrapper: every request becomes a BDI intention, compiles to a |
| SkillDAG, executes through evidence-gathering chips, and terminates in |
| the discipline gate. See ARCHITECTURE.md. |
| |
| Usage: |
| python rivet.py # serve on config port (8100) |
| python rivet.py --port 8200 |
| python rivet.py --ask "..." [--user t] # one-shot CLI |
| python rivet.py --show-bdi # dump seeded beliefs |
| """ |
|
|
| import argparse |
| import asyncio |
| import json |
| import sys |
| import time |
| import uuid |
| from http.server import BaseHTTPRequestHandler, HTTPServer |
| from pathlib import Path |
|
|
| V2_ROOT = Path(__file__).parent |
| sys.path.insert(0, str(V2_ROOT)) |
|
|
| from kintsugi_core import ( |
| KINTSUGI_SOURCE, |
| BDIStore, |
| DAGExecutor, |
| SkillContext, |
| SkillRegistry, |
| ) |
| from engine.beliefs import refresh_git_belief, seed_bdi |
| from engine.config import load_config |
| from engine.model_client import build_client |
| from engine.planner import Planner |
| from engine.session import SessionManager |
| from engine.synthesis import SynthesisChip |
| from pharos.pack_loader import load_router |
| from skills.code_analysis import CodeAnalysisChip |
| from skills.discipline_gate import DisciplineGateChip |
| from skills.migration_safety import MigrationSafetyChip |
| from skills.security_review import SecurityReviewChip |
| from skills.test_runner import TestRunnerChip |
| from tools.file_tools import RepoFiles |
| from tools.git_tools import GitTools |
| from tools.schema_tools import SchemaTools |
| from tools.test_tools import TestTools |
|
|
|
|
| class RivetAgent: |
| def __init__(self, config_path: Path | str | None = None, |
| model_client=None): |
| cfg_path = Path(config_path or V2_ROOT / "kintsugi_config.yaml") |
| self.config = load_config(cfg_path) |
| paths = self.config.get("paths", {}) or {} |
|
|
| self.context_dir = (cfg_path.parent / paths.get( |
| "context_dir", "../context")).resolve() |
|
|
| |
| org_id = self.config.get("org", {}).get("id", "multiverse_school") |
| self.bdi = BDIStore(org_id) |
| seed_bdi(self.bdi, self.config, self.context_dir) |
|
|
| |
| repo = paths.get("campus_repo") or "" |
| self.repo_files = None |
| self.git_tools = None |
| if repo and Path(repo).is_dir(): |
| write_enabled = bool( |
| (self.config.get("tools") or {}).get("repo_write_enabled")) |
| self.repo_files = RepoFiles(repo_root=repo, |
| write_enabled=write_enabled) |
| self.git_tools = GitTools(repo_root=repo) |
| refresh_git_belief(self.bdi, repo, self.context_dir) |
| self.schema_tools = SchemaTools( |
| self.context_dir, |
| dsn=paths.get("campus_dsn") or "", |
| migrations_dir=paths.get("migrations_dir") or "", |
| ) |
| self.test_tools = TestTools(repo_root=repo) |
|
|
| |
| model_cfg = self.config.get("model", {}) or {} |
| self.model = model_client or build_client(model_cfg) |
| pharos_cfg = self.config.get("pharos", {}) or {} |
| packs_dir = (cfg_path.parent / pharos_cfg.get( |
| "packs_dir", "../packs")).resolve() |
| self.pharos = load_router( |
| packs_dir, |
| match_threshold=float(pharos_cfg.get("match_threshold", 0.30)), |
| source_threshold=float(pharos_cfg.get("source_threshold", 0.55)), |
| max_packs=int(pharos_cfg.get("max_packs", 2)), |
| ) |
|
|
| |
| self.registry = SkillRegistry() |
| self.registry.register(CodeAnalysisChip(self.repo_files, self.git_tools)) |
| self.registry.register(MigrationSafetyChip(self.schema_tools)) |
| self.registry.register(SecurityReviewChip(self.repo_files)) |
| self.registry.register(SynthesisChip( |
| self.model, self.bdi, self.pharos, |
| max_tokens=int(model_cfg.get("max_answer_tokens", 1536)), |
| )) |
| self.registry.register(TestRunnerChip(self.test_tools)) |
| self.registry.register(DisciplineGateChip(self.bdi)) |
|
|
| |
| self.planner = Planner(self.bdi, self.registry) |
| self.executor = DAGExecutor(self.registry, max_parallel=4) |
| sess_cfg = self.config.get("session", {}) or {} |
| self.sessions = SessionManager( |
| ttl_seconds=int(sess_cfg.get("ttl_seconds", 3600)), |
| rate_limit_per_hour=int(sess_cfg.get("rate_limit_per_hour", 60)), |
| ) |
| self.org_id = org_id |
|
|
| |
|
|
| def ask(self, question: str, user: str = "anonymous") -> dict: |
| t0 = time.time() |
| question = (question or "").strip() |
| if not question: |
| return {"error": "empty question"} |
|
|
| if not self.sessions.check_rate_limit(user): |
| return {"error": "rate limit exceeded — try again later", |
| "user": user} |
| session = self.sessions.get(user) |
|
|
| plan = self.planner.form_plan(question, user) |
| intention = self.bdi.get_intention(plan.intention_id) |
|
|
| context = SkillContext( |
| org_id=self.org_id, |
| user_id=user, |
| session_id=f"{user}:{int(session.created_at)}", |
| metadata={ |
| "question": question, |
| "session": session, |
| "belief_ids": intention.belief_ids if intention else [], |
| }, |
| ) |
|
|
| result = asyncio.run(self.executor.execute( |
| plan.dag, context, initial_artifacts={"question": question}, |
| )) |
|
|
| final = result.artifacts.get("final") or {} |
| ok = result.success and bool(final.get("text")) |
| self.planner.complete_plan(plan.intention_id, ok) |
|
|
| if not final.get("text"): |
| errors = result.node_errors or {"pipeline": "no final artifact"} |
| return { |
| "error": "plan execution failed", |
| "node_errors": errors, |
| "intent": plan.intent, |
| "plan": plan.rationale, |
| "elapsed_seconds": round(time.time() - t0, 1), |
| } |
|
|
| session.add_turn("user", question) |
| session.add_turn("rivet", final["text"][:1000]) |
|
|
| return { |
| "response": final["text"], |
| "gate_passed": final.get("passed", False), |
| "confidence": final.get("confidence", "LOW"), |
| "flags": final.get("flags", []), |
| "requires_review": final.get("requires_review", []), |
| "beliefs_consulted": final.get("beliefs_consulted", []), |
| "intent": plan.intent, |
| "plan": plan.rationale, |
| "packs_used": (result.artifacts.get("draft") or {}).get( |
| "packs_used", []), |
| "files_read": sorted(session.files_read), |
| "user": user, |
| "elapsed_seconds": round(time.time() - t0, 1), |
| } |
|
|
| def health(self) -> dict: |
| return { |
| "status": "ok", |
| "agent": self.config.get("org", {}).get("agent_name", "Rivet"), |
| "kintsugi_source": KINTSUGI_SOURCE, |
| "model_backend": self.config.get("model", {}).get("backend"), |
| "model": self.config.get("model", {}).get("name"), |
| "skills": self.registry.list_names(), |
| "beliefs": len(self.bdi.list_beliefs()), |
| "packs": [p.name for p in self.pharos.library.packs], |
| "repo_attached": self.repo_files is not None, |
| "sessions": self.sessions.stats(), |
| } |
|
|
| def bdi_snapshot(self) -> dict: |
| snap = self.bdi.get_snapshot() |
| return { |
| "org_id": snap.org_id, |
| "beliefs": [ |
| {"id": b.id, "confidence": b.confidence, |
| "source": b.source, "tags": b.tags, |
| "content": b.content[:200]} |
| for b in snap.beliefs |
| ], |
| "desires": [ |
| {"id": d.id, "priority": d.priority, "content": d.content} |
| for d in snap.desires |
| ], |
| "intentions_active": len([ |
| i for i in snap.intentions if i.status.value == "active" |
| ]), |
| "intentions_total": len(snap.intentions), |
| } |
|
|
|
|
| |
|
|
|
|
| def make_handler(agent: RivetAgent): |
| model_name = (agent.config.get("model") or {}).get("name", "rivet") |
|
|
| class Handler(BaseHTTPRequestHandler): |
| def do_POST(self): |
| try: |
| length = int(self.headers.get("Content-Length", 0)) |
| body = json.loads(self.rfile.read(length)) if length else {} |
| except (json.JSONDecodeError, ValueError): |
| return self._json({"error": "invalid JSON body"}, 400) |
|
|
| if self.path == "/ask": |
| result = agent.ask(body.get("question", ""), |
| body.get("user", "anonymous")) |
| self._json(result, 200 if "error" not in result else 422) |
| elif self.path == "/v1/chat/completions": |
| self._openai_chat(body) |
| else: |
| self._json({"error": f"unknown endpoint {self.path}"}, 404) |
|
|
| def _openai_chat(self, body: dict): |
| messages = body.get("messages", []) |
| if not messages: |
| return self._json({"error": {"message": "messages required", |
| "type": "invalid_request_error"}}, 400) |
| question = messages[-1].get("content", "") |
| user = body.get("user", "ide") |
|
|
| result = agent.ask(question, user) |
|
|
| if "error" in result: |
| return self._json({"error": {"message": result["error"], |
| "type": "server_error"}}, 500) |
|
|
| text = result.get("response", "") |
| prompt_chars = sum(len(m.get("content", "")) for m in messages) |
| self._json({ |
| "id": f"chatcmpl-{uuid.uuid4().hex[:24]}", |
| "object": "chat.completion", |
| "created": int(time.time()), |
| "model": model_name, |
| "choices": [{ |
| "index": 0, |
| "message": {"role": "assistant", "content": text}, |
| "finish_reason": "stop", |
| }], |
| "usage": { |
| "prompt_tokens": prompt_chars // 4, |
| "completion_tokens": len(text) // 4, |
| "total_tokens": (prompt_chars + len(text)) // 4, |
| }, |
| }) |
|
|
| def do_GET(self): |
| if self.path == "/health": |
| self._json(agent.health()) |
| elif self.path == "/bdi": |
| self._json(agent.bdi_snapshot()) |
| elif self.path == "/v1/models": |
| self._json({"object": "list", "data": [{ |
| "id": model_name, "object": "model", |
| "owned_by": "rivet", |
| }]}) |
| else: |
| self._json({"endpoints": { |
| "POST /ask": '{"question": "...", "user": "..."}', |
| "POST /v1/chat/completions": "OpenAI-compatible chat API", |
| "GET /v1/models": "available models", |
| "GET /health": "status + loaded skills/packs/beliefs", |
| "GET /bdi": "current belief/desire/intention state", |
| }}) |
|
|
| def _json(self, data, status=200): |
| payload = json.dumps(data, indent=2, default=str).encode() |
| self.send_response(status) |
| self.send_header("Content-Type", "application/json") |
| self.send_header("Content-Length", str(len(payload))) |
| self.end_headers() |
| self.wfile.write(payload) |
|
|
| def log_message(self, fmt, *args): |
| print(f"[rivet] {args[0] if args else ''}", flush=True) |
|
|
| return Handler |
|
|
|
|
| def main() -> None: |
| ap = argparse.ArgumentParser(description="Rivet code assistant (Kintsugi v2)") |
| ap.add_argument("--config", default=str(V2_ROOT / "kintsugi_config.yaml")) |
| ap.add_argument("--port", type=int, default=0) |
| ap.add_argument("--ask", help="one-shot question, print JSON and exit") |
| ap.add_argument("--user", default="cli") |
| ap.add_argument("--show-bdi", action="store_true") |
| args = ap.parse_args() |
|
|
| agent = RivetAgent(args.config) |
|
|
| if args.show_bdi: |
| print(json.dumps(agent.bdi_snapshot(), indent=2, default=str)) |
| return |
| if args.ask: |
| print(json.dumps(agent.ask(args.ask, args.user), indent=2, default=str)) |
| return |
|
|
| port = args.port or int( |
| (agent.config.get("server") or {}).get("port", 8100)) |
| health = agent.health() |
| server = HTTPServer(("0.0.0.0", port), make_handler(agent)) |
| print(f"Rivet v2 listening on :{port}", flush=True) |
| print(f" kintsugi: {health['kintsugi_source']}", flush=True) |
| print(f" model: {health['model_backend']}:{health['model']}", flush=True) |
| print(f" skills: {', '.join(health['skills'])}", flush=True) |
| print(f" beliefs: {health['beliefs']} packs: {health['packs']}", flush=True) |
| print(f" repo: {'attached' if health['repo_attached'] else 'not on this machine'}", flush=True) |
| try: |
| server.serve_forever() |
| except KeyboardInterrupt: |
| print("\nRivet shutting down.", flush=True) |
| server.shutdown() |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|