Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import numpy as np | |
| import faiss | |
| import re | |
| from datasets import load_dataset | |
| from sentence_transformers import SentenceTransformer | |
| from google import genai | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| import logging | |
| logging.basicConfig(level=logging.INFO) | |
| # CONFIG | |
| API_KEY = os.getenv("GEMINI_API_KEY") | |
| MODEL_NAME = "gemini-2.5-flash-lite" | |
| if not API_KEY: | |
| raise ValueError("Missing GEMINI_API_KEY") | |
| TOP_K = 4 | |
| MAX_MEMORY_ENTRIES = 10 # limit memory size | |
| MEMORY_EMBED_DIM = 384 # use smaller embeddings for memory (MiniLM-L6) | |
| HF_TOKEN = os.getenv("HF_TOKEN") | |
| if not HF_TOKEN: | |
| raise ValueError("HF_TOKEN not found in environment variables") | |
| client = genai.Client(api_key=API_KEY) | |
| embedder = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2", device="cpu") | |
| memory_embedder = embedder | |
| # CACHE FILES | |
| INDEX_FILE = "faiss.index" | |
| DOC_FILE = "documents.json" | |
| EMBED_FILE = "embeddings.npy" | |
| embeddings = None | |
| #MEMORY STORAGE | |
| memory_store = {} # raw memory items per session | |
| memory_embeddings = {} # semantic embeddings per session | |
| # LOAD KNOWLEDGE | |
| def load_knowledge(): | |
| """ | |
| Load or cache telecom knowledge documents safely. | |
| Works correctly with HuggingFace streaming datasets. | |
| """ | |
| # ---------- LOAD CACHE ---------- | |
| if os.path.exists(DOC_FILE): | |
| logging.info("✅ Loading cached documents") | |
| with open(DOC_FILE, "r") as f: | |
| return json.load(f) | |
| # ---------- LOAD DATASET (STREAMING SAFE) ---------- | |
| try: | |
| ds = load_dataset( | |
| "electricsheepafrica/nigerian-telecom-customer-support-ticket-records", | |
| split="train", | |
| streaming=True, | |
| token=HF_TOKEN | |
| ) | |
| except Exception as e: | |
| logging.warning(f"Dataset load failed: {e}") | |
| return [] | |
| logging.info("⚙️ Streaming dataset and building knowledge base...") | |
| docs = [] | |
| # ---------- DETECT COLUMNS FROM FIRST ROW ---------- | |
| iterator = iter(ds) | |
| try: | |
| first_row = next(iterator) | |
| except StopIteration: | |
| logging.warning("Dataset empty") | |
| return [] | |
| available_cols = list(first_row.keys()) | |
| def safe_get(row, key): | |
| return row.get(key, "unknown") | |
| # process first row | |
| rows = [first_row] | |
| # continue streaming remaining rows | |
| MAX_DOCS = 400 # start small | |
| for i, row in enumerate(iterator): | |
| if i >= MAX_DOCS: | |
| break | |
| rows.append(row) | |
| # ---------- BUILD DOCUMENTS ---------- | |
| for i, row in enumerate(rows): | |
| text = f""" | |
| Telecommunications Support Knowledge | |
| Problem Type: {safe_get(row, 'issue_type')} | |
| Observed Context: | |
| - Operator: {safe_get(row, 'operator')} | |
| - Channel: {safe_get(row, 'channel')} | |
| - Priority: {safe_get(row, 'priority')} | |
| Likely Resolution Pattern: | |
| Cases with similar attributes were resolved in | |
| {safe_get(row, 'resolution_time_hours')} hours | |
| with customer satisfaction score | |
| {safe_get(row, 'customer_satisfaction')}. | |
| Use this as statistical troubleshooting evidence. | |
| """.strip() | |
| docs.append({ | |
| "id": f"DOC_{i}", | |
| "text": text | |
| }) | |
| # ---------- CACHE ---------- | |
| with open(DOC_FILE, "w") as f: | |
| json.dump(docs, f) | |
| logging.info(f"✅ Cached {len(docs)} telecom documents") | |
| return docs | |
| def get_kb(): | |
| global documents, index, embeddings | |
| if documents is None or index is None: | |
| documents = load_knowledge() | |
| index, embeddings = load_or_create_index(documents) | |
| return documents, index | |
| if __name__ == "__main__": | |
| logging.info("Initializing Knowledge Base...") | |
| get_kb() | |
| #TRAINING DATA COLLECTION | |
| def collect_training_data(user_input, answer, evaluation): | |
| global index, embeddings, documents | |
| # 1. Ensure everything is initialized | |
| if index is None or documents is None: | |
| get_kb() | |
| # 2. Check the evaluation threshold | |
| if (evaluation.get("score", 0) >= 9 and | |
| evaluation.get("grounded", False) and | |
| not evaluation.get("needs_improvement", True) and len(answer.get("evidence_used", [])) >= 2 | |
| ): | |
| entry_text = user_input + "\n" + json.dumps(answer) | |
| new_emb = embedder.encode([entry_text], normalize_embeddings=True) | |
| entry = { | |
| "id": f"TRAIN_{len(documents)}", | |
| "text": entry_text, | |
| "metadata": { | |
| "score": evaluation["score"], | |
| "evidence": answer.get("evidence_used", []) | |
| } | |
| } | |
| # 3. Semantic Deduplication | |
| if memory_embeddings.get("global") is None and embeddings is not None: | |
| memory_embeddings["global"] = embeddings.copy() | |
| global_mem = memory_embeddings.get("global") | |
| # Only add if it's unique enough (similarity < 0.9) | |
| if global_mem is None or np.max(cosine_similarity(new_emb, global_mem)) < 0.9: | |
| logging.info("Adding high-quality unique sample to Knowledge Base") | |
| # Update Document List | |
| documents.append(entry) | |
| # Update FAISS Index (float32 is required by FAISS) | |
| index.add(new_emb.astype("float32")) | |
| # Update Global Embeddings for future deduplication | |
| embeddings = np.vstack([embeddings, new_emb]) | |
| memory_embeddings["global"] = np.vstack([global_mem, new_emb]) if global_mem is not None else new_emb | |
| # Persistent Storage | |
| faiss.write_index(index, INDEX_FILE) | |
| np.save(EMBED_FILE, embeddings) | |
| # Save raw JSON for record keeping | |
| os.makedirs("auto_dataset", exist_ok=True) | |
| file_name = f"auto_dataset/{len(os.listdir('auto_dataset'))}.json" | |
| with open(file_name, "w") as f: | |
| json.dump(entry, f) | |
| def rebuild_index(): | |
| global index | |
| dim = embeddings.shape[1] | |
| index = faiss.IndexHNSWFlat(dim, 32, faiss.METRIC_INNER_PRODUCT) | |
| index.hnsw.efConstruction = 200 | |
| index.add(embeddings.astype("float32")) | |
| faiss.write_index(index, INDEX_FILE) | |
| if os.path.exists(INDEX_FILE): | |
| logging.info("FAST START: loading cached index") | |
| # FAISS INDEX LOADING / BUILDING | |
| def load_or_create_index(documents): | |
| global embeddings | |
| if os.path.exists(INDEX_FILE) and os.path.exists(EMBED_FILE): | |
| logging.info("✅ Loading cached FAISS index") | |
| index = faiss.read_index(INDEX_FILE) | |
| embeddings = np.load(EMBED_FILE) | |
| return index, embeddings | |
| logging.info("⚙️ Building embeddings (first run only)...") | |
| texts = [d["text"] for d in documents] | |
| embeddings = embedder.encode(texts, normalize_embeddings=True, show_progress_bar=True) | |
| # HNSW for incremental updates & RAM efficiency | |
| dim = embeddings.shape[1] | |
| index = faiss.IndexHNSWFlat(dim, 32, faiss.METRIC_INNER_PRODUCT) | |
| index.hnsw.efConstruction = 200 | |
| index.add(np.array(embeddings).astype('float32')) | |
| faiss.write_index(index, INDEX_FILE) | |
| np.save(EMBED_FILE, embeddings) | |
| logging.info("✅ FAISS index cached") | |
| return index, embeddings | |
| #index, embeddings = load_or_create_index(documents) | |
| documents = None | |
| index = None | |
| # MEMORY MANAGEMENT | |
| def get_memory(session="default"): | |
| """Return memory for a session.""" | |
| return memory_store.get(session, []) | |
| def update_memory(session, item): | |
| """ | |
| Add item to session memory. | |
| Maintains MAX_MEMORY_ENTRIES limit and updates embeddings. | |
| """ | |
| mem = memory_store.setdefault(session, []) | |
| mem.append(item) | |
| # Truncate if memory exceeds max entries | |
| if len(mem) > MAX_MEMORY_ENTRIES: | |
| # summarize old memory | |
| summary_prompt = f""" | |
| Summarize the following telecom conversation to preserve context. Keep only key facts in JSON format if possible: | |
| {mem} | |
| """ | |
| try: | |
| summary_resp = client.models.generate_content( | |
| model=MODEL_NAME, | |
| contents=summary_prompt | |
| ) | |
| summary_text = summary_resp.text | |
| except Exception as e: | |
| logging.warning(f"Memory summarization failed: {e}") | |
| summary_text = str(mem[-1]) # fallback: last entry | |
| # Replace memory with summarized entry | |
| mem = [{"summary": summary_text}] | |
| memory_store[session] = mem | |
| memory_embeddings[session] = memory_embedder.encode( | |
| [summary_text], | |
| normalize_embeddings=True | |
| ) | |
| # Update embeddings for semantic retrieval | |
| latest = mem[-1] | |
| text = str( | |
| latest.get("summary") | |
| or latest.get("assistant") | |
| or latest.get("user") | |
| or "" | |
| ) | |
| new_emb = memory_embedder.encode([text], normalize_embeddings=True) | |
| if session not in memory_embeddings: | |
| memory_embeddings[session] = new_emb | |
| else: | |
| memory_embeddings[session] = np.vstack( | |
| [memory_embeddings[session], new_emb] | |
| ) | |
| return | |
| # RETRIEVAL FUNCTION | |
| def retrieve(query, session="default"): | |
| documents, index = get_kb() | |
| """ | |
| Returns top-K documents + top memory entries based on similarity | |
| """ | |
| results = [] | |
| # ----- RAG from FAISS ----- | |
| if len(documents) > 0: | |
| q_emb = embedder.encode([query], normalize_embeddings=True).astype('float32') | |
| if index is None or len(documents) == 0: | |
| return [] | |
| D, I = index.search(q_emb, min(TOP_K, len(documents))) | |
| for idx in I[0]: | |
| results.append(documents[idx]) | |
| # ----- Memory retrieval ----- | |
| mem_embs = memory_embeddings.get(session) | |
| if mem_embs is not None and len(mem_embs) > 0: | |
| q_emb_mem = memory_embedder.encode([query], normalize_embeddings=True) | |
| sim_scores = cosine_similarity(q_emb_mem, mem_embs)[0] | |
| top_idxs = np.argsort(sim_scores)[-2:] # top 2 memories | |
| for idx in reversed(top_idxs): | |
| if sim_scores[idx] > 0.3: | |
| mem_entry = get_memory(session)[idx] | |
| results.append({ | |
| "id": f"MEM_{idx}", | |
| "text": mem_entry.get("summary") | |
| or mem_entry.get("assistant") | |
| or "" | |
| }) | |
| return results | |
| # SYSTEM PROMPT | |
| SYSTEM_PROMPT = """ | |
| You are a Tier-1 Telecommunications Support Engineer. | |
| You MUST ground every diagnosis in provided evidence. | |
| RULES: | |
| - Use ONLY given evidence documents | |
| - Cite evidence IDs used | |
| - If evidence insufficient → request human agent | |
| - No hallucination | |
| Return STRICT JSON: | |
| { | |
| "problem_category":"", | |
| "diagnosis":"", | |
| "recommended_steps":[], | |
| "evidence_used":[], | |
| "confidence_score":0-1, | |
| "requires_human_agent":false | |
| } | |
| Also determine problem_category from: network, billing, sim, device, configuration. | |
| """ | |
| # ROBUST JSON GENERATION | |
| def generate_json(prompt): | |
| try: | |
| resp = client.models.generate_content( | |
| model=MODEL_NAME, | |
| contents=prompt, | |
| config={"response_mime_type": "application/json"} | |
| ) | |
| return json.loads(resp.text) | |
| except Exception as e: | |
| logging.warning(f"JSON generation failed: {e}") | |
| return {"error": "Invalid JSON"} | |
| # STREAMING ANSWER | |
| def stream_answer(prompt): | |
| buffer = "" | |
| try: | |
| stream = client.models.generate_content_stream( | |
| model=MODEL_NAME, | |
| contents=prompt, | |
| config={"response_mime_type": "application/json"} | |
| ) | |
| for chunk in stream: | |
| if chunk.text: | |
| buffer += chunk.text | |
| if len(buffer) > 512: # yield micro-batches | |
| yield buffer | |
| buffer = "" | |
| if buffer: | |
| yield buffer | |
| except Exception as e: | |
| logging.warning(f"Streaming failed: {e}") | |
| yield '{"text": "Streaming failure"}' | |
| def rule_based_validation(answer): | |
| if not answer.get("evidence_used"): | |
| return False | |
| if answer.get("confidence_score", 0) > 0.95: | |
| return False | |
| if len(answer.get("diagnosis", "")) < 10: | |
| return False | |
| return True | |
| # AI JUDGE | |
| JUDGE_PROMPT = """ | |
| You are a telecom AI evaluator. | |
| Check: | |
| 1. Diagnosis supported by cited evidence | |
| 2. Evidence IDs exist in context | |
| 3. No unsupported claims | |
| 4. Logical troubleshooting | |
| Return JSON: | |
| { | |
| "score":0, | |
| "hallucination_risk":"", | |
| "grounded":true, | |
| "needs_improvement":false, | |
| "reason":"" | |
| } | |
| """ | |
| REQUIRED_KEYS = { | |
| "problem_category", | |
| "diagnosis", | |
| "recommended_steps", | |
| "evidence_used", | |
| "confidence_score", | |
| "requires_human_agent" | |
| } | |
| def valid_schema(answer): | |
| return isinstance(answer, dict) and REQUIRED_KEYS.issubset(answer.keys()) | |
| def judge(query, context, answer): | |
| prompt = f""" | |
| {JUDGE_PROMPT} | |
| Query: | |
| {query} | |
| Evidence Context: | |
| {json.dumps(context)} | |
| Assistant Response: | |
| {json.dumps(answer)} | |
| """ | |
| return generate_json(prompt) | |
| def validate_evidence(answer, context): | |
| context_ids = {d["id"] for d in context} | |
| used_ids = set(answer.get("evidence_used", [])) | |
| if not used_ids.issubset(context_ids): | |
| return False | |
| return True | |
| # SELF-IMPROVING RETRY LOOP | |
| def generate_with_retry(query, context, memory, category, max_retry=1): | |
| last_answer = None | |
| last_eval = None | |
| for _ in range(max_retry): | |
| prompt = f""" | |
| {SYSTEM_PROMPT} | |
| Memory: | |
| {json.dumps(memory)} | |
| Context: | |
| {json.dumps([{'id': d['id'], 'text': d['text']} for d in context])} | |
| User Issue: | |
| {query} | |
| User Issue Category: | |
| {category} | |
| """ | |
| answer = generate_json(prompt) | |
| last_answer = answer | |
| if not valid_schema(answer): | |
| continue | |
| if not rule_based_validation(answer): | |
| continue | |
| if not validate_evidence(answer, context): | |
| answer["requires_human_agent"] = True | |
| answer["confidence_score"] = 0.3 | |
| last_eval = judge(query, context, answer) | |
| # Retry if low confidence or requires human | |
| if answer.get("confidence_score", 1) < 0.7 or answer.get("requires_human_agent", False): | |
| l2_prompt = f""" | |
| {SYSTEM_PROMPT} | |
| You are retrying because confidence was low. | |
| Re-evaluate STRICTLY using evidence only. | |
| Memory: | |
| {json.dumps(memory)} | |
| Context: | |
| {json.dumps([{'id': d['id'], 'text': d['text']} for d in context])} | |
| User Issue: | |
| {query} | |
| """ | |
| answer = generate_json(l2_prompt) | |
| last_answer = answer | |
| last_eval = judge(query, context, answer) | |
| # Success threshold | |
| if last_eval.get("score", 0) >= 6 and last_eval.get("grounded", True): | |
| return last_answer, last_eval | |
| return last_answer, last_eval | |
| # ANALYTICS LOGGING | |
| analytics = [] | |
| def log_session(query, result, evaluation, category): | |
| analytics.append({ | |
| "query": query, | |
| "category": category, | |
| "score": evaluation.get("score", 0), | |
| "hallucination": evaluation.get("hallucination_risk", "") | |
| }) | |
| # MAIN TELECOM AGENT (non-streaming) | |
| # ===================================================== | |
| def telecom_agent(user_input, session="default"): | |
| memory = get_memory(session) | |
| category = "unknown" | |
| context = retrieve(user_input, session) | |
| if not context: | |
| return { | |
| "problem_category": "unknown", | |
| "diagnosis": "No supporting evidence found", | |
| "recommended_steps": [], | |
| "evidence_used": [], | |
| "confidence_score": 0.0, | |
| "requires_human_agent": True | |
| }, {"score": 0}, [] | |
| answer, evaluation = generate_with_retry(user_input, context, memory, category) | |
| update_memory(session, {"user": user_input, "assistant": answer}) | |
| log_session(user_input, answer, evaluation, category) | |
| collect_training_data(user_input, answer, evaluation) | |
| # Return latest 5 analytics for insight | |
| return answer, evaluation, analytics[-5:] | |
| def safe_parse(text): | |
| match = re.search(r'\{.*?\}', text, re.DOTALL) | |
| if match: | |
| try: | |
| return json.loads(match.group()) | |
| except: | |
| return {"error": "corrupt json"} | |
| return {"error": "no json found"} | |
| # STREAMING VERSION | |
| def telecom_agent_stream(user_input, session="default"): | |
| memory = get_memory(session) | |
| category = "unknown" | |
| context = retrieve(user_input, session) | |
| if not context: | |
| yield {"type": "final", "answer": { | |
| "problem_category": "unknown", | |
| "diagnosis": "No supporting evidence found", | |
| "recommended_steps": [], | |
| "evidence_used": [], | |
| "confidence_score": 0.0, | |
| "requires_human_agent": True | |
| }} | |
| return | |
| prompt = f""" | |
| {SYSTEM_PROMPT} | |
| Memory: | |
| {json.dumps(memory)} | |
| Context: | |
| {json.dumps([{'id': d['id'], 'text': d['text']} for d in context])} | |
| User Issue: | |
| {user_input} | |
| """ | |
| final_text = "" | |
| for partial in stream_answer(prompt): | |
| final_text += partial | |
| yield {"text": partial} | |
| # Robust final parsing | |
| try: | |
| answer = safe_parse(final_text) | |
| except Exception as e: | |
| logging.warning(f"Final JSON parsing failed: {e}") | |
| answer = {"error": "Invalid JSON"} | |
| evaluation = judge(user_input, context, answer) | |
| update_memory(session, { | |
| "user": user_input, | |
| "assistant": json.dumps(answer) | |
| }) | |
| log_session(user_input, answer, evaluation, category) | |
| collect_training_data(user_input, answer, evaluation) | |
| yield { | |
| "type": "final", | |
| "answer": answer, | |
| "evaluation": evaluation, | |
| "analytics": analytics[-5:] | |
| } |