ai-steering / app.py
abhimittal's picture
Switch Space to free, fully offline simulated mode (no API key needed)
094dbef verified
Raw
History Blame Contribute Delete
12.1 kB
"""Gradio demo for the ai-steering package β€” offline/simulated mode.
No API key required and no live model calls are made. Each tab runs a real,
deterministic Python stand-in for the piece of logic that would otherwise
call Claude, so the demo is genuinely interactive without costing anything.
Install `ANTHROPIC_API_KEY` locally and run `examples/*.py` (or import
`ai_steering` directly) to see the real, Claude-backed behavior this Space
is illustrating.
"""
from __future__ import annotations
import re
import gradio as gr
from ai_steering.guardrails import GuardrailConfig, _keyword_prefilter
from ai_steering.routing import ROUTES
BANNER = (
"> πŸ”Œ **Simulated mode** β€” this Space makes no live model calls, so no API key is needed. "
"Where noted, a small local heuristic stands in for the Claude call the real "
"`ai_steering` function would make. The keyword prefilter in the Guardrails tab is the "
"actual production code, since it's designed to never need a model call."
)
# ---------------------------------------------------------------------------
# Reasoning Enhancement β€” curated worked examples (the real chain_of_thought /
# tree_of_thoughts functions need a live Claude call; these are what they return).
# ---------------------------------------------------------------------------
_COT_EXAMPLES = {
"A farmer has 17 sheep. All but 9 die. How many sheep are left?": {
"thinking": (
"'All but 9 die' means every sheep except 9 dies β€” so the number that survive "
"is exactly the number excluded from dying, not 17 minus 9. The total of 17 is "
"a distractor; the answer is the 9 explicitly spared."
),
"answer": "Answer: 9",
},
"If a train travels 60 miles in 45 minutes, what is its speed in mph?": {
"thinking": (
"Speed = distance / time. Convert 45 minutes to hours: 45/60 = 0.75 hours. "
"60 miles / 0.75 hours = 80 mph."
),
"answer": "Answer: 80 mph",
},
}
_TOT_EXAMPLE_BRANCHES = [
{
"approach": "Vertical (shard by tenant)",
"reasoning": "Each tenant's data stays on one shard, keyed by tenant_id. Simple routing, no cross-shard joins for single-tenant queries.",
"answer": "Shard by tenant_id with consistent hashing.",
"score": 0.62,
"critique": "Great isolation, but large tenants create hot shards and rebalancing is disruptive.",
},
{
"approach": "Horizontal (shard by entity range)",
"reasoning": "Split by a monotonic key range (e.g. user_id ranges). Enables even data growth distribution and simple range scans.",
"answer": "Range-shard by user_id with a routing table for range-to-shard lookup.",
"score": 0.78,
"critique": "Better load distribution than tenant sharding, but range hotspots on sequential IDs need mitigation (e.g. hashed prefixes).",
},
{
"approach": "Directory-based dynamic sharding",
"reasoning": "A lookup service maps keys to shards explicitly, allowing shards to be split/merged/moved without a rehashing event.",
"answer": "Directory service + hashed keys, with online shard splitting.",
"score": 0.85,
"critique": "Most operationally flexible and best long-term choice, at the cost of an extra directory-service hop and more moving parts to operate.",
},
]
def run_reasoning(question: str, mode: str):
if mode == "Chain of Thought":
example = _COT_EXAMPLES.get(question) or next(iter(_COT_EXAMPLES.values()))
return example["thinking"], example["answer"]
branches_md = "\n\n".join(
f"**{b['approach']}** (score {b['score']:.2f})\n{b['reasoning']}\n→ {b['answer']}\n\n*{b['critique']}*"
for b in _TOT_EXAMPLE_BRANCHES
)
best = max(_TOT_EXAMPLE_BRANCHES, key=lambda b: b["score"])
return branches_md, f"**Best answer:** {best['answer']}"
# ---------------------------------------------------------------------------
# Context Window Management β€” real buffer logic, with a word-count token
# estimate and a naive local condenser standing in for the Claude calls.
# ---------------------------------------------------------------------------
_context_messages: list[dict[str, str]] = []
def _estimate_tokens(messages: list[dict[str, str]]) -> int:
return sum(len(m["content"].split()) for m in messages)
def _local_condense(messages: list[dict[str, str]]) -> str:
words = " ".join(m["content"] for m in messages).split()
preview = " ".join(words[:15])
return f"{preview}... [{len(words)} words condensed into this summary]"
def context_reset(max_tokens: int, keep_recent: int):
global _context_messages
_context_messages = []
return "Buffer reset.", 0
def context_add(turn: str, max_tokens: int, keep_recent: int):
global _context_messages
if not turn.strip():
raise gr.Error("Enter a message first.")
_context_messages.append({"role": "user", "content": turn})
keep_recent = int(keep_recent)
if _estimate_tokens(_context_messages) > int(max_tokens):
recent = _context_messages[-keep_recent:] if keep_recent else []
older = _context_messages[: len(_context_messages) - keep_recent] if keep_recent else _context_messages
if older:
summary = _local_condense(older)
candidate = [
{"role": "user", "content": f"[Earlier conversation summary β€” simulated]\n{summary}"},
{"role": "assistant", "content": "Understood, I have the context from before."},
] + recent
# If the condensed version still doesn't fit, drop the oldest kept-recent
# messages one at a time (mirrors ai_steering.context.fit_to_budget).
while len(candidate) > 2 and _estimate_tokens(candidate) > int(max_tokens):
candidate.pop(2)
_context_messages = candidate
transcript = "\n\n".join(f"**{m['role']}**: {m['content']}" for m in _context_messages)
return transcript, _estimate_tokens(_context_messages)
# ---------------------------------------------------------------------------
# Semantic Guardrails β€” the keyword prefilter is the real production code
# (it's designed to run without a model call); the semantic layer is a tiny
# word-overlap heuristic standing in for the Claude classifier call.
# ---------------------------------------------------------------------------
def run_guardrails(message: str, allowed_topics: str, blocked_keywords: str):
if not message.strip():
raise gr.Error("Enter a message first.")
config = GuardrailConfig(
allowed_topics=[t.strip() for t in allowed_topics.split(",") if t.strip()],
blocked_keywords=[k.strip() for k in blocked_keywords.split(",") if k.strip()],
)
prefiltered = _keyword_prefilter(message, config)
if prefiltered is not None:
return f"🚫 **Blocked by keyword prefilter** (real code, no model call)\n\n{prefiltered.reason}"
if config.allowed_topics:
topic_words = {w.lower() for topic in config.allowed_topics for w in re.findall(r"\w+", topic)}
message_words = {w.lower() for w in re.findall(r"\w+", message)}
on_topic = bool(topic_words & message_words)
if not on_topic:
return (
"🚫 **Blocked β€” off-topic** (simulated: word-overlap heuristic standing in for "
f"the real semantic classifier)\n\nNo overlap with allowed topics: {', '.join(config.allowed_topics)}"
)
return (
"βœ… **Allowed** β€” keyword prefilter found nothing blocked, and the message is on-topic.\n\n"
"*In live mode, the request would now go to Claude for a real answer.*"
)
# ---------------------------------------------------------------------------
# Model Routing β€” a real local heuristic classifier standing in for the cheap
# Claude classification call; the routed answer itself is illustrative.
# ---------------------------------------------------------------------------
_COMPLEX_HINTS = ("design", "architecture", "fault-tolerant", "exactly-once", "distributed", "optimal", "trade-off", "tradeoff")
_MODERATE_HINTS = ("summarize", "compare", "explain", "why", "how")
def _local_classify(prompt: str) -> tuple[str, str]:
lowered = prompt.lower()
word_count = len(prompt.split())
if any(h in lowered for h in _COMPLEX_HINTS) or word_count > 25:
return "complex", "matched a complex-task keyword or the prompt is long"
if any(h in lowered for h in _MODERATE_HINTS) or word_count > 10:
return "moderate", "matched a multi-step-reasoning keyword or moderate length"
return "simple", "short, direct factual request"
def run_routing(prompt: str):
if not prompt.strip():
raise gr.Error("Enter a prompt first.")
complexity, reason = _local_classify(prompt)
model = ROUTES[complexity]
summary = f"Routed to **{model}** β€” classified as *{complexity}* (simulated heuristic: {reason})"
return summary, "*In live mode, this is where the routed model's real answer would appear.*"
with gr.Blocks(title="AI Steering") as demo:
gr.Markdown("# Core AI Steering Skills\n" + BANNER)
with gr.Tab("Reasoning Enhancement"):
gr.Markdown("Chain of Thought forces step-by-step reasoning; Tree of Thoughts explores several approaches and picks the best.")
r_question = gr.Dropdown(
list(_COT_EXAMPLES.keys()),
value=next(iter(_COT_EXAMPLES.keys())),
label="Question (Chain of Thought mode)",
)
r_mode = gr.Radio(["Chain of Thought", "Tree of Thoughts"], value="Chain of Thought", label="Mode")
r_button = gr.Button("Show example")
r_trace = gr.Markdown(label="Reasoning trace")
r_answer = gr.Markdown(label="Answer")
r_button.click(run_reasoning, inputs=[r_question, r_mode], outputs=[r_trace, r_answer])
with gr.Tab("Context Window Management"):
gr.Markdown("Add turns to a conversation with a small token budget and watch it condense older turns instead of forgetting them.")
with gr.Row():
c_budget = gr.Number(label="Max tokens (simulated, word-based)", value=25)
c_keep = gr.Number(label="Keep recent messages", value=3)
c_reset = gr.Button("Reset buffer")
c_turn = gr.Textbox(label="Next message", value="My name is Priya and I'm building a rocket telemetry dashboard.")
c_add = gr.Button("Add message")
c_tokens = gr.Number(label="Current token count (simulated)", interactive=False)
c_transcript = gr.Markdown(label="Buffered transcript")
c_reset.click(context_reset, inputs=[c_budget, c_keep], outputs=[c_transcript, c_tokens])
c_add.click(context_add, inputs=[c_turn, c_budget, c_keep], outputs=[c_transcript, c_tokens])
with gr.Tab("Semantic Guardrails"):
gr.Markdown("A keyword prefilter (real code) plus a semantic layer (simulated here) decide whether a message reaches the model.")
g_topics = gr.Textbox(label="Allowed topics (comma-separated, blank = any topic)", value="billing subscription plan")
g_keywords = gr.Textbox(label="Blocked keywords (comma-separated)", value="ignore previous instructions")
g_message = gr.Textbox(label="User message", value="How do I upgrade my subscription plan?")
g_button = gr.Button("Check")
g_output = gr.Markdown()
g_button.click(run_guardrails, inputs=[g_message, g_topics, g_keywords], outputs=g_output)
with gr.Tab("Model Routing"):
gr.Markdown("A cheap classifier (simulated here as a local heuristic) decides which Claude tier a request is routed to.")
m_prompt = gr.Textbox(label="Prompt", value="What's the capital of Japan?")
m_button = gr.Button("Route")
m_summary = gr.Markdown()
m_answer = gr.Markdown()
m_button.click(run_routing, inputs=[m_prompt], outputs=[m_summary, m_answer])
if __name__ == "__main__":
demo.launch()