Spaces:
Sleeping
Sleeping
| import json | |
| import math | |
| import re | |
| from collections import Counter | |
| from pathlib import Path | |
| from typing import Dict, List, Optional | |
| try: | |
| from sentence_transformers import SentenceTransformer | |
| import numpy as np | |
| _ST_AVAILABLE = True | |
| except ImportError: | |
| _ST_AVAILABLE = False | |
| def _tokenize(text: str) -> List[str]: | |
| return re.findall(r'\b[a-z0-9]+\b', text.lower()) | |
| def _snippet(text: str, max_chars: int = 200) -> str: | |
| if len(text) <= max_chars: | |
| return text | |
| trimmed = text[:max_chars] | |
| last_space = trimmed.rfind(' ') | |
| return (trimmed[:last_space] if last_space > 0 else trimmed) + '...' | |
| class _KeywordIndex: | |
| """TF-IDF keyword search index.""" | |
| def __init__(self, texts: List[str]): | |
| self._n = len(texts) | |
| if self._n == 0: | |
| self._idf: Dict[str, float] = {} | |
| self._vectors: List[Dict[str, float]] = [] | |
| return | |
| tokenized = [_tokenize(t) for t in texts] | |
| df: Counter = Counter() | |
| for toks in tokenized: | |
| for tok in set(toks): | |
| df[tok] += 1 | |
| self._idf = {t: math.log((self._n + 1) / (df[t] + 1)) for t in df} | |
| self._vectors = [] | |
| for toks in tokenized: | |
| tf = Counter(toks) | |
| total = len(toks) or 1 | |
| self._vectors.append( | |
| {t: (tf[t] / total) * self._idf.get(t, 0.0) for t in tf} | |
| ) | |
| def search(self, query: str, k: int) -> List[int]: | |
| if not self._vectors: | |
| return [] | |
| q_toks = _tokenize(query) | |
| if not q_toks: | |
| return list(range(min(k, self._n))) | |
| q_tf = Counter(q_toks) | |
| q_total = len(q_toks) | |
| q_vec = {t: (q_tf[t] / q_total) * self._idf.get(t, 0.0) for t in q_tf} | |
| scores = [ | |
| (i, sum(q_vec.get(t, 0.0) * dv.get(t, 0.0) for t in q_vec)) | |
| for i, dv in enumerate(self._vectors) | |
| ] | |
| scores.sort(key=lambda x: x[1], reverse=True) | |
| return [i for i, _ in scores[:k]] | |
| class Corpus: | |
| def __init__(self, data_dir: Optional[Path] = None): | |
| if data_dir is None: | |
| data_dir = Path(__file__).parent.parent / "data" | |
| self._data_dir = Path(data_dir) | |
| self._kb: List[Dict] = self._load("kb.json") | |
| self._tickets: List[Dict] = self._load("past_tickets.json") | |
| self._incidents: List[Dict] = self._load("incidents.json") | |
| self._train: List[Dict] = self._load("train_tickets.json") | |
| self._eval: List[Dict] = self._load("eval_tickets.json") | |
| self._kb_map = {a["article_id"]: a for a in self._kb if "article_id" in a} | |
| self._ticket_map = {t["ticket_id"]: t for t in self._tickets if "ticket_id" in t} | |
| self._incident_map = {i["incident_id"]: i for i in self._incidents if "incident_id" in i} | |
| self._build_indices() | |
| def _load(self, filename: str) -> List[Dict]: | |
| path = self._data_dir / filename | |
| if not path.exists(): | |
| return [] | |
| try: | |
| with open(path) as f: | |
| data = json.load(f) | |
| return data if isinstance(data, list) else [] | |
| except Exception: | |
| return [] | |
| def _kb_texts(self) -> List[str]: | |
| return [a.get("title", "") + " " + a.get("body", "") for a in self._kb] | |
| def _ticket_texts(self) -> List[str]: | |
| return [t.get("title", "") + " " + t.get("description", "") for t in self._tickets] | |
| def _incident_texts(self) -> List[str]: | |
| return [ | |
| i.get("title", "") + " " + i.get("summary", "") + " " + i.get("root_cause", "") | |
| for i in self._incidents | |
| ] | |
| def _build_indices(self): | |
| kb_texts = self._kb_texts() | |
| ticket_texts = self._ticket_texts() | |
| incident_texts = self._incident_texts() | |
| has_data = any([kb_texts, ticket_texts, incident_texts]) | |
| if _ST_AVAILABLE and has_data: | |
| self._model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") | |
| self._kb_emb = ( | |
| self._model.encode(kb_texts, show_progress_bar=False) | |
| if kb_texts else np.zeros((0, 384)) | |
| ) | |
| self._ticket_emb = ( | |
| self._model.encode(ticket_texts, show_progress_bar=False) | |
| if ticket_texts else np.zeros((0, 384)) | |
| ) | |
| self._incident_emb = ( | |
| self._model.encode(incident_texts, show_progress_bar=False) | |
| if incident_texts else np.zeros((0, 384)) | |
| ) | |
| self._use_semantic = True | |
| else: | |
| self._kb_idx = _KeywordIndex(kb_texts) | |
| self._ticket_idx = _KeywordIndex(ticket_texts) | |
| self._incident_idx = _KeywordIndex(incident_texts) | |
| self._use_semantic = False | |
| def _sem_search(self, query: str, embeddings, k: int) -> List[int]: | |
| if embeddings.shape[0] == 0: | |
| return [] | |
| q = self._model.encode([query], show_progress_bar=False)[0] | |
| q_norm = q / (np.linalg.norm(q) + 1e-8) | |
| norms = np.linalg.norm(embeddings, axis=1, keepdims=True) + 1e-8 | |
| scores = (embeddings / norms) @ q_norm | |
| top = np.argsort(scores)[::-1][:k] | |
| return top.tolist() | |
| # ------------------------------------------------------------------ # | |
| # Public API # | |
| # ------------------------------------------------------------------ # | |
| def search_kb(self, query: str, max_results: int = 5) -> List[Dict]: | |
| if self._use_semantic: | |
| indices = self._sem_search(query, self._kb_emb, max_results) | |
| else: | |
| indices = self._kb_idx.search(query, max_results) | |
| return [ | |
| { | |
| "article_id": self._kb[i].get("article_id", ""), | |
| "title": self._kb[i].get("title", ""), | |
| "snippet": _snippet(self._kb[i].get("body", "")), | |
| "section": self._kb[i].get("domain", ""), | |
| } | |
| for i in indices | |
| ] | |
| def get_article(self, article_id: str) -> Optional[Dict]: | |
| a = self._kb_map.get(article_id) | |
| if not a: | |
| return None | |
| return { | |
| "article_id": a.get("article_id", ""), | |
| "title": a.get("title", ""), | |
| "body": a.get("body", ""), | |
| "tags": a.get("tags", []), | |
| } | |
| def search_tickets( | |
| self, query: str, status: Optional[str] = None, max_results: int = 5 | |
| ) -> List[Dict]: | |
| if self._use_semantic: | |
| # Search all, then filter by status | |
| k = min(len(self._tickets), max(max_results * 3, 20)) | |
| indices = self._sem_search(query, self._ticket_emb, k) | |
| if status: | |
| indices = [ | |
| i for i in indices | |
| if self._tickets[i].get("status", "").lower() == status.lower() | |
| ] | |
| indices = indices[:max_results] | |
| else: | |
| if status: | |
| pool = [ | |
| (i, t) for i, t in enumerate(self._tickets) | |
| if t.get("status", "").lower() == status.lower() | |
| ] | |
| pool_texts = [t.get("title", "") + " " + t.get("description", "") for _, t in pool] | |
| local_idx = _KeywordIndex(pool_texts) | |
| local_hits = local_idx.search(query, max_results) | |
| indices = [pool[j][0] for j in local_hits] | |
| else: | |
| indices = self._ticket_idx.search(query, max_results) | |
| return [ | |
| { | |
| "ticket_id": self._tickets[i].get("ticket_id", ""), | |
| "title": self._tickets[i].get("title", ""), | |
| "snippet": _snippet(self._tickets[i].get("description", "")), | |
| "status": self._tickets[i].get("status", ""), | |
| } | |
| for i in indices | |
| ] | |
| def get_ticket(self, ticket_id: str) -> Optional[Dict]: | |
| t = self._ticket_map.get(ticket_id) | |
| if not t: | |
| return None | |
| return { | |
| "ticket_id": t.get("ticket_id", ""), | |
| "title": t.get("title", ""), | |
| "description": t.get("description", ""), | |
| "comments": t.get("comments", []), | |
| "resolution": t.get("resolution"), | |
| } | |
| def search_incidents(self, query: str, max_results: int = 3) -> List[Dict]: | |
| if self._use_semantic: | |
| indices = self._sem_search(query, self._incident_emb, max_results) | |
| else: | |
| indices = self._incident_idx.search(query, max_results) | |
| return [ | |
| { | |
| "incident_id": self._incidents[i].get("incident_id", ""), | |
| "title": self._incidents[i].get("title", ""), | |
| "snippet": _snippet(self._incidents[i].get("summary", "")), | |
| "severity": self._incidents[i].get("severity", ""), | |
| } | |
| for i in indices | |
| ] | |
| def get_incident(self, incident_id: str) -> Optional[Dict]: | |
| inc = self._incident_map.get(incident_id) | |
| if not inc: | |
| return None | |
| return { | |
| "incident_id": inc.get("incident_id", ""), | |
| "title": inc.get("title", ""), | |
| "summary": inc.get("summary", ""), | |
| "root_cause": inc.get("root_cause", ""), | |
| "remediation": inc.get("remediation", ""), | |
| } | |
| def train_tickets(self) -> List[Dict]: | |
| return self._train | |
| def eval_tickets(self) -> List[Dict]: | |
| return self._eval | |