Spaces:
Sleeping
Sleeping
| import os | |
| from pathlib import Path | |
| from dataclasses import dataclass | |
| import re | |
| import torch | |
| import numpy as np | |
| import sys | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| try: | |
| from .dataset import ( | |
| Vocabulary, tokenize, DISTORTION_LABELS, CRISIS_LABELS, | |
| PAD_TOKEN, SOS_TOKEN, EOS_TOKEN, | |
| ) | |
| from .models import AnupamaModel | |
| except ImportError: | |
| from dataset import ( | |
| Vocabulary, tokenize, DISTORTION_LABELS, CRISIS_LABELS, | |
| PAD_TOKEN, SOS_TOKEN, EOS_TOKEN, | |
| ) | |
| from model.models import AnupamaModel | |
| class ClassifierOutputs: | |
| crisis_label: str # "safe" | "at_risk" | "crisis" | |
| crisis_probs: list[float] | |
| mood_score: int # 1β5 | |
| valence: float # continuous 1β5 | |
| distortion: str # distortion label or "none" | |
| distortion_probs: list[float] | |
| class EngineResponse: | |
| text: str | |
| classifiers: ClassifierOutputs | |
| is_crisis: bool | |
| conditioning_tokens: list[str] | |
| def tokens_to_sentence(tokens: list[str]) -> str: | |
| """Detokenize: join tokens and clean up spacing around punctuation.""" | |
| text = " ".join(tokens) | |
| text = re.sub(r" ([?.!,])", r"\1", text) | |
| text = re.sub(r"\s+", " ", text).strip() | |
| if text and text[0].islower(): | |
| text = text[0].upper() + text[1:] | |
| # Ensure sentence ends with punctuation | |
| if text and text[-1] not in ".?!": | |
| text += "." | |
| return text | |
| class Anupama: | |
| CRISIS_PROTOCOL = ( | |
| "I hear you, and I'm genuinely concerned about your safety right now. " | |
| "Please reach out to a crisis line β they're available 24/7:\n\n" | |
| "988 Suicide & Crisis Lifeline: Call or text 988 (US)\n" | |
| "Crisis Text Line: Text HOME to 741741\n" | |
| "Emergency: Call 911 if you're in immediate danger\n\n" | |
| "You don't have to go through this alone." | |
| ) | |
| MODE_COND_TOKENS = { | |
| "support": "<MODE_SUPPORT>", | |
| "cbt": "<MODE_CBT>", | |
| "intake": "<MODE_INTAKE>", | |
| } | |
| def __init__( | |
| self, | |
| model: AnupamaModel, | |
| vocab: Vocabulary, | |
| device: torch.device, | |
| max_gen_len: int = 80, | |
| temperature: float = 0.85, | |
| top_p: float = 0.92, | |
| ): | |
| self.model = model.to(device) | |
| self.model.eval() | |
| self.vocab = vocab | |
| self.device = device | |
| self.max_gen_len = max_gen_len | |
| self.temperature = temperature | |
| self.top_p = top_p | |
| def load(cls, checkpoint_dir: str, device_str: str = "auto") -> "Anupama": | |
| """Load model from a training checkpoint directory.""" | |
| if device_str == "auto": | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| else: | |
| device = torch.device(device_str) | |
| ckpt_dir = Path(checkpoint_dir) | |
| vocab = Vocabulary.load(ckpt_dir / "vocab.pkl") | |
| embed_matrix = np.load(ckpt_dir / "embed_matrix.npy") | |
| model = AnupamaModel(embed_matrix, len(vocab), pad_idx=vocab.pad_idx) | |
| # Load best model or fall back to final | |
| ckpt_path = ckpt_dir / "best_model.pt" | |
| if not ckpt_path.exists(): | |
| ckpt_path = ckpt_dir / "final_model.pt" | |
| ckpt = torch.load(ckpt_path, map_location=device) | |
| model.load_state_dict(ckpt["model_state"]) | |
| print(f"[Engine] Loaded model from {ckpt_path}") | |
| return cls(model, vocab, device) | |
| # ββ Tokenize & encode ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _encode(self, text: str): | |
| tokens = tokenize(text) | |
| ids = self.vocab.encode(tokens) | |
| if not ids: | |
| ids = [self.vocab.unk_idx] | |
| id_tensor = torch.tensor([ids], dtype=torch.long, device=self.device) | |
| lengths = torch.tensor([len(ids)]) | |
| return id_tensor, lengths | |
| def classify(self, text: str) -> ClassifierOutputs: | |
| id_tensor, lengths = self._encode(text) | |
| # Crisis | |
| crisis_labels, crisis_probs = self.model.crisis.predict(id_tensor, lengths) | |
| c_label = CRISIS_LABELS[crisis_labels[0].item()] | |
| c_probs = crisis_probs[0].tolist() | |
| # Sentiment | |
| mood_labels, valence, sent_probs = self.model.sentiment.predict(id_tensor, lengths) | |
| mood = mood_labels[0].item() | |
| v = valence[0].item() | |
| # Distortion | |
| dist_labels, dist_probs = self.model.distortion.predict(id_tensor, lengths) | |
| d_label = DISTORTION_LABELS[dist_labels[0].item()] | |
| d_probs = dist_probs[0].tolist() | |
| return ClassifierOutputs( | |
| crisis_label=c_label, | |
| crisis_probs=c_probs, | |
| mood_score=mood, | |
| valence=round(v, 2), | |
| distortion=d_label, | |
| distortion_probs=d_probs, | |
| ) | |
| # ββ Build conditioning tokens from classifier outputs ββββββββββββββββββ | |
| def _build_cond_tokens(self, cls_out: ClassifierOutputs, mode: str) -> list[str]: | |
| cond = [] | |
| # Crisis token | |
| cond.append(f"<{cls_out.crisis_label.upper()}>") | |
| # Mood token | |
| cond.append(f"<MOOD_{cls_out.mood_score}>") | |
| # Mode token | |
| cond.append(self.MODE_COND_TOKENS.get(mode, "<MODE_SUPPORT>")) | |
| # Distortion signal | |
| if cls_out.distortion != "none": | |
| cond.append("<DISTORTION>") | |
| else: | |
| cond.append("<NO_DISTORTION>") | |
| return cond | |
| # ββ Generate response ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _generate(self, text: str, cond_tokens: list[str]) -> str: | |
| id_tensor, lengths = self._encode(text) | |
| cond_ids = [ | |
| self.vocab.word2idx.get(tok, self.vocab.unk_idx) | |
| for tok in cond_tokens | |
| ] | |
| token_ids = self.model.generator.generate( | |
| src_ids=id_tensor, | |
| src_lengths=lengths, | |
| cond_ids=cond_ids, | |
| sos_idx=self.vocab.sos_idx, | |
| eos_idx=self.vocab.eos_idx, | |
| max_len=self.max_gen_len, | |
| temperature=self.temperature, | |
| top_p=self.top_p, | |
| ) | |
| tokens = self.vocab.decode(token_ids) | |
| # Strip special tokens that leaked through | |
| tokens = [t for t in tokens if not (t.startswith("<") and t.endswith(">"))] | |
| return tokens_to_sentence(tokens) | |
| def respond(self, text: str, mode: str = "support") -> EngineResponse: | |
| """ | |
| Full pipeline: classify input β build cond tokens β generate response. | |
| Crisis inputs skip generation and return the crisis protocol. | |
| """ | |
| cls_out = self.classify(text) | |
| if cls_out.crisis_label == "crisis": | |
| return EngineResponse( | |
| text=self.CRISIS_PROTOCOL, | |
| classifiers=cls_out, | |
| is_crisis=True, | |
| conditioning_tokens=[], | |
| ) | |
| cond_tokens = self._build_cond_tokens(cls_out, mode) | |
| response_text = self._generate(text, cond_tokens) | |
| # Safety fallback: if generated response is too short or empty | |
| if len(response_text.strip()) < 10: | |
| response_text = self._fallback_response(cls_out, mode) | |
| return EngineResponse( | |
| text=response_text, | |
| classifiers=cls_out, | |
| is_crisis=False, | |
| conditioning_tokens=cond_tokens, | |
| ) | |
| def _fallback_response(self, cls_out: ClassifierOutputs, mode: str) -> str: | |
| """Rule-based fallback when generation produces low-quality output.""" | |
| if cls_out.mood_score <= 2: | |
| return ("It sounds like you're going through something really difficult. " | |
| "I'm here to listen. Would you like to share more about what's been happening?") | |
| if cls_out.distortion != "none": | |
| return ("I notice there might be some challenging thought patterns at play. " | |
| "Let's slow down and look at this together β what's the situation you're facing?") | |
| return ("Thank you for sharing that with me. " | |
| "How long have you been feeling this way?") | |
| if __name__ == "__main__": | |
| import sys | |
| if len(sys.argv) < 2: | |
| print("Usage: python engine.py <checkpoint_dir>") | |
| sys.exit(1) | |
| engine = Anupama.load(sys.argv[1]) | |
| test_inputs = [ | |
| ("I've been feeling really anxious about my job and can't sleep", "support"), | |
| ("I always fail everything, I'm just a useless person", "cbt"), | |
| ("I've been struggling for about three weeks now", "intake"), | |
| ("I don't see any point in going on", "support"), # should trigger crisis | |
| ] | |
| for text, mode in test_inputs: | |
| print(f"\n{'β'*60}") | |
| print(f"User [{mode}]: {text}") | |
| result = engine.respond(text, mode) | |
| print(f"Crisis: {result.is_crisis} | " | |
| f"Mood: {result.classifiers.mood_score} | " | |
| f"Distortion: {result.classifiers.distortion}") | |
| print(f"Cond: {result.conditioning_tokens}") | |
| print(f"Bot: {result.text}") | |