Add promotion gate for tier-2 regulars
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user