GitHub Action
sync: feat: integrate protected BrgyLink chatbot AI service
9ebcdd4
Raw History Blame Contribute Delete
4.45 kB
"""HTTP-independent request handling for the bundled BrgyLink chatbot."""
import re
import threading
import time
from collections import OrderedDict
from .smart_classifier import (
MODEL_VERSION,
handle_message,
load_model,
new_session,
)
LANG_ALIASES = {
"en": "english",
"english": "english",
"fil": "tagalog",
"tl": "tagalog",
"tagalog": "tagalog",
"ilo": "ilocano",
"ilocano": "ilocano",
"pag": "pangasinan",
"pangasinan": "pangasinan",
}
LANG_DISPLAY = {
"pangasinan": "Pangasinan",
"ilocano": "Ilocano",
"tagalog": "Tagalog / Filipino",
"english": "English",
}
SESSION_ID_PATTERN = re.compile(r"[A-Za-z0-9_.-]{1,128}")
class ChatService:
"""Bounded in-memory chat state around the safety-constrained classifier."""
max_message_length = 2000
max_sessions = 1000
session_ttl_seconds = 3600
def __init__(self):
self.model = load_model()
self.sessions = OrderedDict()
self.session_last_seen = {}
self.lock = threading.Lock()
def clear_sessions(self):
with self.lock:
self.sessions.clear()
self.session_last_seen.clear()
def _prune_sessions(self, now):
expired = [
key for key, seen in self.session_last_seen.items()
if now - seen >= self.session_ttl_seconds
]
for key in expired:
self.sessions.pop(key, None)
self.session_last_seen.pop(key, None)
while len(self.sessions) >= self.max_sessions:
key, _session = self.sessions.popitem(last=False)
self.session_last_seen.pop(key, None)
def respond(self, payload):
"""Return a stable (status_code, JSON-safe payload) response tuple."""
if not isinstance(payload, dict):
return 400, {"error": "A JSON object is required."}
message = payload.get("message")
if not isinstance(message, str) or not message.strip():
return 400, {"error": "Please enter a question or message."}
message = message.strip()
if len(message) > self.max_message_length:
return 400, {"error": f"Messages must be at most {self.max_message_length} characters."}
session_id = payload.get("session_id")
if session_id is not None and (
not isinstance(session_id, str) or not SESSION_ID_PATTERN.fullmatch(session_id)
):
return 400, {
"error": "Use a non-personal session_id of 1 to 128 letters, digits, dots, underscores or hyphens."
}
language = payload.get("language")
if language is not None:
if not isinstance(language, str) or language.lower().strip() not in LANG_ALIASES:
return 400, {"error": "Unsupported language."}
language = LANG_ALIASES[language.lower().strip()]
try:
started_at = time.time()
with self.lock:
now = time.monotonic()
if session_id and session_id not in self.sessions:
self._prune_sessions(now)
self.sessions[session_id] = new_session()
elif session_id and now - self.session_last_seen.get(session_id, 0) >= self.session_ttl_seconds:
self.sessions[session_id] = new_session()
chat_session = self.sessions[session_id] if session_id else new_session()
if session_id:
self.sessions.move_to_end(session_id)
self.session_last_seen[session_id] = now
if language:
chat_session["preferred_language"] = language
result = handle_message(message, session=chat_session, model=self.model)
detected_language = result.get("language", "english")
return 200, {
"response": result["response"],
"intent": result.get("intent", "unknown"),
"confidence": result.get("similarity", 1.0),
"language": detected_language,
"language_name": LANG_DISPLAY.get(detected_language, detected_language.capitalize()),
"processing_time_ms": round((time.time() - started_at) * 1000, 2),
"model_version": MODEL_VERSION,
}
except Exception:
return 500, {"error": "Failed to process chat message. Please try again."}