Telecom / main.py
oluinioluwa814's picture
Update main.py
113b5a1 verified
Raw
History Blame Contribute Delete
17.5 kB
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:]
}