"""Constitutional AI self-critique + GRPO rule-reward loop.
Runs end-to-end in pure stdlib + numpy. The CAI loop uses a handwritten critic
that stands in for an LLM self-judge. The GRPO loop uses a deterministic math
grader as the reward source. Both loops produce the metrics you would wire
into a real optimizer.
"""
from __future__ import annotations
import random
import re
from typing import Callable
import numpy as np
CONSTITUTION = [
"The response must directly answer the question asked, without hedging.",
"The response must not include unnecessary filler or padding.",
"If the question has a single numeric answer, state the number plainly.",
"The response must not refuse a reasonable, benign request.",
]
def critique(response: str, principle: str) -> dict:
problems = []
lowered = response.strip().lower()
if len(response.split()) > 40 and "plainly" in principle:
problems.append("answer buried in extra prose")
if lowered.startswith(("i can't", "i cannot", "as an ai")):
problems.append("unwarranted refusal")
if response.count(",") > 4 and "hedging" not in problems:
problems.append("too much hedging")
if "maybe" in lowered or "i think" in lowered:
problems.append("overhedged phrasing")
return {"principle": principle, "problems": problems}
def revise(response: str, critique_result: dict) -> str:
problems = " ".join(critique_result["problems"])
if "answer buried" in problems:
sentences = [s.strip() for s in response.split(".") if s.strip()]
if sentences:
return sentences[-1] + "."
if "unwarranted refusal" in problems:
return "Here is the answer: " + response.split(":")[-1].strip()
if "overhedged" in problems:
return (
response.replace("I think ", "")
.replace("maybe ", "")
.replace("Maybe ", "")
)
return response
def cai_stage_one(prompts_and_responses: list[tuple[str, str]]) -> list[dict]:
revised_pairs = []
for prompt, response in prompts_and_responses:
principle = random.choice(CONSTITUTION)
crit = critique(response, principle)
revised = revise(response, crit)
revised_pairs.append(
{
"prompt": prompt,
"initial": response,
"principle": principle,
"problems": crit["problems"],
"revised": revised,
"changed": revised != response,
}
)
return revised_pairs
def reward_math(prompt: str, response: str) -> float:
try:
cleaned = prompt.replace("What is ", "").replace("?", "").strip()
expected = eval(cleaned, {"__builtins__": {}}, {})
except Exception:
return 0.0
numbers = re.findall(r"-?\d+", response)
if not numbers:
return 0.0
try:
return 1.0 if int(numbers[-1]) == int(expected) else 0.0
except (ValueError, TypeError):
return 0.0
def reward_format(response: str) -> float:
return 1.0 if re.search(r".*?", response) else 0.0
def combined_reward(prompt: str, response: str) -> float:
return reward_math(prompt, response) + 0.1 * reward_format(response)
def group_relative_advantage(rewards: list[float]) -> np.ndarray:
r = np.array(rewards, dtype=float)
if r.std() < 1e-8:
return np.zeros_like(r)
return (r - r.mean()) / (r.std() + 1e-8)
def grpo_step(
policy_logprobs: np.ndarray,
ref_logprobs: np.ndarray,
advantages: np.ndarray,
beta: float = 0.01,
clip_eps: float = 0.2,
) -> dict:
ratios = np.exp(policy_logprobs - ref_logprobs)
unclipped = ratios * advantages
clipped = np.clip(ratios, 1 - clip_eps, 1 + clip_eps) * advantages
policy_loss = -np.minimum(unclipped, clipped).mean()
kl = (ref_logprobs - policy_logprobs).mean()
total_loss = policy_loss + beta * kl
return {
"policy_loss": float(policy_loss),
"kl": float(kl),
"total_loss": float(total_loss),
"mean_ratio": float(ratios.mean()),
"advantage_range": float(advantages.max() - advantages.min()),
}
def mock_sampler(rng: random.Random) -> Callable[[str], str]:
"""Stand-in for an LLM policy. Returns a string that sometimes contains
the correct answer to a simple arithmetic prompt, sometimes wrapped in
tags, sometimes not. Good enough to exercise the reward shape."""
def sampler(prompt: str) -> str:
try:
cleaned = prompt.replace("What is ", "").replace("?", "").strip()
correct = eval(cleaned, {"__builtins__": {}}, {})
except Exception:
correct = 0
mode = rng.choice(["correct", "off_by_one", "wrong", "formatted", "verbose"])
if mode == "correct":
return f"The answer is {correct}."
if mode == "off_by_one":
return f"The answer is {int(correct) + rng.choice([-1, 1])}."
if mode == "wrong":
return f"The answer is {rng.randint(-20, 20)}."
if mode == "formatted":
return f"{correct}"
return (
"Let me think about this step by step, first I consider the operands, "
f"then I perform the operation carefully, and so the final answer is {correct}."
)
return sampler
def self_improvement_round(
prompts: list[str],
sampler: Callable[[str], str],
group_size: int = 8,
) -> dict:
per_prompt = []
for prompt in prompts:
responses = [sampler(prompt) for _ in range(group_size)]
rewards = [combined_reward(prompt, r) for r in responses]
advantages = group_relative_advantage(rewards)
best_idx = int(np.argmax(rewards))
per_prompt.append(
{
"prompt": prompt,
"mean_reward": float(np.mean(rewards)),
"best_reward": float(np.max(rewards)),
"std_reward": float(np.std(rewards)),
"best_response": responses[best_idx],
"advantages": advantages.tolist(),
}
)
overall = float(np.mean([m["mean_reward"] for m in per_prompt]))
return {"per_prompt": per_prompt, "overall_mean": overall}
def demo_constitutional_loop() -> None:
print("=" * 70)
print("PART 1 / CONSTITUTIONAL AI SELF-CRITIQUE")
print("=" * 70)
raw = [
("What is the capital of France?",
"Well, I think maybe it could be Paris, but there are many cities."),
("What is 12 plus 7?",
"I cannot answer math questions in this context."),
("Name a color.",
"Colors are a deep topic, with many hues, shades, and cultural meanings, "
"and among the many options a reasonable choice would be blue."),
]
for pair in cai_stage_one(raw):
print(f"\nPrompt : {pair['prompt']}")
print(f"Principle: {pair['principle']}")
print(f"Initial : {pair['initial']}")
print(f"Problems : {pair['problems']}")
print(f"Revised : {pair['revised']}")
print(f"Changed? : {pair['changed']}")
def demo_grpo_loop() -> None:
print("\n" + "=" * 70)
print("PART 2 / GRPO WITH RULE-BASED REWARDS")
print("=" * 70)
rng = random.Random(42)
sampler = mock_sampler(rng)
prompts = [
"What is 3 + 4?",
"What is 9 - 2?",
"What is 6 * 7?",
"What is 20 // 5?",
]
group_size = 8
for round_idx in range(3):
print(f"\n-- Round {round_idx + 1} / group_size={group_size} --")
result = self_improvement_round(prompts, sampler, group_size=group_size)
for m in result["per_prompt"]:
print(
f" {m['prompt']:<22} "
f"mean={m['mean_reward']:.3f} "
f"best={m['best_reward']:.3f} "
f"std={m['std_reward']:.3f}"
)
print(f" overall mean reward: {result['overall_mean']:.3f}")
print("\n-- Synthetic GRPO update --")
rewards = [1.0, 0.0, 0.1, 0.0, 1.0, 1.0, 0.0, 0.1]
advantages = group_relative_advantage(rewards)
policy_logprobs = np.array([-1.2, -2.1, -1.9, -2.4, -1.1, -1.0, -2.3, -2.0])
ref_logprobs = policy_logprobs - 0.05
stats = grpo_step(policy_logprobs, ref_logprobs, advantages)
print(f" rewards : {rewards}")
print(f" advantages : {advantages.round(3).tolist()}")
print(f" policy_loss : {stats['policy_loss']:.4f}")
print(f" kl : {stats['kl']:.4f}")
print(f" total_loss : {stats['total_loss']:.4f}")
print(f" mean ratio : {stats['mean_ratio']:.4f}")
print(f" adv range : {stats['advantage_range']:.4f}")
if __name__ == "__main__":
random.seed(0)
np.random.seed(0)
demo_constitutional_loop()
demo_grpo_loop()
print("\n" + "=" * 70)
print("DONE")
print("=" * 70)