Add promotion gate for tier-2 regulars

This commit is contained in:
2026-04-05 02:43:32 -06:00
parent e4a18e13ab
commit d7e475331d
2 changed files with 74 additions and 0 deletions
+50
View File
@@ -1,7 +1,13 @@
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
import json
import re import re
import httpx
from ..config import settings
from .cost_tracker import cost_tracker
HOME = Path.home() HOME = Path.home()
VAULT = HOME / "code" / "dotfiles" VAULT = HOME / "code" / "dotfiles"
SILAS_DIR = VAULT / "silas" SILAS_DIR = VAULT / "silas"
@@ -49,3 +55,47 @@ def load_all_active_regulars() -> list[Regular]:
for f in REGULARS_DIR.glob("*.md"): for f in REGULARS_DIR.glob("*.md"):
out.append(load_regular(f)) out.append(load_regular(f))
return out return out
PROMOTION_MODEL = "anthropic/claude-sonnet-4.6"
PROMOTION_PROMPT = """You are evaluating whether a one-time caller should become a recurring character.
CALLER: {name}
TRANSCRIPT:
{transcript}
A recurring character must have a 3-5 episode arc with genuine progression — not just "calls weekly to complain about the same thing." The arc must have a possible resolution.
Output JSON:
{{"promote": true|false, "arc_plan": "...", "reason": "..."}}
Bar is HIGH. Only promote if the character has real internal conflict, growth potential, and a believable resolution trajectory."""
async def _call_sonnet(prompt: str) -> dict:
async with httpx.AsyncClient(timeout=60.0) as client:
resp = await client.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={"Authorization": f"Bearer {settings.openrouter_api_key}"},
json={
"model": PROMOTION_MODEL,
"messages": [{"role": "user", "content": prompt}],
"response_format": {"type": "json_object"},
"max_tokens": 500,
},
)
resp.raise_for_status()
data = resp.json()
usage = data.get("usage", {})
cost_tracker.record_llm_call(
category="promotion_eval",
model=PROMOTION_MODEL,
usage_data=usage,
)
return json.loads(data["choices"][0]["message"]["content"])
async def evaluate_promotion(caller_name: str, call_transcript: str) -> dict:
prompt = PROMOTION_PROMPT.format(name=caller_name, transcript=call_transcript)
return await _call_sonnet(prompt)
+24
View File
@@ -1,3 +1,6 @@
import asyncio
from unittest.mock import AsyncMock, patch
from backend.services.regulars_v2 import Regular, load_regular, REGULARS_DIR, SILAS_DIR from backend.services.regulars_v2 import Regular, load_regular, REGULARS_DIR, SILAS_DIR
@@ -25,3 +28,24 @@ Silas runs a small desert cult outside Truth or Consequences...
assert reg.age == 54 assert reg.age == 54
assert "splintering" in reg.arc_state assert "splintering" in reg.arc_state
assert "Silas runs a small desert cult" in reg.lore_body assert "Silas runs a small desert cult" in reg.lore_body
def test_evaluate_promotion_returns_arc_plan_when_worthy():
from backend.services.regulars_v2 import evaluate_promotion
fake_response = {
"promote": True,
"arc_plan": "3 episodes. He'll start distant, then reveal he's actually the one who damaged the car, then resolve with an apology.",
"reason": "Has clear internal conflict with room to grow",
}
with patch("backend.services.regulars_v2._call_sonnet", new=AsyncMock(return_value=fake_response)):
result = asyncio.run(evaluate_promotion(caller_name="Bobby", call_transcript="..."))
assert result["promote"] is True
assert "3 episodes" in result["arc_plan"]
def test_evaluate_promotion_rejects_when_no_arc():
from backend.services.regulars_v2 import evaluate_promotion
fake_response = {"promote": False, "arc_plan": None, "reason": "One-note complaint, no growth"}
with patch("backend.services.regulars_v2._call_sonnet", new=AsyncMock(return_value=fake_response)):
result = asyncio.run(evaluate_promotion(caller_name="Carl", call_transcript="..."))
assert result["promote"] is False