Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -587,19 +587,123 @@ def chat_respond(message: str, history: list) -> str:
|
|
| 587 |
|
| 588 |
|
| 589 |
# -----------------------------
|
| 590 |
-
#
|
| 591 |
# -----------------------------
|
| 592 |
|
| 593 |
-
|
| 594 |
-
|
| 595 |
|
| 596 |
-
Ask questions and get instant answers from the knowledge base using semantic search.
|
| 597 |
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
|
| 604 |
examples = [
|
| 605 |
q["query"]
|
|
@@ -611,13 +715,70 @@ examples = [
|
|
| 611 |
"Why is self-service important for customer support?",
|
| 612 |
]
|
| 613 |
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
|
| 622 |
# -----------------------------
|
| 623 |
# LAUNCH
|
|
@@ -630,7 +791,7 @@ if __name__ == "__main__":
|
|
| 630 |
if is_huggingface or is_container:
|
| 631 |
env_label = "π€ HuggingFace Spaces" if is_huggingface else "π³ container"
|
| 632 |
logger.info(f"Launching on {env_label}...")
|
| 633 |
-
|
| 634 |
else:
|
| 635 |
logger.info("π» Launching locally...")
|
| 636 |
-
|
|
|
|
| 587 |
|
| 588 |
|
| 589 |
# -----------------------------
|
| 590 |
+
# KB FILE MANAGEMENT
|
| 591 |
# -----------------------------
|
| 592 |
|
| 593 |
+
ALLOWED_EXTENSIONS = {".txt", ".md"}
|
| 594 |
+
MAX_FILE_SIZE_MB = 10
|
| 595 |
|
|
|
|
| 596 |
|
| 597 |
+
def list_kb_files() -> str:
|
| 598 |
+
"""Return a formatted markdown table of current KB files."""
|
| 599 |
+
paths = sorted(
|
| 600 |
+
glob.glob(os.path.join(KB_DIR, "*.txt")) +
|
| 601 |
+
glob.glob(os.path.join(KB_DIR, "*.md"))
|
| 602 |
+
)
|
| 603 |
+
if not paths:
|
| 604 |
+
return "_No files in knowledge base yet. Upload some documents to get started._"
|
| 605 |
+
|
| 606 |
+
rows = ["| File | Size |", "|------|------|"]
|
| 607 |
+
for p in paths:
|
| 608 |
+
size_kb = os.path.getsize(p) / 1024
|
| 609 |
+
rows.append(f"| {os.path.basename(p)} | {size_kb:.1f} KB |")
|
| 610 |
+
return "\n".join(rows)
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
def upload_kb_files(files) -> Tuple[str, str]:
|
| 614 |
+
"""
|
| 615 |
+
Accept uploaded files, validate them, save to KB_DIR, and rebuild the index.
|
| 616 |
+
Returns (status_message, updated_file_list).
|
| 617 |
+
"""
|
| 618 |
+
if not files:
|
| 619 |
+
return "β οΈ No files selected.", list_kb_files()
|
| 620 |
+
|
| 621 |
+
os.makedirs(KB_DIR, exist_ok=True)
|
| 622 |
+
|
| 623 |
+
saved = []
|
| 624 |
+
skipped = []
|
| 625 |
+
|
| 626 |
+
for file in files:
|
| 627 |
+
# Gradio passes either a filepath string or a NamedString object
|
| 628 |
+
filepath = file if isinstance(file, str) else file.name
|
| 629 |
+
original_name = os.path.basename(filepath)
|
| 630 |
+
ext = os.path.splitext(original_name)[1].lower()
|
| 631 |
+
|
| 632 |
+
if ext not in ALLOWED_EXTENSIONS:
|
| 633 |
+
skipped.append(f"{original_name} (unsupported type β use .txt or .md)")
|
| 634 |
+
continue
|
| 635 |
+
|
| 636 |
+
size_mb = os.path.getsize(filepath) / (1024 * 1024)
|
| 637 |
+
if size_mb > MAX_FILE_SIZE_MB:
|
| 638 |
+
skipped.append(f"{original_name} (too large β max {MAX_FILE_SIZE_MB} MB)")
|
| 639 |
+
continue
|
| 640 |
+
|
| 641 |
+
dest = os.path.join(KB_DIR, original_name)
|
| 642 |
+
try:
|
| 643 |
+
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
|
| 644 |
+
content = f.read()
|
| 645 |
+
if not content.strip():
|
| 646 |
+
skipped.append(f"{original_name} (empty file)")
|
| 647 |
+
continue
|
| 648 |
+
with open(dest, "w", encoding="utf-8") as f:
|
| 649 |
+
f.write(content)
|
| 650 |
+
saved.append(original_name)
|
| 651 |
+
logger.info(f"KB file saved: {dest}")
|
| 652 |
+
except Exception as e:
|
| 653 |
+
skipped.append(f"{original_name} (error: {e})")
|
| 654 |
+
logger.error(f"Failed to save {original_name}: {e}")
|
| 655 |
+
|
| 656 |
+
if saved:
|
| 657 |
+
logger.info("Rebuilding KB index after upload...")
|
| 658 |
+
try:
|
| 659 |
+
kb_index.reload()
|
| 660 |
+
reload_msg = "β
Index rebuilt successfully."
|
| 661 |
+
except Exception as e:
|
| 662 |
+
reload_msg = f"β οΈ Index rebuild failed: {e}"
|
| 663 |
+
logger.error(reload_msg)
|
| 664 |
+
else:
|
| 665 |
+
reload_msg = ""
|
| 666 |
+
|
| 667 |
+
status_parts = []
|
| 668 |
+
if saved:
|
| 669 |
+
status_parts.append(f"β
**Uploaded:** {', '.join(saved)}")
|
| 670 |
+
if skipped:
|
| 671 |
+
status_parts.append(f"β οΈ **Skipped:** {', '.join(skipped)}")
|
| 672 |
+
if reload_msg:
|
| 673 |
+
status_parts.append(reload_msg)
|
| 674 |
+
|
| 675 |
+
return "\n\n".join(status_parts), list_kb_files()
|
| 676 |
+
|
| 677 |
+
|
| 678 |
+
def delete_kb_file(filename: str) -> Tuple[str, str]:
|
| 679 |
+
"""Delete a file from the KB directory and rebuild the index."""
|
| 680 |
+
filename = filename.strip()
|
| 681 |
+
if not filename:
|
| 682 |
+
return "β οΈ No filename provided.", list_kb_files()
|
| 683 |
+
|
| 684 |
+
# Safety: strip any path components β only allow plain filenames
|
| 685 |
+
filename = os.path.basename(filename)
|
| 686 |
+
ext = os.path.splitext(filename)[1].lower()
|
| 687 |
+
if ext not in ALLOWED_EXTENSIONS:
|
| 688 |
+
return f"β οΈ Invalid file type: {filename}", list_kb_files()
|
| 689 |
+
|
| 690 |
+
target = os.path.join(KB_DIR, filename)
|
| 691 |
+
if not os.path.exists(target):
|
| 692 |
+
return f"β οΈ File not found: {filename}", list_kb_files()
|
| 693 |
+
|
| 694 |
+
try:
|
| 695 |
+
os.remove(target)
|
| 696 |
+
logger.info(f"Deleted KB file: {target}")
|
| 697 |
+
kb_index.reload()
|
| 698 |
+
return f"ποΈ **Deleted** `{filename}` and rebuilt index.", list_kb_files()
|
| 699 |
+
except Exception as e:
|
| 700 |
+
logger.error(f"Failed to delete {filename}: {e}")
|
| 701 |
+
return f"β Could not delete `{filename}`: {e}", list_kb_files()
|
| 702 |
+
|
| 703 |
+
|
| 704 |
+
# -----------------------------
|
| 705 |
+
# GRADIO UI
|
| 706 |
+
# -----------------------------
|
| 707 |
|
| 708 |
examples = [
|
| 709 |
q["query"]
|
|
|
|
| 715 |
"Why is self-service important for customer support?",
|
| 716 |
]
|
| 717 |
|
| 718 |
+
with gr.Blocks(title=f"π€ {CONFIG['client']['name']}") as demo:
|
| 719 |
+
gr.Markdown(f"# π€ {CONFIG['client']['name']}")
|
| 720 |
+
|
| 721 |
+
with gr.Tabs():
|
| 722 |
+
|
| 723 |
+
# ββ Tab 1: Chat ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 724 |
+
with gr.TabItem("π¬ Chat"):
|
| 725 |
+
gr.Markdown("""
|
| 726 |
+
Ask questions and get instant answers from the knowledge base using semantic search.
|
| 727 |
+
|
| 728 |
+
**Tips:** Be specific Β· Use keywords Β· Ask one question at a time
|
| 729 |
+
""")
|
| 730 |
+
gr.ChatInterface(
|
| 731 |
+
fn=chat_respond,
|
| 732 |
+
examples=examples,
|
| 733 |
+
cache_examples=False,
|
| 734 |
+
)
|
| 735 |
+
|
| 736 |
+
# ββ Tab 2: KB Manager ββββββββββββββββββββββββββββββββββββββββ
|
| 737 |
+
with gr.TabItem("π KB Manager"):
|
| 738 |
+
gr.Markdown("""
|
| 739 |
+
### Manage Knowledge Base Documents
|
| 740 |
+
|
| 741 |
+
Upload `.txt` or `.md` files to expand what the assistant can answer.
|
| 742 |
+
The index is rebuilt automatically after every upload or deletion.
|
| 743 |
+
""")
|
| 744 |
+
|
| 745 |
+
with gr.Row():
|
| 746 |
+
with gr.Column(scale=2):
|
| 747 |
+
upload_input = gr.File(
|
| 748 |
+
label="Upload documents (.txt or .md, max 10 MB each)",
|
| 749 |
+
file_count="multiple",
|
| 750 |
+
file_types=[".txt", ".md"],
|
| 751 |
+
)
|
| 752 |
+
upload_btn = gr.Button("β¬οΈ Upload & Index", variant="primary")
|
| 753 |
+
upload_status = gr.Markdown(label="Upload Status")
|
| 754 |
+
|
| 755 |
+
with gr.Column(scale=1):
|
| 756 |
+
gr.Markdown("#### Current KB Files")
|
| 757 |
+
kb_file_list = gr.Markdown(value=list_kb_files)
|
| 758 |
+
|
| 759 |
+
gr.Markdown("#### Delete a File")
|
| 760 |
+
delete_input = gr.Textbox(
|
| 761 |
+
placeholder="exact filename e.g. my_doc.md",
|
| 762 |
+
label="Filename to delete",
|
| 763 |
+
show_label=False,
|
| 764 |
+
)
|
| 765 |
+
delete_btn = gr.Button("ποΈ Delete", variant="stop")
|
| 766 |
+
delete_status = gr.Markdown(label="Delete Status")
|
| 767 |
+
|
| 768 |
+
# Wire up upload
|
| 769 |
+
upload_btn.click(
|
| 770 |
+
fn=upload_kb_files,
|
| 771 |
+
inputs=[upload_input],
|
| 772 |
+
outputs=[upload_status, kb_file_list],
|
| 773 |
+
)
|
| 774 |
+
|
| 775 |
+
# Wire up delete
|
| 776 |
+
delete_btn.click(
|
| 777 |
+
fn=delete_kb_file,
|
| 778 |
+
inputs=[delete_input],
|
| 779 |
+
outputs=[delete_status, kb_file_list],
|
| 780 |
+
)
|
| 781 |
+
|
| 782 |
|
| 783 |
# -----------------------------
|
| 784 |
# LAUNCH
|
|
|
|
| 791 |
if is_huggingface or is_container:
|
| 792 |
env_label = "π€ HuggingFace Spaces" if is_huggingface else "π³ container"
|
| 793 |
logger.info(f"Launching on {env_label}...")
|
| 794 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|
| 795 |
else:
|
| 796 |
logger.info("π» Launching locally...")
|
| 797 |
+
demo.launch(share=False)
|