Files
ten31-database/backend/mcp/architect_agent.py
T
Keysat fffc90c7a4 Replace v5 settlement spine with v2.0 reserve-asset spine (v0.1.0:73)
Swap the dead "scarcity as the connecting idea" / bitcoin-as-settlement
spine for the v2.0 reserve-asset spine (bitcoin = apex non-debasable
reserve asset; debasement = forcing function; AI = abundance engine;
throughline is an asset-value/capital-flow claim, not settlement; three
seams Energy<->Compute, Debasement<->Bitcoin, AI<->Data-Ownership)
everywhere it was still encoded in live code, the seed, and the docs.

- architect_agent.py / outreach_agent.py: both system prompts carried
  "scarcity as the connecting idea" and shipped settlement framing into
  every generated draft; rewritten to the reserve-asset spine.
- thesis_seed.py: THROUGHLINE, PILLAR_1, the AI/energy-operator segment
  angle, and THESIS_V2 corrected and voice-cleaned (no em dash / "X, not
  Y" / "bet"). PILLAR_2/3 (real revenue, founder access) kept.
- ensure_thesis_v2_promoted / revert_thesis_v2_promotion: make the v2.0
  spine the working APPROVED spine and re-ground/clean the core nodes,
  deployment-state-invariant (structural targeting, not body text) and
  fully reversible (captures prior body/title/status/deleted_at). NODE
  level only: never sets a thesis_version canonical (guardrail #4); no
  hard deletes (guardrail #3). Wired into init_db after the v2 candidate
  stage.
- docs/thesis-handoff.md replaced wholesale with the complete v2.0 doc;
  Ten31_Agentic_Build_Plan.md + PHASE_1.md throughline glosses updated.

The v2.0 spine remains an unratified draft from the signal-engine
workstream: canonical freeze stays the partners' dual sign-off, and
Appendix-A conviction/exposure figures stay Grant's working read.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 08:22:24 -05:00

153 lines
7.6 KiB
Python

"""The Architect agent — generates and revises thesis options using Claude.
This is the one step that runs on the frontier model (Claude) rather than the
local Qwen, because sharpening narrative is exactly Claude's strength. Thesis
content is non-sensitive house messaging, so it goes to Claude directly with no
redaction. The agent reads the current thesis and writes new VARIANT drafts back
through architect_tools; nothing it produces is canonical until a human approves.
Flexibility by design (per Grant): a node can hold any number of competing
options at once. generate_options adds N drafts to a node's variant group;
partners keep, refine, branch, or retire them freely.
Requires ANTHROPIC_API_KEY in the environment (wired as a StartOS config secret
on the box). Fails gracefully with a clear message if the key or SDK is absent.
"""
import json
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import architect_tools as at # noqa: E402
MODEL = os.environ.get("CRM_ARCHITECT_MODEL", "claude-opus-4-8")
VOICE = ("Direct, concrete, conviction-driven. Never use 'bet'/'betting'/'gamble' "
"language (we invest with conviction). No em dashes. No 'X, not Y' "
"constructions. No kitchen-sink lists. Plain sentences a serious LP can "
"verify in their head, with real specifics where possible.")
def _client():
try:
from anthropic import Anthropic
except Exception:
raise RuntimeError("anthropic SDK not installed in this environment")
key = os.environ.get("ANTHROPIC_API_KEY")
if not key:
raise RuntimeError("ANTHROPIC_API_KEY not configured")
return Anthropic(api_key=key)
def _render_thesis(thesis):
"""Flatten the current thesis tree into text for the (cacheable) system prompt."""
line = thesis.get("line", {})
lines = [f"Thesis line: {line.get('name')} ({line.get('line_key')})"]
def walk(nodes, depth=0):
for n in nodes:
body = (n.get("body") or "").strip()
tag = n.get("node_type")
var = f" [variant_group={n['variant_group']}]" if n.get("variant_group") else ""
lines.append(f"{' ' * depth}- {tag}{var}: {n.get('title') or ''} {body}".rstrip())
walk(n.get("children", []), depth + 1)
walk(thesis.get("tree", []))
return "\n".join(lines)
def _system(thesis):
text = ("You are the Architect, the in-house copilot that sharpens Ten31's investment "
"thesis with the partners. Ten31 invests in critical infrastructure across bitcoin, "
"AI, energy, and freedom technologies. The spine of the thesis: fiat is being debased "
"while AI drives the marginal cost of anything reproducible toward zero, so durable "
"value migrates to what stays provably scarce and verifiable. Bitcoin is the apex form "
"of that, a fixed-supply, non-debasable, verifiable reserve asset. AI is the abundance "
"engine and bitcoin is the scarcity anchor, two faces of one megatrend. The throughline "
"is an asset-value and capital-flow claim: as money debases and AI commoditizes the "
"reproducible, value accrues to the scarce side of one supply chain and the monetary "
"premium accrues to bitcoin as the non-debasable reserve asset. This is not a claim "
"that the world transacts, settles, or clears in bitcoin. The structure runs on three "
"seams: Energy and Compute (the same scarce firm power feeds both AI and bitcoin), "
"Debasement and Bitcoin (bitcoin as reserve and as pristine collateral for credit, "
"never payments), and AI and Data-Ownership (sovereign data and confidential inference, "
"the own-your-stack and own-your-inference layer). Strike is a financial-services and "
"reserve re-rate, never a payments story. Proof standard: every proof point must be "
"falsifiable as scaled substance with a number, never a first-instance milestone. Do "
"not invent proof and do not over-expose sensitive deal or return specifics. "
f"VOICE RULES (follow exactly): {VOICE}\n\n"
"Here is the current working thesis:\n" + _render_thesis(thesis))
# Cache the thesis context so iterating across requests is cheap.
return [{"type": "text", "text": text, "cache_control": {"type": "ephemeral"}}]
def _parse_options(text):
text = re.sub(r"^```(json)?|```$", "", text.strip(), flags=re.MULTILINE).strip()
m = re.search(r"\[.*\]", text, re.DOTALL)
if not m:
return []
try:
arr = json.loads(m.group(0))
return [o for o in arr if isinstance(o, dict) and o.get("text")]
except json.JSONDecodeError:
return []
def _call(thesis, user_text, max_tokens=1800):
resp = _client().messages.create(model=MODEL, max_tokens=max_tokens,
system=_system(thesis),
messages=[{"role": "user", "content": user_text}])
return "".join(b.text for b in resp.content if getattr(b, "type", None) == "text")
def generate_options(line_key, node_id, n=3, guidance="", db=None):
"""Generate N fresh alternative versions of one node and add them as variant
drafts in that node's variant group."""
thesis = at.get_thesis(line_key, db=db)
if thesis.get("error"):
return thesis
node = at.get_node(node_id, db=db)
if node.get("error"):
return node
user = (f'Generate {n} distinct alternative versions of this {node["node_type"]} '
f'(current text: "{(node.get("body") or "").strip()}").\n'
f'Guidance from the partner: {guidance.strip() or "sharpen it and make it more concrete"}.\n'
f'Each option should be genuinely different in angle, not minor rewordings. '
f'Return ONLY a JSON array of {n} objects, each {{"text": "...", "rationale": "one short line"}}.')
raw = _call(thesis, user)
options = _parse_options(raw)[:n]
if not options:
return {"error": "no_options_parsed", "raw": raw[:300]}
group = node.get("variant_group") or node_id
# Make sure the original node shares the group, so the UI shows them together.
if not node.get("variant_group"):
at.upsert_thesis_node(node["line_id"], node["node_type"], node.get("body") or "",
title=node.get("title"), node_id=node_id, variant_group=group,
change_reason="grouped for variants", db=db)
created = []
for opt in options:
r = at.upsert_thesis_node(node["line_id"], node["node_type"], opt["text"],
title=node.get("title"), parent_id=node.get("parent_id"),
variant_group=group,
meta={"architect_generated": True, "rationale": opt.get("rationale"),
"guidance": guidance}, db=db)
created.append({"id": r.get("id"), "text": opt["text"], "rationale": opt.get("rationale")})
return {"node_id": node_id, "variant_group": group, "generated": len(created), "options": created}
def revise(line_key, node_id, feedback, n=2, db=None):
"""Revise one node in light of partner feedback, producing N options that
address it (kept as variant drafts)."""
return generate_options(line_key, node_id, n=n,
guidance=f"Address this partner feedback directly: {feedback}", db=db)
def status():
"""Whether the Architect can run (key + SDK present)."""
try:
_client()
return {"ready": True, "model": MODEL}
except Exception as exc:
return {"ready": False, "reason": str(exc)}