Spaces:
Sleeping
Sleeping
Upload 9 files
Browse files- README.md +18 -11
- app.py +271 -0
- kb/billing_invoices.md +6 -0
- kb/build_first_automation.md +16 -0
- kb/connect_whatsapp.md +18 -0
- kb/get_started.md +14 -0
- kb/reset_password.md +8 -0
- kb/troubleshoot_instagram_connect.md +13 -0
- requirements.txt +6 -0
README.md
CHANGED
|
@@ -1,12 +1,19 @@
|
|
| 1 |
-
|
| 2 |
-
title: Self Service KB Assistant
|
| 3 |
-
emoji: 🏆
|
| 4 |
-
colorFrom: green
|
| 5 |
-
colorTo: purple
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: 5.49.1
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Self-Service KB Assistant (Free, No API Key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
A lightweight RAG chatbot that answers **only from your Markdown Knowledge Base**, cites sources, asks clarifying questions when uncertain, and offers quick guided intents. Built with **Gradio**, **FAISS**, **sentence-transformers**, and an **extractive QA model**—no paid API needed.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
- Markdown-based KB (`/kb/*.md`)
|
| 7 |
+
- Embeddings: `sentence-transformers/all-MiniLM-L6-v2`
|
| 8 |
+
- Vector search: FAISS (cosine on normalized vectors)
|
| 9 |
+
- Reader: `deepset/roberta-base-squad2` (extractive QA)
|
| 10 |
+
- Citations (title + section)
|
| 11 |
+
- Low-confidence fallback (suggest related articles)
|
| 12 |
+
- Quick intents (buttons for top tasks)
|
| 13 |
+
- One-click “Rebuild Index” admin control
|
| 14 |
+
|
| 15 |
+
## Run locally
|
| 16 |
+
```bash
|
| 17 |
+
python -m venv .venv && source .venv/bin/activate # or .venv\Scripts\activate on Windows
|
| 18 |
+
pip install -r requirements.txt
|
| 19 |
+
python app.py
|
app.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import json
|
| 4 |
+
import time
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import List, Dict, Tuple
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import faiss
|
| 10 |
+
|
| 11 |
+
import gradio as gr
|
| 12 |
+
from transformers import pipeline, AutoTokenizer, AutoModelForQuestionAnswering
|
| 13 |
+
from sentence_transformers import SentenceTransformer
|
| 14 |
+
|
| 15 |
+
KB_DIR = Path("./kb")
|
| 16 |
+
INDEX_DIR = Path("./.index")
|
| 17 |
+
INDEX_DIR.mkdir(exist_ok=True, parents=True)
|
| 18 |
+
|
| 19 |
+
EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
|
| 20 |
+
READER_MODEL_NAME = "deepset/roberta-base-squad2"
|
| 21 |
+
|
| 22 |
+
EMBEDDINGS_PATH = INDEX_DIR / "kb_embeddings.npy"
|
| 23 |
+
METADATA_PATH = INDEX_DIR / "kb_metadata.json"
|
| 24 |
+
FAISS_PATH = INDEX_DIR / "kb_faiss.index"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ---------------------------
|
| 28 |
+
# Utilities: Markdown loading
|
| 29 |
+
# ---------------------------
|
| 30 |
+
|
| 31 |
+
HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$", re.MULTILINE)
|
| 32 |
+
|
| 33 |
+
def read_markdown_files(kb_dir: Path) -> List[Dict]:
|
| 34 |
+
docs = []
|
| 35 |
+
for md_path in sorted(kb_dir.glob("*.md")):
|
| 36 |
+
text = md_path.read_text(encoding="utf-8", errors="ignore")
|
| 37 |
+
title = md_path.stem.replace("_", " ").title()
|
| 38 |
+
# Try first H1 as title if present
|
| 39 |
+
m = re.search(r"^#\s+(.*)$", text, flags=re.MULTILINE)
|
| 40 |
+
if m:
|
| 41 |
+
title = m.group(1).strip()
|
| 42 |
+
|
| 43 |
+
docs.append({
|
| 44 |
+
"filepath": str(md_path),
|
| 45 |
+
"filename": md_path.name,
|
| 46 |
+
"title": title,
|
| 47 |
+
"text": text
|
| 48 |
+
})
|
| 49 |
+
return docs
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def chunk_markdown(doc: Dict, chunk_chars: int = 1200, overlap: int = 150) -> List[Dict]:
|
| 53 |
+
"""
|
| 54 |
+
Simple header-aware chunking: split by H2/H3 when possible and then by char length.
|
| 55 |
+
Stores anchor-ish metadata for basic citations.
|
| 56 |
+
"""
|
| 57 |
+
text = doc["text"]
|
| 58 |
+
# Split by H2/H3 as sections (fallback to entire text)
|
| 59 |
+
sections = re.split(r"(?=^##\s+|\n##\s+|\n###\s+|^###\s+)", text, flags=re.MULTILINE)
|
| 60 |
+
if len(sections) == 1:
|
| 61 |
+
sections = [text]
|
| 62 |
+
|
| 63 |
+
chunks = []
|
| 64 |
+
for sec in sections:
|
| 65 |
+
sec = sec.strip()
|
| 66 |
+
if not sec:
|
| 67 |
+
continue
|
| 68 |
+
|
| 69 |
+
# Derive a section heading for citation
|
| 70 |
+
heading_match = HEADING_RE.search(sec)
|
| 71 |
+
section_heading = heading_match.group(2).strip() if heading_match else doc["title"]
|
| 72 |
+
|
| 73 |
+
# Hard wrap into chunks
|
| 74 |
+
start = 0
|
| 75 |
+
while start < len(sec):
|
| 76 |
+
end = min(start + chunk_chars, len(sec))
|
| 77 |
+
chunk_text = sec[start:end].strip()
|
| 78 |
+
if chunk_text:
|
| 79 |
+
chunks.append({
|
| 80 |
+
"doc_title": doc["title"],
|
| 81 |
+
"filename": doc["filename"],
|
| 82 |
+
"filepath": doc["filepath"],
|
| 83 |
+
"section": section_heading,
|
| 84 |
+
"content": chunk_text
|
| 85 |
+
})
|
| 86 |
+
start = end - overlap if end - overlap > 0 else end
|
| 87 |
+
if start < 0:
|
| 88 |
+
start = 0
|
| 89 |
+
if end == len(sec):
|
| 90 |
+
break
|
| 91 |
+
|
| 92 |
+
return chunks
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# ---------------------------
|
| 96 |
+
# Build / Load Index
|
| 97 |
+
# ---------------------------
|
| 98 |
+
|
| 99 |
+
class KBIndex:
|
| 100 |
+
def __init__(self):
|
| 101 |
+
self.embedder = SentenceTransformer(EMBEDDING_MODEL_NAME)
|
| 102 |
+
self.reader_tokenizer = AutoTokenizer.from_pretrained(READER_MODEL_NAME)
|
| 103 |
+
self.reader_model = AutoModelForQuestionAnswering.from_pretrained(READER_MODEL_NAME)
|
| 104 |
+
self.reader = pipeline("question-answering", model=self.reader_model, tokenizer=self.reader_tokenizer)
|
| 105 |
+
|
| 106 |
+
self.index = None # FAISS index
|
| 107 |
+
self.embeddings = None # numpy array
|
| 108 |
+
self.metadata = [] # list of dicts
|
| 109 |
+
|
| 110 |
+
def build(self, kb_dir: Path):
|
| 111 |
+
docs = read_markdown_files(kb_dir)
|
| 112 |
+
if not docs:
|
| 113 |
+
raise RuntimeError(f"No markdown files found in {kb_dir.resolve()}. Please add *.md files.")
|
| 114 |
+
|
| 115 |
+
# Produce chunks
|
| 116 |
+
all_chunks = []
|
| 117 |
+
for d in docs:
|
| 118 |
+
all_chunks.extend(chunk_markdown(d))
|
| 119 |
+
|
| 120 |
+
texts = [c["content"] for c in all_chunks]
|
| 121 |
+
if not texts:
|
| 122 |
+
raise RuntimeError("No content chunks generated from KB.")
|
| 123 |
+
|
| 124 |
+
embeddings = self.embedder.encode(texts, batch_size=32, convert_to_numpy=True, show_progress_bar=False)
|
| 125 |
+
# Normalize for cosine similarity
|
| 126 |
+
faiss.normalize_L2(embeddings)
|
| 127 |
+
|
| 128 |
+
# Build FAISS index (cosine via inner product on normalized vectors)
|
| 129 |
+
dim = embeddings.shape[1]
|
| 130 |
+
index = faiss.IndexFlatIP(dim)
|
| 131 |
+
index.add(embeddings)
|
| 132 |
+
|
| 133 |
+
self.index = index
|
| 134 |
+
self.embeddings = embeddings
|
| 135 |
+
self.metadata = all_chunks
|
| 136 |
+
|
| 137 |
+
# Persist to disk
|
| 138 |
+
np.save(EMBEDDINGS_PATH, embeddings)
|
| 139 |
+
with open(METADATA_PATH, "w", encoding="utf-8") as f:
|
| 140 |
+
json.dump(self.metadata, f, ensure_ascii=False, indent=2)
|
| 141 |
+
faiss.write_index(index, str(FAISS_PATH))
|
| 142 |
+
|
| 143 |
+
def load(self):
|
| 144 |
+
if not (EMBEDDINGS_PATH.exists() and METADATA_PATH.exists() and FAISS_PATH.exists()):
|
| 145 |
+
return False
|
| 146 |
+
|
| 147 |
+
self.embeddings = np.load(EMBEDDINGS_PATH)
|
| 148 |
+
with open(METADATA_PATH, "r", encoding="utf-8") as f:
|
| 149 |
+
self.metadata = json.load(f)
|
| 150 |
+
self.index = faiss.read_index(str(FAISS_PATH))
|
| 151 |
+
return True
|
| 152 |
+
|
| 153 |
+
def rebuild_if_kb_changed(self):
|
| 154 |
+
"""
|
| 155 |
+
Very light heuristic: if index older than newest kb file, rebuild.
|
| 156 |
+
"""
|
| 157 |
+
kb_mtime = max([p.stat().st_mtime for p in KB_DIR.glob("*.md")] or [0])
|
| 158 |
+
idx_mtime = min([
|
| 159 |
+
EMBEDDINGS_PATH.stat().st_mtime if EMBEDDINGS_PATH.exists() else 0,
|
| 160 |
+
METADATA_PATH.stat().st_mtime if METADATA_PATH.exists() else 0,
|
| 161 |
+
FAISS_PATH.stat().st_mtime if FAISS_PATH.exists() else 0,
|
| 162 |
+
])
|
| 163 |
+
if kb_mtime > idx_mtime:
|
| 164 |
+
self.build(KB_DIR)
|
| 165 |
+
|
| 166 |
+
def retrieve(self, query: str, top_k: int = 4) -> List[Tuple[int, float]]:
|
| 167 |
+
q_emb = self.embedder.encode([query], convert_to_numpy=True)
|
| 168 |
+
faiss.normalize_L2(q_emb)
|
| 169 |
+
D, I = self.index.search(q_emb, top_k)
|
| 170 |
+
indices = I[0].tolist()
|
| 171 |
+
sims = D[0].tolist()
|
| 172 |
+
return list(zip(indices, sims))
|
| 173 |
+
|
| 174 |
+
def answer(self, question: str, retrieved: List[Tuple[int, float]]):
|
| 175 |
+
"""
|
| 176 |
+
Use extractive QA across the top retrieved chunks; pick the best span by score.
|
| 177 |
+
Return (answer_text, best_score, citations)
|
| 178 |
+
"""
|
| 179 |
+
best = {"text": None, "score": -1e9, "meta": None, "ctx": None, "sim": 0.0}
|
| 180 |
+
for idx, sim in retrieved:
|
| 181 |
+
meta = self.metadata[idx]
|
| 182 |
+
context = meta["content"]
|
| 183 |
+
try:
|
| 184 |
+
out = self.reader(question=question, context=context)
|
| 185 |
+
except Exception:
|
| 186 |
+
continue
|
| 187 |
+
score = float(out.get("score", 0.0))
|
| 188 |
+
if score > best["score"]:
|
| 189 |
+
best = {
|
| 190 |
+
"text": out.get("answer", "").strip(),
|
| 191 |
+
"score": score,
|
| 192 |
+
"meta": meta,
|
| 193 |
+
"ctx": context,
|
| 194 |
+
"sim": float(sim)
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
if not best["text"]:
|
| 198 |
+
return None, 0.0, []
|
| 199 |
+
|
| 200 |
+
# Build citations: top 2 sources from retrieved
|
| 201 |
+
citations = []
|
| 202 |
+
seen = set()
|
| 203 |
+
for idx, sim in retrieved[:2]:
|
| 204 |
+
meta = self.metadata[idx]
|
| 205 |
+
key = (meta["filename"], meta["section"])
|
| 206 |
+
if key in seen:
|
| 207 |
+
continue
|
| 208 |
+
seen.add(key)
|
| 209 |
+
citations.append({
|
| 210 |
+
"title": meta["doc_title"],
|
| 211 |
+
"filename": meta["filename"],
|
| 212 |
+
"section": meta["section"]
|
| 213 |
+
})
|
| 214 |
+
return best["text"], best["score"], citations
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
kb = KBIndex()
|
| 218 |
+
|
| 219 |
+
def ensure_index():
|
| 220 |
+
if not kb.load():
|
| 221 |
+
kb.build(KB_DIR)
|
| 222 |
+
else:
|
| 223 |
+
kb.rebuild_if_kb_changed()
|
| 224 |
+
|
| 225 |
+
ensure_index()
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
# ---------------------------
|
| 229 |
+
# Clarify / Guardrails logic
|
| 230 |
+
# ---------------------------
|
| 231 |
+
|
| 232 |
+
def format_citations(citations: List[Dict]) -> str:
|
| 233 |
+
if not citations:
|
| 234 |
+
return ""
|
| 235 |
+
lines = []
|
| 236 |
+
for c in citations:
|
| 237 |
+
lines.append(f"• **{c['title']}** — _{c['section']}_ (`{c['filename']}`)")
|
| 238 |
+
return "\n".join(lines)
|
| 239 |
+
|
| 240 |
+
LOW_CONF_THRESHOLD = 0.20 # reader score heuristic (0–1)
|
| 241 |
+
LOW_SIM_THRESHOLD = 0.30 # retriever sim heuristic (cosine/IP on normalized vectors)
|
| 242 |
+
|
| 243 |
+
HELPFUL_SUGGESTIONS = [
|
| 244 |
+
("Connect WhatsApp", "How do I connect my WhatsApp number?"),
|
| 245 |
+
("Reset Password", "I can't sign in / forgot my password"),
|
| 246 |
+
("First Automation", "How do I create my first automation?"),
|
| 247 |
+
("Billing & Invoices", "How do I download invoices for billing?"),
|
| 248 |
+
("Fix Instagram Connect", "Why can't I connect Instagram?")
|
| 249 |
+
]
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def respond(user_msg, history):
|
| 253 |
+
user_msg = (user_msg or "").strip()
|
| 254 |
+
if not user_msg:
|
| 255 |
+
return "How can I help? Try: **Connect WhatsApp** or **Reset password**."
|
| 256 |
+
|
| 257 |
+
# Retrieve
|
| 258 |
+
retrieved = kb.retrieve(user_msg, top_k=4)
|
| 259 |
+
if not retrieved:
|
| 260 |
+
return "I couldn't find anything yet. Try rephrasing or pick a quick action below."
|
| 261 |
+
|
| 262 |
+
# Answer
|
| 263 |
+
span, score, citations = kb.answer(user_msg, retrieved)
|
| 264 |
+
|
| 265 |
+
# If no span, surface top articles as fallback
|
| 266 |
+
if not span:
|
| 267 |
+
suggestions = "\n".join([f"- {c['title']} — _{c['section']}_" for c in citations]) or "- Try a different query."
|
| 268 |
+
return f"I’m not fully sure. Here are the closest matches:\n\n{suggestions}"
|
| 269 |
+
|
| 270 |
+
# Confidence heuristics
|
| 271 |
+
best_sim = max(_
|
kb/billing_invoices.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Billing & Invoices
|
| 2 |
+
## Quick Answer
|
| 3 |
+
Download invoices in **Settings > Billing > Invoices**.
|
| 4 |
+
## Notes
|
| 5 |
+
- The assistant cannot access or change billing data.
|
| 6 |
+
- For refund or plan changes, contact support.
|
kb/build_first_automation.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Build Your First Automation
|
| 2 |
+
## Overview
|
| 3 |
+
Automations run actions when a trigger condition is met.
|
| 4 |
+
## Steps
|
| 5 |
+
1. Go to **Automation > Flows**.
|
| 6 |
+
2. Click **Create Flow**.
|
| 7 |
+
3. Choose a template or start blank.
|
| 8 |
+
4. Add a trigger (e.g., message received).
|
| 9 |
+
5. Add an action (e.g., send reply).
|
| 10 |
+
6. **Save** and **Enable**.
|
| 11 |
+
## Tips
|
| 12 |
+
- Start simple; add conditions later.
|
| 13 |
+
- Test with a sample event first.
|
| 14 |
+
## Related
|
| 15 |
+
- Connect WhatsApp
|
| 16 |
+
- Troubleshooting
|
kb/connect_whatsapp.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# How to Connect WhatsApp
|
| 2 |
+
## Objectives
|
| 3 |
+
Connect a WhatsApp number to enable messaging flows.
|
| 4 |
+
## Prerequisites
|
| 5 |
+
- Admin access
|
| 6 |
+
- WhatsApp Business number
|
| 7 |
+
## Steps
|
| 8 |
+
1. Go to **Settings > Channels > WhatsApp**.
|
| 9 |
+
2. Click **Connect Number**.
|
| 10 |
+
3. Follow the provider flow and grant all permissions.
|
| 11 |
+
4. Confirm the number shows **Active**.
|
| 12 |
+
## Common Pitfalls
|
| 13 |
+
- Business verification pending
|
| 14 |
+
- Missing permissions
|
| 15 |
+
- Number linked elsewhere
|
| 16 |
+
## Related
|
| 17 |
+
- Troubleshoot Instagram Connect
|
| 18 |
+
- Build First Automation
|
kb/get_started.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Get Started
|
| 2 |
+
## Objectives
|
| 3 |
+
Create your account, sign in, and explore the dashboard.
|
| 4 |
+
## Prerequisites
|
| 5 |
+
An email address and internet connection.
|
| 6 |
+
## Steps
|
| 7 |
+
1. Open the **Dashboard**.
|
| 8 |
+
2. Select **Create Account**.
|
| 9 |
+
3. Verify your email.
|
| 10 |
+
4. Sign in and review **Dashboard > Overview**.
|
| 11 |
+
## Related
|
| 12 |
+
- Reset Password
|
| 13 |
+
- Build First Automation
|
| 14 |
+
|
kb/reset_password.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Reset Password / Sign-In Issues
|
| 2 |
+
## Quick Answer
|
| 3 |
+
Use **Forgot Password** on the sign-in screen, then check your email (and spam).
|
| 4 |
+
## Details
|
| 5 |
+
- Reset links expire after 15 minutes.
|
| 6 |
+
- Multiple requests: the newest link works.
|
| 7 |
+
## Related
|
| 8 |
+
- Get Started
|
kb/troubleshoot_instagram_connect.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Troubleshoot Instagram Connect
|
| 2 |
+
## Symptoms
|
| 3 |
+
- “Unable to connect account”
|
| 4 |
+
- “Permissions missing”
|
| 5 |
+
## Possible Causes
|
| 6 |
+
- Business account not linked to the Page
|
| 7 |
+
- Permissions not granted during connect flow
|
| 8 |
+
## Fix
|
| 9 |
+
1. In **Facebook Business Settings**, link the Instagram account to the Page.
|
| 10 |
+
2. Re-run the connect flow and grant all requested permissions.
|
| 11 |
+
3. Check **Settings > Channels > Instagram** shows **Connected**.
|
| 12 |
+
## Related
|
| 13 |
+
- Connect WhatsApp
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.44.0
|
| 2 |
+
transformers>=4.44.0
|
| 3 |
+
sentence-transformers>=3.0.0
|
| 4 |
+
faiss-cpu>=1.8.0
|
| 5 |
+
torch>=2.2.0
|
| 6 |
+
numpy>=1.26
|