Spaces:
Sleeping
Sleeping
File size: 4,774 Bytes
74bb5fe ac1f51b 74bb5fe ac1f51b 74bb5fe ac1f51b 74bb5fe ac1f51b 74bb5fe ac1f51b 74bb5fe ac1f51b 74bb5fe ac1f51b 74bb5fe ac1f51b 74bb5fe ac1f51b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# 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(),
} |