sofzcc's picture
Update app.py
5fb0fa9 verified
raw
history blame
7.69 kB
import os
import glob
import math
from typing import List, Tuple
import gradio as gr
import numpy as np
from sentence_transformers import SentenceTransformer
# -----------------------------
# CONFIG
# -----------------------------
KB_DIR = "./kb" # optional: folder with .txt or .md files
EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
TOP_K = 3 # how many chunks to show per answer
CHUNK_SIZE = 500 # characters
CHUNK_OVERLAP = 100 # characters
# -----------------------------
# UTILITIES
# -----------------------------
def chunk_text(text: str, chunk_size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP) -> List[str]:
"""Split long text into overlapping chunks so retrieval is more precise."""
if not text:
return []
chunks = []
start = 0
length = len(text)
while start < length:
end = min(start + chunk_size, length)
chunk = text[start:end].strip()
if chunk:
chunks.append(chunk)
start += chunk_size - overlap
return chunks
def load_kb_texts(kb_dir: str = KB_DIR) -> List[Tuple[str, str]]:
"""
Load all .txt and .md files from the KB directory.
Returns a list of (source_name, content).
"""
texts = []
if os.path.isdir(kb_dir):
paths = glob.glob(os.path.join(kb_dir, "*.txt")) + glob.glob(os.path.join(kb_dir, "*.md"))
for path in paths:
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read()
if content.strip():
texts.append((os.path.basename(path), content))
except Exception as e:
print(f"Could not read {path}: {e}")
# If no files found, fall back to some built-in demo content
if not texts:
print("No KB files found. Using built-in demo content.")
demo_text = """
Welcome to the Self-Service KB Assistant.
This assistant is meant to help you find information inside a knowledge base.
In a real setup, it would be connected to your own articles, procedures,
troubleshooting guides and FAQs.
Good knowledge base content is:
- Clear and structured with headings, steps and expected outcomes.
- Written in a customer-friendly tone.
- Easy to scan, with short paragraphs and bullet points.
- Maintained regularly to reflect product and process changes.
Example use cases for a KB assistant:
- Agents quickly searching for internal procedures.
- Customers asking “how do I…” style questions.
- Managers analyzing gaps in documentation based on repeated queries.
"""
texts.append(("demo_content.txt", demo_text))
return texts
# -----------------------------
# KB INDEX
# -----------------------------
class KBIndex:
def __init__(self, model_name: str = EMBEDDING_MODEL_NAME):
print("Loading embedding model...")
self.model = SentenceTransformer(model_name)
print("Model loaded.")
self.chunks: List[str] = []
self.chunk_sources: List[str] = []
self.embeddings: np.ndarray | None = None
self.build_index()
def build_index(self):
"""Load KB texts, split into chunks, and build an embedding index."""
texts = load_kb_texts(KB_DIR)
all_chunks = []
all_sources = []
for source_name, content in texts:
for chunk in chunk_text(content):
all_chunks.append(chunk)
all_sources.append(source_name)
if not all_chunks:
print("⚠️ No chunks found for KB index.")
self.chunks = []
self.chunk_sources = []
self.embeddings = None
return
print(f"Creating embeddings for {len(all_chunks)} chunks...")
embeddings = self.model.encode(all_chunks, show_progress_bar=False, convert_to_numpy=True)
self.chunks = all_chunks
self.chunk_sources = all_sources
self.embeddings = embeddings
print("KB index ready.")
def search(self, query: str, top_k: int = TOP_K) -> List[Tuple[str, str, float]]:
"""Return top-k (chunk, source_name, score) for a given query."""
if not query.strip():
return []
if self.embeddings is None or not len(self.chunks):
return []
query_vec = self.model.encode([query], show_progress_bar=False, convert_to_numpy=True)[0]
# Cosine similarity
dot_scores = np.dot(self.embeddings, query_vec)
norm_docs = np.linalg.norm(self.embeddings, axis=1)
norm_query = np.linalg.norm(query_vec) + 1e-10
scores = dot_scores / (norm_docs * norm_query + 1e-10)
top_idx = np.argsort(scores)[::-1][:top_k]
results = []
for idx in top_idx:
results.append((self.chunks[idx], self.chunk_sources[idx], float(scores[idx])))
return results
kb_index = KBIndex()
# -----------------------------
# CHAT LOGIC
# -----------------------------
def build_answer(query: str) -> str:
"""Use the KB index to build a human-readable answer."""
results = kb_index.search(query, top_k=TOP_K)
if not results:
return (
"I couldn't find anything relevant in the knowledge base for this query yet.\n\n"
"If this were connected to your real KB, this would be a good moment to:\n"
"- Create a new article, or\n"
"- Improve the existing documentation for this topic."
)
intro = "Here’s what I found in the knowledge base:\n"
bullets = []
for i, (chunk, source, score) in enumerate(results, start=1):
bullets.append(f"{i}. From **{source}**:\n{chunk.strip()}\n")
guidance = (
"\nYou can ask follow-up questions, or try a more specific query if this doesn't fully answer your question."
)
return intro + "\n".join(bullets) + guidance
def chat_respond(message: str, history):
answer = build_answer(message)
# Normalize history into the format ChatInterface expects
normalized = []
if isinstance(history, list):
for item in history:
if isinstance(item, dict):
normalized.append(item)
elif isinstance(item, tuple) and len(item) == 2:
normalized.append({"role": "user", "content": item[0]})
normalized.append({"role": "assistant", "content": item[1]})
# Append new messages
normalized.append({"role": "user", "content": message})
normalized.append({"role": "assistant", "content": answer})
# MUST return: (string_answer, list_of_dict_messages)
return answer, normalized
# -----------------------------
# GRADIO UI
# -----------------------------
description = """
Ask questions as if you were talking to a knowledge base assistant.
In a real scenario, this assistant would be connected to your own
help center or internal documentation. Here, it's using a small demo
knowledge base to show how retrieval-based self-service can work.
"""
chat = gr.ChatInterface(
fn=chat_respond,
title="Self-Service KB Assistant",
description=description,
chatbot=gr.Chatbot(
height=420,
show_copy_button=True,
type="messages"
),
examples=[
"What makes a good knowledge base article?",
"How could a KB assistant help agents?",
"Why is self-service important for customer support?",
],
cache_examples=False,
)
if __name__ == "__main__":
# On Hugging Face Spaces, you don't need to specify server_name/port,
# but it's harmless if you do.
chat.launch()