Eyob-Sol's picture
Upload 41 files
ac1f51b verified
raw
history blame
4.77 kB
# app/policy.py
from __future__ import annotations
import json, os, re
from typing import Dict, Any, List, Tuple
# ---------- Loading & configuration ----------
def _root_dir() -> str:
here = os.path.dirname(os.path.abspath(__file__))
return os.path.dirname(here)
def _load_keywords_from_file() -> Dict[str, Any]:
"""Optional: data/policy_keywords.json -> {"cafe_keywords":[...], "smalltalk_regex": "..."}"""
path = os.path.join(_root_dir(), "data", "policy_keywords.json")
if not os.path.exists(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f) or {}
except Exception:
return {}
def _env_list(var: str) -> List[str]:
raw = (os.getenv(var) or "").strip()
if not raw:
return []
return [x.strip() for x in raw.split(",") if x.strip()]
def _compile_regex(patterns: List[str]) -> re.Pattern:
if not patterns:
return re.compile(r"$^") # match nothing
return re.compile(r"|".join([re.escape(k) for k in patterns]), re.I)
# Defaults (used only if not overridden by env/file)
_DEFAULT_CAFE_KEYWORDS = [
"menu","order","item","dish","pizza","burger","salad","pasta","vegan","gluten",
"price","special","deal","offer","hours","open","close","time","location","address",
"book","reserve","reservation","table","party","pickup","delivery","takeout","payment",
"futurecafe","future cafe","future-cafe","café","coffee","drinks","beverage","side"
]
_DEFAULT_SMALLTALK_RE = r"\b(hi|hello|hey|good\s+(morning|afternoon|evening)|thanks|thank you|bye|goodbye)\b"
# Merge precedence: ENV > file > defaults
_file_conf = _load_keywords_from_file()
_CAFE_KEYWORDS = _env_list("CAFE_KEYWORDS") or _file_conf.get("cafe_keywords") or _DEFAULT_CAFE_KEYWORDS
_SMALLTALK_RE_STR = os.getenv("SMALLTALK_REGEX") or _file_conf.get("smalltalk_regex") or _DEFAULT_SMALLTALK_RE
_kw_re = _compile_regex(_CAFE_KEYWORDS)
_smalltalk_re = re.compile(_SMALLTALK_RE_STR, re.I)
# ---------- Limits & messages ----------
def unrelated_limit() -> int:
"""How many off-topic turns allowed before ending (clamped 1..5)."""
try:
n = int(os.getenv("CAFE_UNRELATED_LIMIT", "3"))
return max(1, min(5, n))
except Exception:
return 3
POLITE_REFUSAL_1 = (
"I'm here to help with FutureCafe—menu, hours, reservations, and orders. "
"Could you ask something about the restaurant?"
)
POLITE_REFUSAL_2 = (
"To keep things focused, I can only help with FutureCafe. "
"Ask me about our menu, hours, or booking a table."
)
END_MESSAGE = (
"I'm only able to help with FutureCafe topics. Let's end this chat for now. "
"If you need menu, hours, or reservations, message me again anytime."
)
def refusal_message(count: int) -> str:
return POLITE_REFUSAL_1 if count <= 1 else POLITE_REFUSAL_2
# ---------- Public utilities ----------
def is_cafe_topic(text: str) -> bool:
return bool(text and _kw_re.search(text))
def is_smalltalk(text: str) -> bool:
return bool(text and _smalltalk_re.search(text))
def enforce_policy(
user_text: str,
guard_state: Dict[str, Any] | None
) -> Tuple[bool, str | None, Dict[str, Any], Dict[str, Any]]:
"""
Lightweight topic gate. Returns:
allowed: bool -> if True, send to LLM; if False, use reply_if_block
reply_if_block: Optional[str]
new_guard: dict -> persist across turns (store in State)
diag: dict -> tiny diagnostics blob for the UI
guard_state schema: {"unrelated": int, "ended": bool}
"""
text = (user_text or "").strip()
guard = dict(guard_state or {"unrelated": 0, "ended": False})
diag: Dict[str, Any] = {"limit": unrelated_limit()}
if guard.get("ended"):
diag["policy"] = "ended"
return False, END_MESSAGE, guard, diag
# Allow smalltalk to go through (LLM can handle niceties)
if is_smalltalk(text) or is_cafe_topic(text):
diag["policy"] = "ok"
return True, None, guard, diag
# Off-topic
guard["unrelated"] = int(guard.get("unrelated", 0)) + 1
diag["unrelated"] = guard["unrelated"]
if guard["unrelated"] >= unrelated_limit():
guard["ended"] = True
diag["policy"] = "ended"
return False, END_MESSAGE, guard, diag
diag["policy"] = "nudge"
return False, refusal_message(guard["unrelated"]), guard, diag
# ---------- Introspection helpers (nice for your Insights pane) ----------
def policy_snapshot() -> Dict[str, Any]:
"""Expose the active config so you can show it in the Insights JSON."""
return {
"cafe_keywords": _CAFE_KEYWORDS,
"smalltalk_regex": _SMALLTALK_RE_STR,
"unrelated_limit": unrelated_limit(),
}