fffc90c7a4
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>
153 lines
7.6 KiB
Python
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)}
|