Spaces:
Running on Zero
Running on Zero
| #!/usr/bin/env python3 | |
| """Constraint tracker for cross-turn memory and constraint application. | |
| Detects user-defined constraints (word limits, formatting rules, anchors/phrases) | |
| in turn 1 and enforces them across subsequent turns using LoRA-backed learning. | |
| Example: | |
| Turn 1: "For this session, keep answers under 15 words and remember the phrase cobalt anchor." | |
| Turn 2: "What should you remember?" | |
| Expected response: Should include "cobalt anchor" and be ≤15 words. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from dataclasses import dataclass, field | |
| from typing import Optional, Dict, Any, List | |
| class DetectedConstraint: | |
| """A parsed constraint from user input.""" | |
| kind: str # "word_limit", "sentence_limit", "anchor_phrase", "format_rule", etc. | |
| value: Any # numeric (word/sentence count) or string (anchor phrase) | |
| raw_text: str # original text where constraint was found | |
| confidence: float = 0.95 | |
| class SessionConstraints: | |
| """Container for all constraints detected in a session.""" | |
| constraints: List[DetectedConstraint] = field(default_factory=list) | |
| anchor_phrases: List[str] = field(default_factory=list) | |
| word_limit: Optional[int] = None | |
| sentence_limit: Optional[int] = None | |
| format_rules: List[str] = field(default_factory=list) | |
| detected_at_turn: int = 0 | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Serialize for session storage.""" | |
| return { | |
| "anchor_phrases": self.anchor_phrases, | |
| "word_limit": self.word_limit, | |
| "sentence_limit": self.sentence_limit, | |
| "format_rules": self.format_rules, | |
| "detected_at_turn": self.detected_at_turn, | |
| "raw_constraints": [ | |
| { | |
| "kind": c.kind, | |
| "value": c.value, | |
| "raw_text": c.raw_text, | |
| "confidence": c.confidence | |
| } | |
| for c in self.constraints | |
| ] | |
| } | |
| def from_dict(cls, data: Dict[str, Any]) -> SessionConstraints: | |
| """Deserialize from session storage.""" | |
| sc = cls() | |
| sc.anchor_phrases = data.get("anchor_phrases", []) | |
| sc.word_limit = data.get("word_limit") | |
| sc.sentence_limit = data.get("sentence_limit") | |
| sc.format_rules = data.get("format_rules", []) | |
| sc.detected_at_turn = data.get("detected_at_turn", 0) | |
| # Reconstruct constraints | |
| for c in data.get("raw_constraints", []): | |
| sc.constraints.append(DetectedConstraint( | |
| kind=c.get("kind"), | |
| value=c.get("value"), | |
| raw_text=c.get("raw_text"), | |
| confidence=c.get("confidence", 0.95) | |
| )) | |
| return sc | |
| class ConstraintDetector: | |
| """Detect constraints from user input.""" | |
| # Patterns for detecting various constraint types | |
| WORD_LIMIT_PATTERNS = [ | |
| r"keep\s+answers?\s+(?:under|below|within|to)\s+(\d+)\s+words?", | |
| r"(?:answer|respond)\s+in\s+(?:under|fewer than)\s+(\d+)\s+words?", | |
| r"(\d+)\s+words?\s+(?:max|maximum|or\s+less)", | |
| r"limit\s+(?:your\s+)?answers?\s+to\s+(\d+)\s+words?", | |
| ] | |
| SENTENCE_LIMIT_PATTERNS = [ | |
| r"keep\s+(?:answers?|responses?)\s+to\s+(\d+)\s+sentences?", | |
| r"(?:answer|respond)\s+in\s+(\d+)\s+sentences?\s+(?:or\s+less)?", | |
| r"(\d+)\s+sentences?\s+(?:max|maximum)", | |
| ] | |
| ANCHOR_PHRASE_PATTERNS = [ | |
| # Quoted phrases: remember "phrase" or remember the phrase "phrase" | |
| r"remember\s+(?:the\s+phrase\s+)?['\"]([^'\"]+)['\"]", | |
| # Unquoted phrase: remember the phrase X (where X doesn't start a new sentence) | |
| r"remember\s+the\s+phrase\s+([a-z][a-z\s]+?)(?:\s+and\s+|\s+or\s+|\.|\s*$)", | |
| # Generic remember without phrase keyword | |
| r"remember\s+['\"]?([a-z][a-z\s]*?)['\"]?(?:\s+(?:and|or)|\.)", | |
| # use/include/mention with optional quotes | |
| r"remember\s+(?:to\s+)?(?:use|include|mention)\s+['\"]?([^'\"\.]+?)['\"]?(?:\s|\.)", | |
| # anchor/key phrase with colon (matches multi-word phrases) | |
| r"anchor\s*(?:phrase|word|term)?\s*:\s*([a-z][a-z\s]*?)(?:\s*\.|\s*$)", | |
| r"(?:key\s+phrase):\s+([a-z][a-z\s]*?)(?:\s*\.|\s*$)", | |
| # ── Informal phrasings ────────────────────────────────────────── | |
| # "don't forget X" / "don't forget the phrase X" | |
| r"don'?t\s+forget\s+(?:the\s+(?:phrase|word|term)\s+)?['\"]?([a-z][a-z\s]+?)['\"]?(?:[.,;]|\s+and\s+|\s*$)", | |
| # "keep in mind X" / "keep in mind the phrase X" | |
| r"keep\s+in\s+mind\s+(?:the\s+(?:phrase|word|term)\s+)?['\"]?([a-z][a-z\s]+?)['\"]?(?:[.,;]|\s+and\s+|\s*$)", | |
| # "call it/this X" / "refer to it/this as X" | |
| r"(?:call\s+(?:it|this)\s+|refer\s+to\s+(?:it|this)\s+as\s+)['\"]?([a-z][a-z\s]+?)['\"]?(?:[.,;]|\s+and\s+|\s*$)", | |
| ] | |
| FORMAT_RULE_PATTERNS = [ | |
| r"(use\s+(?:bullet\s+)?points?)", | |
| r"(format\s+as\s+(?:json|markdown|yaml))", | |
| # Negated formatting rules — restricted to real formatting targets so | |
| # ordinary negations ("no word constraint", "no constraints needed", | |
| # "no problem", "no idea") are NOT captured as constraints. | |
| r"((?:no|avoid|without|don'?t\s+use|do\s+not\s+use)\s+" | |
| r"(?:bullet\s*points?|bullets?|numbered\s+lists?|lists?|markdown|json|" | |
| r"yaml|xml|code\s*blocks?|headers?|headings?|emojis?|emoji|jargon|" | |
| r"tables?|formatting|prose|paragraphs?))", | |
| ] | |
| # Phrases that explicitly DECLINE constraints — when present, the query is | |
| # asking for NO restrictions, so we must not derive constraints from it. | |
| CONSTRAINT_NEGATION_PATTERNS = [ | |
| r"\bno\s+(?:word|sentence|length|format(?:ting)?|character)?\s*constraints?\b", | |
| r"\bno\s+constraints?\s+(?:needed|required|please)\b", | |
| r"\bno\s+(?:word|character|length)\s+limit\b", | |
| r"\bwithout\s+(?:any\s+)?constraints?\b", | |
| r"\bignore\s+(?:the\s+|any\s+|previous\s+)?constraints?\b", | |
| r"\bno\s+restrictions?\b", | |
| ] | |
| def detect(self, query: str, turn_num: int = 1) -> SessionConstraints: | |
| """Detect all constraints in a query. | |
| Args: | |
| query: User input text | |
| turn_num: Turn number (used to track when constraints were set) | |
| Returns: | |
| SessionConstraints with detected constraints | |
| """ | |
| sc = SessionConstraints(detected_at_turn=turn_num) | |
| # If the user explicitly declines constraints, derive none from this turn. | |
| for neg in self.CONSTRAINT_NEGATION_PATTERNS: | |
| if re.search(neg, query, re.IGNORECASE): | |
| return sc | |
| # Detect word limits | |
| for pattern in self.WORD_LIMIT_PATTERNS: | |
| match = re.search(pattern, query, re.IGNORECASE) | |
| if match: | |
| try: | |
| limit = int(match.group(1)) | |
| sc.word_limit = limit | |
| sc.constraints.append(DetectedConstraint( | |
| kind="word_limit", | |
| value=limit, | |
| raw_text=match.group(0), | |
| confidence=0.95 | |
| )) | |
| break | |
| except (ValueError, IndexError): | |
| pass | |
| # Detect sentence limits | |
| for pattern in self.SENTENCE_LIMIT_PATTERNS: | |
| match = re.search(pattern, query, re.IGNORECASE) | |
| if match: | |
| try: | |
| limit = int(match.group(1)) | |
| sc.sentence_limit = limit | |
| sc.constraints.append(DetectedConstraint( | |
| kind="sentence_limit", | |
| value=limit, | |
| raw_text=match.group(0), | |
| confidence=0.95 | |
| )) | |
| break | |
| except (ValueError, IndexError): | |
| pass | |
| # Detect anchor phrases | |
| for pattern in self.ANCHOR_PHRASE_PATTERNS: | |
| matches = re.finditer(pattern, query, re.IGNORECASE) | |
| for match in matches: | |
| try: | |
| phrase = match.group(1).strip() | |
| if phrase and len(phrase) > 2: # At least 3 chars | |
| sc.anchor_phrases.append(phrase) | |
| sc.constraints.append(DetectedConstraint( | |
| kind="anchor_phrase", | |
| value=phrase, | |
| raw_text=match.group(0), | |
| confidence=0.90 | |
| )) | |
| except IndexError: | |
| pass | |
| # Detect format rules | |
| for pattern in self.FORMAT_RULE_PATTERNS: | |
| matches = re.finditer(pattern, query, re.IGNORECASE) | |
| for match in matches: | |
| try: | |
| rule = match.group(1).lower().strip() | |
| if rule not in sc.format_rules: | |
| sc.format_rules.append(rule) | |
| sc.constraints.append(DetectedConstraint( | |
| kind="format_rule", | |
| value=rule, | |
| raw_text=match.group(0), | |
| confidence=0.85 | |
| )) | |
| except IndexError: | |
| pass | |
| return sc | |
| class ConstraintEnforcer: | |
| """Enforce detected constraints on responses.""" | |
| def word_count(text: str) -> int: | |
| """Count words in text (roughly).""" | |
| return len([w for w in text.split() if w.strip()]) | |
| def sentence_count(text: str) -> int: | |
| """Count sentences (roughly).""" | |
| sentences = re.split(r'[.!?]+', text.strip()) | |
| return len([s for s in sentences if s.strip()]) | |
| def has_anchor_phrases(text: str, phrases: List[str]) -> bool: | |
| """Check if all anchor phrases are present.""" | |
| text_lower = text.lower() | |
| return all(phrase.lower() in text_lower for phrase in phrases) | |
| def build_constraint_reminder(constraints: SessionConstraints) -> str: | |
| """Build a constraint reminder string for the system prompt.""" | |
| if not constraints.constraints: | |
| return "" | |
| lines = ["[SESSION CONSTRAINTS]"] | |
| if constraints.word_limit: | |
| lines.append(f"- Keep your response to {constraints.word_limit} words or fewer") | |
| if constraints.sentence_limit: | |
| lines.append(f"- Keep your response to {constraints.sentence_limit} sentences or fewer") | |
| if constraints.anchor_phrases: | |
| phrases_str = ", ".join(f'"{p}"' for p in constraints.anchor_phrases) | |
| lines.append(f"- IMPORTANT: Include these anchor phrases in your response: {phrases_str}") | |
| if constraints.format_rules: | |
| for rule in constraints.format_rules: | |
| lines.append(f"- Format: {rule}") | |
| lines.append("") | |
| return "\n".join(lines) | |
| class ConstraintTracker: | |
| """Main tracker for managing constraints across a session.""" | |
| def __init__(self): | |
| self.detector = ConstraintDetector() | |
| self.enforcer = ConstraintEnforcer() | |
| self.session_constraints: Optional[SessionConstraints] = None | |
| self.turn_count = 0 | |
| def process_turn(self, query: str, is_first_turn: bool = False) -> SessionConstraints: | |
| """Process a turn and detect/retrieve constraints. | |
| Always scans the current query for new constraints. On the first turn the | |
| session constraints are replaced; on subsequent turns newly-found anchors, | |
| limits, and format rules are merged in without clobbering what was already set. | |
| Args: | |
| query: User input | |
| is_first_turn: Whether this is the first turn (resets constraints) | |
| Returns: | |
| SessionConstraints for this turn | |
| """ | |
| self.turn_count += 1 | |
| if is_first_turn: | |
| # First turn: full reset — detect fresh from this query | |
| self.session_constraints = self.detector.detect(query, turn_num=1) | |
| else: | |
| # Fast-path: skip regex work entirely when the query has no constraint | |
| # keywords. "What is the weather?" never contains anchors or limits — | |
| # the keyword scan is O(n) and avoids 20+ regex compilations per turn. | |
| _CONSTRAINT_SIGNALS = ( | |
| 'remember', 'anchor', 'phrase', 'keyword', 'keep', 'limit', | |
| 'word', 'sentence', 'format', 'avoid', 'under', 'within', 'maximum', | |
| 'forget', 'note', 'call', 'refer', | |
| ) | |
| q_lower = query.lower() | |
| if not any(kw in q_lower for kw in _CONSTRAINT_SIGNALS): | |
| return self.session_constraints or SessionConstraints() | |
| # Mid-session: detect new constraints and merge (never clobber existing) | |
| new_sc = self.detector.detect(query, turn_num=self.turn_count) | |
| if new_sc.constraints: | |
| if not self.session_constraints: | |
| self.session_constraints = new_sc | |
| else: | |
| self._merge_into(new_sc) | |
| return self.session_constraints or SessionConstraints() | |
| def _merge_into(self, new_sc: SessionConstraints) -> None: | |
| """Merge new_sc into self.session_constraints without overwriting set values.""" | |
| sc = self.session_constraints | |
| for c in new_sc.constraints: | |
| if c.kind == "anchor_phrase" and c.value not in sc.anchor_phrases: | |
| sc.anchor_phrases.append(c.value) | |
| sc.constraints.append(c) | |
| elif c.kind == "word_limit" and sc.word_limit is None: | |
| sc.word_limit = c.value | |
| sc.constraints.append(c) | |
| elif c.kind == "sentence_limit" and sc.sentence_limit is None: | |
| sc.sentence_limit = c.value | |
| sc.constraints.append(c) | |
| elif c.kind == "format_rule" and c.value not in sc.format_rules: | |
| sc.format_rules.append(c.value) | |
| sc.constraints.append(c) | |
| def get_constraint_reminder(self) -> str: | |
| """Get the constraint reminder to inject into system prompt.""" | |
| if not self.session_constraints or not self.session_constraints.constraints: | |
| return "" | |
| return self.enforcer.build_constraint_reminder(self.session_constraints) | |
| def check_constraint_compliance(self, response: str) -> Dict[str, Any]: | |
| """Check if response meets constraints. | |
| Returns: | |
| Dict with compliance status and violations. | |
| """ | |
| if not self.session_constraints or not self.session_constraints.constraints: | |
| return {"compliant": True, "violations": []} | |
| violations = [] | |
| if self.session_constraints.word_limit: | |
| wc = self.enforcer.word_count(response) | |
| if wc > self.session_constraints.word_limit: | |
| violations.append({ | |
| "kind": "word_limit", | |
| "expected": self.session_constraints.word_limit, | |
| "actual": wc | |
| }) | |
| if self.session_constraints.sentence_limit: | |
| sc = self.enforcer.sentence_count(response) | |
| if sc > self.session_constraints.sentence_limit: | |
| violations.append({ | |
| "kind": "sentence_limit", | |
| "expected": self.session_constraints.sentence_limit, | |
| "actual": sc | |
| }) | |
| if self.session_constraints.anchor_phrases: | |
| if not self.enforcer.has_anchor_phrases(response, self.session_constraints.anchor_phrases): | |
| violations.append({ | |
| "kind": "missing_anchor_phrases", | |
| "expected": self.session_constraints.anchor_phrases | |
| }) | |
| return { | |
| "compliant": len(violations) == 0, | |
| "violations": violations | |
| } | |
| def reset(self): | |
| """Reset tracker for new session.""" | |
| self.session_constraints = None | |
| self.turn_count = 0 | |