Architect agent: Claude-powered thesis generation (backend scaffolding)
- backend/mcp/architect_agent.py: generate_options + revise on Claude (prompt-
cached thesis context, claude-opus-4-8, Ten31 voice rules). Writes N variant
drafts to a node's variant group; nothing canonical without human approval.
Fails gracefully if the API key / SDK is absent.
- server.py endpoints: GET /api/architect/status, GET /api/thesis/{key}/tree,
GET /api/thesis/nodes/{id}/variants, POST .../generate, POST .../feedback,
POST /api/thesis/lines, POST /api/thesis/lines/{key}/nodes. architect_tools
gains get_node_variants.
- Dockerfile installs `anthropic`; docker_entrypoint loads ANTHROPIC_API_KEY from
/data/secrets/anthropic-api-key (self-disabling until the key is dropped in).
Full HTTP surface verified end-to-end (graceful 502 without a key).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,16 @@ try:
|
||||
except Exception:
|
||||
entity_jobs = None
|
||||
|
||||
# Phase-1: the Architect agent (runs on Claude) + its tools live in backend/mcp.
|
||||
# (Compute the path inline — this runs before BASE_DIR is defined below.)
|
||||
try:
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "mcp"))
|
||||
import architect_tools as _architect_tools # type: ignore
|
||||
import architect_agent as _architect_agent # type: ignore
|
||||
except Exception:
|
||||
_architect_tools = None
|
||||
_architect_agent = None
|
||||
|
||||
# ─── Configuration ────────────────────────────────────────────────────────────
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -1758,6 +1768,12 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
return self.handle_get_thesis_version(user, path.split('/')[-1])
|
||||
if re.match(r'^/api/thesis/[^/]+/canonical$', path):
|
||||
return self.handle_get_canonical_thesis(user, path.split('/')[-2])
|
||||
if path == '/api/architect/status':
|
||||
return self.handle_architect_status(user)
|
||||
if re.match(r'^/api/thesis/nodes/[^/]+/variants$', path):
|
||||
return self.handle_get_node_variants(user, path.split('/')[-2])
|
||||
if re.match(r'^/api/thesis/[^/]+/tree$', path):
|
||||
return self.handle_get_thesis_tree(user, path.split('/')[-2])
|
||||
|
||||
# ─── Entity-merge review queue ───
|
||||
if path == '/api/entities/merge-candidates':
|
||||
@@ -1830,6 +1846,15 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
# ─── Architect thesis review (Phase 1, human approval gate) ───
|
||||
if re.match(r'^/api/thesis/versions/[^/]+/review$', path):
|
||||
return self.handle_thesis_review(user, path.split('/')[-2], body)
|
||||
# ─── Architect generation (Claude) ───
|
||||
if re.match(r'^/api/thesis/nodes/[^/]+/generate$', path):
|
||||
return self.handle_generate_options(user, path.split('/')[-2], body)
|
||||
if re.match(r'^/api/thesis/nodes/[^/]+/feedback$', path):
|
||||
return self.handle_node_feedback(user, path.split('/')[-2], body)
|
||||
if path == '/api/thesis/lines':
|
||||
return self.handle_create_thesis_line(user, body)
|
||||
if re.match(r'^/api/thesis/lines/[^/]+/nodes$', path):
|
||||
return self.handle_add_thesis_node(user, path.split('/')[-2], body)
|
||||
|
||||
# ─── UI-triggered index jobs + entity-merge decisions (Phase 1) ───
|
||||
if path == '/api/index/rebuild':
|
||||
@@ -3539,6 +3564,94 @@ class CRMHandler(BaseHTTPRequestHandler):
|
||||
return self.send_error_json(res['error'], 400)
|
||||
return self.send_json({"data": res})
|
||||
|
||||
# ─── Architect thesis authoring (Phase 1) ───
|
||||
def handle_get_thesis_tree(self, user, line_key):
|
||||
if _architect_tools is None:
|
||||
return self.send_error_json("Unavailable", 503)
|
||||
return self.send_json(_architect_tools.get_thesis(line_key, DB_PATH))
|
||||
|
||||
def handle_create_thesis_line(self, user, body):
|
||||
if not require_admin(user):
|
||||
return self.send_error_json("Admin required", 403)
|
||||
if _architect_tools is None:
|
||||
return self.send_error_json("Unavailable", 503)
|
||||
body = body or {}
|
||||
if not body.get('line_key') or not body.get('name'):
|
||||
return self.send_error_json("line_key and name required", 400)
|
||||
return self.send_json({"data": _architect_tools.create_thesis_line(
|
||||
body['line_key'], body['name'], segment_key=body.get('segment_key'),
|
||||
is_core=bool(body.get('is_core')), description=body.get('description'), db=DB_PATH)})
|
||||
|
||||
def handle_add_thesis_node(self, user, line_key, body):
|
||||
if not require_admin(user):
|
||||
return self.send_error_json("Admin required", 403)
|
||||
if _architect_tools is None:
|
||||
return self.send_error_json("Unavailable", 503)
|
||||
conn = get_db()
|
||||
row = conn.execute("SELECT id FROM thesis_lines WHERE line_key=? AND deleted_at IS NULL", (line_key,)).fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
return self.send_error_json("Line not found", 404)
|
||||
body = body or {}
|
||||
return self.send_json({"data": _architect_tools.upsert_thesis_node(
|
||||
row['id'], body.get('node_type', 'claim'), body.get('body', ''), title=body.get('title'),
|
||||
parent_id=body.get('parent_id'), node_id=body.get('node_id'),
|
||||
variant_group=body.get('variant_group'), change_reason=body.get('change_reason'), db=DB_PATH)})
|
||||
|
||||
# ─── Architect agent (Phase 1, runs on Claude) ───
|
||||
def handle_architect_status(self, user):
|
||||
if _architect_agent is None:
|
||||
return self.send_error_json("Unavailable", 503)
|
||||
return self.send_json({"data": _architect_agent.status()})
|
||||
|
||||
def handle_get_node_variants(self, user, node_id):
|
||||
if _architect_tools is None:
|
||||
return self.send_error_json("Unavailable", 503)
|
||||
return self.send_json(_architect_tools.get_node_variants(node_id, DB_PATH))
|
||||
|
||||
def _architect_line_key(self, node_id):
|
||||
conn = get_db()
|
||||
row = conn.execute("SELECT l.line_key FROM thesis_nodes n JOIN thesis_lines l ON l.id=n.line_id WHERE n.id=?",
|
||||
(node_id,)).fetchone()
|
||||
conn.close()
|
||||
return row['line_key'] if row else None
|
||||
|
||||
def handle_generate_options(self, user, node_id, body):
|
||||
if not require_admin(user):
|
||||
return self.send_error_json("Admin required", 403)
|
||||
if _architect_agent is None:
|
||||
return self.send_error_json("Unavailable", 503)
|
||||
lk = self._architect_line_key(node_id)
|
||||
if not lk:
|
||||
return self.send_error_json("Node not found", 404)
|
||||
body = body or {}
|
||||
try:
|
||||
res = _architect_agent.generate_options(lk, node_id, int(body.get('n', 3) or 3),
|
||||
body.get('guidance', '') or '', DB_PATH)
|
||||
except Exception as exc:
|
||||
return self.send_error_json(str(exc), 502)
|
||||
if res.get('error'):
|
||||
return self.send_error_json(res.get('raw') or res['error'], 502)
|
||||
return self.send_json({"data": res})
|
||||
|
||||
def handle_node_feedback(self, user, node_id, body):
|
||||
if not require_admin(user):
|
||||
return self.send_error_json("Admin required", 403)
|
||||
if _architect_agent is None:
|
||||
return self.send_error_json("Unavailable", 503)
|
||||
lk = self._architect_line_key(node_id)
|
||||
if not lk:
|
||||
return self.send_error_json("Node not found", 404)
|
||||
body = body or {}
|
||||
try:
|
||||
res = _architect_agent.revise(lk, node_id, body.get('feedback', '') or '',
|
||||
int(body.get('n', 2) or 2), DB_PATH)
|
||||
except Exception as exc:
|
||||
return self.send_error_json(str(exc), 502)
|
||||
if res.get('error'):
|
||||
return self.send_error_json(res.get('raw') or res['error'], 502)
|
||||
return self.send_json({"data": res})
|
||||
|
||||
# ─── Architect thesis (Phase 1) ───
|
||||
def handle_list_thesis_lines(self, user):
|
||||
if thesis_review is None:
|
||||
|
||||
Reference in New Issue
Block a user