HumboldtJoker commited on
Commit
9f58be2
·
verified ·
1 Parent(s): f27df82

Clean up nested duplicate: v2/v2/rivet.py

Browse files
Files changed (1) hide show
  1. v2/v2/rivet.py +0 -296
v2/v2/rivet.py DELETED
@@ -1,296 +0,0 @@
1
- """Rivet — Kintsugi-based code assistant for the Multiverse Campus.
2
-
3
- Not a chat wrapper: every request becomes a BDI intention, compiles to a
4
- SkillDAG, executes through evidence-gathering chips, and terminates in
5
- the discipline gate. See ARCHITECTURE.md.
6
-
7
- Usage:
8
- python rivet.py # serve on config port (8100)
9
- python rivet.py --port 8200
10
- python rivet.py --ask "..." [--user t] # one-shot CLI
11
- python rivet.py --show-bdi # dump seeded beliefs
12
- """
13
-
14
- import argparse
15
- import asyncio
16
- import json
17
- import sys
18
- import time
19
- from http.server import BaseHTTPRequestHandler, HTTPServer
20
- from pathlib import Path
21
-
22
- V2_ROOT = Path(__file__).parent
23
- sys.path.insert(0, str(V2_ROOT))
24
-
25
- from kintsugi_core import ( # noqa: E402
26
- KINTSUGI_SOURCE,
27
- BDIStore,
28
- DAGExecutor,
29
- SkillContext,
30
- SkillRegistry,
31
- )
32
- from engine.beliefs import refresh_git_belief, seed_bdi # noqa: E402
33
- from engine.config import load_config # noqa: E402
34
- from engine.model_client import build_client # noqa: E402
35
- from engine.planner import Planner # noqa: E402
36
- from engine.session import SessionManager # noqa: E402
37
- from engine.synthesis import SynthesisChip # noqa: E402
38
- from pharos.pack_loader import load_router # noqa: E402
39
- from skills.code_analysis import CodeAnalysisChip # noqa: E402
40
- from skills.discipline_gate import DisciplineGateChip # noqa: E402
41
- from skills.migration_safety import MigrationSafetyChip # noqa: E402
42
- from skills.security_review import SecurityReviewChip # noqa: E402
43
- from skills.test_runner import TestRunnerChip # noqa: E402
44
- from tools.file_tools import RepoFiles # noqa: E402
45
- from tools.git_tools import GitTools # noqa: E402
46
- from tools.schema_tools import SchemaTools # noqa: E402
47
- from tools.test_tools import TestTools # noqa: E402
48
-
49
-
50
- class RivetAgent:
51
- def __init__(self, config_path: Path | str | None = None,
52
- model_client=None):
53
- cfg_path = Path(config_path or V2_ROOT / "kintsugi_config.yaml")
54
- self.config = load_config(cfg_path)
55
- paths = self.config.get("paths", {}) or {}
56
-
57
- self.context_dir = (cfg_path.parent / paths.get(
58
- "context_dir", "../context")).resolve()
59
-
60
- # --- BDI ------------------------------------------------------
61
- org_id = self.config.get("org", {}).get("id", "multiverse_school")
62
- self.bdi = BDIStore(org_id)
63
- seed_bdi(self.bdi, self.config, self.context_dir)
64
-
65
- # --- tool harness ---------------------------------------------
66
- repo = paths.get("campus_repo") or ""
67
- self.repo_files = None
68
- self.git_tools = None
69
- if repo and Path(repo).is_dir():
70
- write_enabled = bool(
71
- (self.config.get("tools") or {}).get("repo_write_enabled"))
72
- self.repo_files = RepoFiles(repo_root=repo,
73
- write_enabled=write_enabled)
74
- self.git_tools = GitTools(repo_root=repo)
75
- refresh_git_belief(self.bdi, repo, self.context_dir)
76
- self.schema_tools = SchemaTools(
77
- self.context_dir,
78
- dsn=paths.get("campus_dsn") or "",
79
- migrations_dir=paths.get("migrations_dir") or "",
80
- )
81
- self.test_tools = TestTools(repo_root=repo)
82
-
83
- # --- model + pharos -------------------------------------------
84
- model_cfg = self.config.get("model", {}) or {}
85
- self.model = model_client or build_client(model_cfg)
86
- pharos_cfg = self.config.get("pharos", {}) or {}
87
- packs_dir = (cfg_path.parent / pharos_cfg.get(
88
- "packs_dir", "../packs")).resolve()
89
- self.pharos = load_router(
90
- packs_dir,
91
- match_threshold=float(pharos_cfg.get("match_threshold", 0.30)),
92
- source_threshold=float(pharos_cfg.get("source_threshold", 0.55)),
93
- max_packs=int(pharos_cfg.get("max_packs", 2)),
94
- )
95
-
96
- # --- skills ----------------------------------------------------
97
- self.registry = SkillRegistry()
98
- self.registry.register(CodeAnalysisChip(self.repo_files, self.git_tools))
99
- self.registry.register(MigrationSafetyChip(self.schema_tools))
100
- self.registry.register(SecurityReviewChip(self.repo_files))
101
- self.registry.register(SynthesisChip(
102
- self.model, self.bdi, self.pharos,
103
- max_tokens=int(model_cfg.get("max_answer_tokens", 1536)),
104
- ))
105
- self.registry.register(TestRunnerChip(self.test_tools))
106
- self.registry.register(DisciplineGateChip(self.bdi))
107
-
108
- # --- planning + execution + sessions ---------------------------
109
- self.planner = Planner(self.bdi, self.registry)
110
- self.executor = DAGExecutor(self.registry, max_parallel=4)
111
- sess_cfg = self.config.get("session", {}) or {}
112
- self.sessions = SessionManager(
113
- ttl_seconds=int(sess_cfg.get("ttl_seconds", 3600)),
114
- rate_limit_per_hour=int(sess_cfg.get("rate_limit_per_hour", 60)),
115
- )
116
- self.org_id = org_id
117
-
118
- # ------------------------------------------------------------------
119
-
120
- def ask(self, question: str, user: str = "anonymous") -> dict:
121
- t0 = time.time()
122
- question = (question or "").strip()
123
- if not question:
124
- return {"error": "empty question"}
125
-
126
- if not self.sessions.check_rate_limit(user):
127
- return {"error": "rate limit exceeded — try again later",
128
- "user": user}
129
- session = self.sessions.get(user)
130
-
131
- plan = self.planner.form_plan(question, user)
132
- intention = self.bdi.get_intention(plan.intention_id)
133
-
134
- context = SkillContext(
135
- org_id=self.org_id,
136
- user_id=user,
137
- session_id=f"{user}:{int(session.created_at)}",
138
- metadata={
139
- "question": question,
140
- "session": session,
141
- "belief_ids": intention.belief_ids if intention else [],
142
- },
143
- )
144
-
145
- result = asyncio.run(self.executor.execute(
146
- plan.dag, context, initial_artifacts={"question": question},
147
- ))
148
-
149
- final = result.artifacts.get("final") or {}
150
- ok = result.success and bool(final.get("text"))
151
- self.planner.complete_plan(plan.intention_id, ok)
152
-
153
- if not final.get("text"):
154
- errors = result.node_errors or {"pipeline": "no final artifact"}
155
- return {
156
- "error": "plan execution failed",
157
- "node_errors": errors,
158
- "intent": plan.intent,
159
- "plan": plan.rationale,
160
- "elapsed_seconds": round(time.time() - t0, 1),
161
- }
162
-
163
- session.add_turn("user", question)
164
- session.add_turn("rivet", final["text"][:1000])
165
-
166
- return {
167
- "response": final["text"],
168
- "gate_passed": final.get("passed", False),
169
- "confidence": final.get("confidence", "LOW"),
170
- "flags": final.get("flags", []),
171
- "requires_review": final.get("requires_review", []),
172
- "beliefs_consulted": final.get("beliefs_consulted", []),
173
- "intent": plan.intent,
174
- "plan": plan.rationale,
175
- "packs_used": (result.artifacts.get("draft") or {}).get(
176
- "packs_used", []),
177
- "files_read": sorted(session.files_read),
178
- "user": user,
179
- "elapsed_seconds": round(time.time() - t0, 1),
180
- }
181
-
182
- def health(self) -> dict:
183
- return {
184
- "status": "ok",
185
- "agent": self.config.get("org", {}).get("agent_name", "Rivet"),
186
- "kintsugi_source": KINTSUGI_SOURCE,
187
- "model_backend": self.config.get("model", {}).get("backend"),
188
- "model": self.config.get("model", {}).get("name"),
189
- "skills": self.registry.list_names(),
190
- "beliefs": len(self.bdi.list_beliefs()),
191
- "packs": [p.name for p in self.pharos.library.packs],
192
- "repo_attached": self.repo_files is not None,
193
- "sessions": self.sessions.stats(),
194
- }
195
-
196
- def bdi_snapshot(self) -> dict:
197
- snap = self.bdi.get_snapshot()
198
- return {
199
- "org_id": snap.org_id,
200
- "beliefs": [
201
- {"id": b.id, "confidence": b.confidence,
202
- "source": b.source, "tags": b.tags,
203
- "content": b.content[:200]}
204
- for b in snap.beliefs
205
- ],
206
- "desires": [
207
- {"id": d.id, "priority": d.priority, "content": d.content}
208
- for d in snap.desires
209
- ],
210
- "intentions_active": len([
211
- i for i in snap.intentions if i.status.value == "active"
212
- ]),
213
- "intentions_total": len(snap.intentions),
214
- }
215
-
216
-
217
- # ----------------------------------------------------------------------
218
-
219
-
220
- def make_handler(agent: RivetAgent):
221
- class Handler(BaseHTTPRequestHandler):
222
- def do_POST(self):
223
- if self.path != "/ask":
224
- return self._json({"error": f"unknown endpoint {self.path}"}, 404)
225
- try:
226
- length = int(self.headers.get("Content-Length", 0))
227
- body = json.loads(self.rfile.read(length)) if length else {}
228
- except (json.JSONDecodeError, ValueError):
229
- return self._json({"error": "invalid JSON body"}, 400)
230
- result = agent.ask(body.get("question", ""),
231
- body.get("user", "anonymous"))
232
- self._json(result, 200 if "error" not in result else 422)
233
-
234
- def do_GET(self):
235
- if self.path == "/health":
236
- self._json(agent.health())
237
- elif self.path == "/bdi":
238
- self._json(agent.bdi_snapshot())
239
- else:
240
- self._json({"endpoints": {
241
- "POST /ask": '{"question": "...", "user": "..."}',
242
- "GET /health": "status + loaded skills/packs/beliefs",
243
- "GET /bdi": "current belief/desire/intention state",
244
- }})
245
-
246
- def _json(self, data, status=200):
247
- payload = json.dumps(data, indent=2, default=str).encode()
248
- self.send_response(status)
249
- self.send_header("Content-Type", "application/json")
250
- self.send_header("Content-Length", str(len(payload)))
251
- self.end_headers()
252
- self.wfile.write(payload)
253
-
254
- def log_message(self, fmt, *args):
255
- print(f"[rivet] {args[0] if args else ''}", flush=True)
256
-
257
- return Handler
258
-
259
-
260
- def main() -> None:
261
- ap = argparse.ArgumentParser(description="Rivet code assistant (Kintsugi v2)")
262
- ap.add_argument("--config", default=str(V2_ROOT / "kintsugi_config.yaml"))
263
- ap.add_argument("--port", type=int, default=0)
264
- ap.add_argument("--ask", help="one-shot question, print JSON and exit")
265
- ap.add_argument("--user", default="cli")
266
- ap.add_argument("--show-bdi", action="store_true")
267
- args = ap.parse_args()
268
-
269
- agent = RivetAgent(args.config)
270
-
271
- if args.show_bdi:
272
- print(json.dumps(agent.bdi_snapshot(), indent=2, default=str))
273
- return
274
- if args.ask:
275
- print(json.dumps(agent.ask(args.ask, args.user), indent=2, default=str))
276
- return
277
-
278
- port = args.port or int(
279
- (agent.config.get("server") or {}).get("port", 8100))
280
- health = agent.health()
281
- server = HTTPServer(("0.0.0.0", port), make_handler(agent))
282
- print(f"Rivet v2 listening on :{port}", flush=True)
283
- print(f" kintsugi: {health['kintsugi_source']}", flush=True)
284
- print(f" model: {health['model_backend']}:{health['model']}", flush=True)
285
- print(f" skills: {', '.join(health['skills'])}", flush=True)
286
- print(f" beliefs: {health['beliefs']} packs: {health['packs']}", flush=True)
287
- print(f" repo: {'attached' if health['repo_attached'] else 'not on this machine'}", flush=True)
288
- try:
289
- server.serve_forever()
290
- except KeyboardInterrupt:
291
- print("\nRivet shutting down.", flush=True)
292
- server.shutdown()
293
-
294
-
295
- if __name__ == "__main__":
296
- main()