Suryasticsai commited on
Commit
62b9025
·
verified ·
1 Parent(s): afcd5d6

Uploaded new Version of AgileAdvisor v0.5

Browse files

# feat: AgileAdvisor v0.5 — Persistent Gist Sync, ScrumLens RAG, Auto-Tags, Duplicate Detection, Role Hierarchy & Quick-Feed

## Summary
This PR ships **AgileAdvisor v0.5**, a major feature drop that turns the static knowledge base into a living, self-improving RAG system. Every feed now auto-persists back to GitHub Gist, facts carry smart category tags, and a new **ScrumLens** mode lets users audit raw meeting transcripts against the vector memory.

---

## ✨ What's New

| Feature | Description |
|---|---|
| **🔄 Persistent Gist Sync** | Every feed—bulk or single-click—now appends to the GitHub Gist API and immediately hot-reloads the ChromaDB collection. The Gist is the single source of truth; local vector DB is just a fast mirror. |
| **🔍 ScrumLens Native RAG** | New **"ScrumLens 🔍"** tab. Paste any meeting transcript (Daily Standup, Retro, Planning) and the advisor cross-checks it against the KB, surfaces anti-patterns, assigns per-role action items, and gives a health score banner (🟢🟡🔴). |
| **⚠️ Duplicate Detection** | Live duplicate guard before you save. Uses exact match + `difflib.SequenceMatcher` (>88% similarity) + substring containment. The Feed tab now has a **"Preview Tags & Duplicates"** button that renders inline warnings. |
| **🏗️ Role Hierarchy Diagram** | New **"Role Hierarchy 🏗️"** tab. A pure HTML/CSS vertical flow diagram showing Stakeholders → PO → SM → Dev Team → RTE with color-coded borders and assigned output badges (e.g., Product Backlog, Impediment Removal, Potentially Shippable Increment). |
| **🏷️ Feed Data Auto-Tags** | Facts are auto-labelled on ingest via keyword heuristics (`ceremony`, `role`, `artifact`, `metric`, `anti-pattern`, `gamification`, `general`). Tags are embedded in Gist lines as `[ceremony] fact...` and stored in ChromaDB metadata for richer retrieval context. |
| **📦 Smart Quick-Feed Card** | After every answer in the **Ask** tab, a compact card appears showing the top retrieved fact + its tags. One **"➕ Feed This Fact to Dataset"** button instantly appends it to Gist without re-typing. |

---

## 🏗️ Technical Changes

- **`KnowledgeBase.auto_tag()`** — zero-token keyword heuristics for categorization.
- **`KnowledgeBase.check_duplicate()`** — exact / similar / contained tri-check.
- **`KnowledgeBase._parse_line()` / `_format_line()`** — round-trip serialization of `[tag] content` format in Gist.
- **`KnowledgeBase._append_lines_to_gist()`** — unified write path used by both bulk feed and quick feed.
- **`AgileAdvisor.analyze_transcript()`** — transcript audit pipeline with expanded context window (`n_results=3`).
- **ChromaDB metadata** — now stores `tags`, `hash`, and `source` per document.
- **Gradio UI** — 5 tabs: Ask, ScrumLens, Feed Dataset, Role Hierarchy, About.

---

## 🔄 Backwards Compatibility

- **Zero breaking changes.** Existing Gist lines without a `[tag]` prefix are gracefully parsed as `general`.
- The ChromaDB collection version metadata is bumped to `2.0` but re-initializes automatically on first run.

---

## 🧪 How to Test

1. **Ask a question** → verify the response still renders the Dataset Card + explanation + quote.
2. **Check Quick-Feed** → after the answer, click **"➕ Feed This Fact to Dataset"** and confirm the Gist grows by one tagged line.
3. **Open ScrumLens** → paste a fake Daily Standup transcript → verify you get a health score + red flags + role actions.
4. **Feed Dataset** → type two lines, one duplicate of an existing fact → click **Preview** → confirm the duplicate is flagged in red. Then click **Feed** and confirm Gist sync message.
5. **Role Hierarchy** → open the tab → verify the vertical flow diagram renders with correct role colors and output badges.
6. **Restart the Space** → confirm the KB reloads from Gist with tags intact (check the stats bar document count).

---

## 📁 Files Added

- `AgileAdvisor-v0.5.py`

---

## ✅ PR Checklist

- [x] Code runs locally without errors
- [x] Gist read/write tested with live token
- [x] ChromaDB persistent storage verified across restarts
- [x] No hardcoded secrets or API keys exposed
- [x] HTML output sanitized via `sanitize_html()`
- [x] Backwards compatible with untagged legacy Gist data

---

**Ready for review - `app.py`.** 🚀

Files changed (1) hide show
  1. AgileAdvisor-v0.5.py +1312 -0
AgileAdvisor-v0.5.py ADDED
@@ -0,0 +1,1312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import chromadb
3
+ from chromadb.utils import embedding_functions
4
+ from groq import Groq
5
+ import os
6
+ import requests
7
+ import json
8
+ import hashlib
9
+ import logging
10
+ from typing import List, Optional, Tuple
11
+ from dataclasses import dataclass, field
12
+ import re
13
+ import difflib
14
+
15
+ # =========================
16
+ # LOGGING SETUP (HF Spaces compatible)
17
+ # =========================
18
+ logging.basicConfig(
19
+ level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
20
+ )
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # =========================
24
+ # CONFIGURATION
25
+ # =========================
26
+
27
+
28
+ @dataclass
29
+ class Config:
30
+ """Centralized configuration - optimized for Hugging Face Spaces"""
31
+
32
+ # GitHub Gist Config
33
+ GITHUB_USERNAME: str = "suryasticsai"
34
+ GIST_ID: str = "c25ec2898e91a2ce09ab74930741907e"
35
+ GIST_FILENAME: str = "agile-knowledge-base.txt"
36
+
37
+ # ChromaDB Config
38
+ COLLECTION_NAME: str = "agile_knowledge"
39
+ CHROMA_PERSIST_DIR: str = "./chroma_db"
40
+
41
+ # Limits
42
+ MAX_QUERY_LENGTH: int = 500
43
+ MAX_FEED_LENGTH: int = 10000
44
+ DEFAULT_N_RESULTS: int = 1
45
+
46
+ # Groq Config
47
+ GROQ_MODEL: str = "llama-3.1-8b-instant"
48
+ GROQ_TEMPERATURE: float = 0.4
49
+ GROQ_MAX_TOKENS: int = 1000
50
+
51
+ # API Timeouts
52
+ REQUEST_TIMEOUT: int = 10
53
+ GROQ_TIMEOUT: float = 60.0
54
+
55
+ # Auto-tagging categories
56
+ CATEGORIES: Tuple[str, ...] = (
57
+ "ceremony",
58
+ "role",
59
+ "artifact",
60
+ "metric",
61
+ "anti-pattern",
62
+ "gamification",
63
+ "general",
64
+ )
65
+
66
+ @property
67
+ def gist_url(self) -> str:
68
+ return f"https://gist.githubusercontent.com/{self.GITHUB_USERNAME}/{self.GIST_ID}/raw/{self.GIST_FILENAME}"
69
+
70
+ @property
71
+ def gist_api_url(self) -> str:
72
+ return f"https://api.github.com/gists/{self.GIST_ID}"
73
+
74
+
75
+ CONFIG = Config()
76
+
77
+ # =========================
78
+ # KNOWLEDGE BASE MANAGER
79
+ # =========================
80
+
81
+
82
+ class KnowledgeBase:
83
+ """
84
+ Encapsulates ChromaDB + Gist operations.
85
+ Optimized for Hugging Face Spaces with persistent storage.
86
+ """
87
+
88
+ def __init__(self):
89
+ self.config = CONFIG
90
+
91
+ # Initialize ChromaDB with persistent storage
92
+ self.client = chromadb.PersistentClient(path=self.config.CHROMA_PERSIST_DIR)
93
+ self.emb = embedding_functions.DefaultEmbeddingFunction()
94
+ self.collection = self._init_collection()
95
+
96
+ # Initialize Groq client with proper timeout
97
+ groq_key = os.environ.get("GROQ_API_KEY")
98
+ if not groq_key:
99
+ logger.error("GROQ_API_KEY not found in environment/secrets!")
100
+ self.groq_client = None
101
+ else:
102
+ self.groq_client = Groq(
103
+ api_key=groq_key, timeout=self.config.GROQ_TIMEOUT, max_retries=2
104
+ )
105
+ logger.info("Groq client initialized")
106
+
107
+ # Load data from Gist or defaults
108
+ self._load_data()
109
+
110
+ def _init_collection(self):
111
+ """Get or create collection safely"""
112
+ try:
113
+ collection = self.client.get_or_create_collection(
114
+ name=self.config.COLLECTION_NAME,
115
+ embedding_function=self.emb,
116
+ metadata={
117
+ "description": "Agile knowledge base for RAG",
118
+ "version": "2.0",
119
+ "source": "github_gist",
120
+ },
121
+ )
122
+ count = collection.count()
123
+ logger.info(f"Collection ready with {count} documents")
124
+ return collection
125
+ except Exception as e:
126
+ logger.error(f"Failed to initialize collection: {e}")
127
+ raise RuntimeError(f"ChromaDB init failed: {e}")
128
+
129
+ # --- Tagging & Parsing Helpers ---
130
+
131
+ def auto_tag(self, text: str) -> List[str]:
132
+ """Keyword-based auto-tagging without burning LLM tokens."""
133
+ text_lower = text.lower()
134
+ tags = set()
135
+ keyword_map = {
136
+ "ceremony": [
137
+ "sprint planning",
138
+ "daily standup",
139
+ "daily scrum",
140
+ "sprint review",
141
+ "sprint retrospective",
142
+ "retrospective",
143
+ "refinement",
144
+ "backlog grooming",
145
+ "review",
146
+ "planning",
147
+ ],
148
+ "role": [
149
+ "product owner",
150
+ "scrum master",
151
+ "dev team",
152
+ "developer",
153
+ "development team",
154
+ "stakeholder",
155
+ "rte",
156
+ "agile coach",
157
+ "scrum team",
158
+ ],
159
+ "artifact": [
160
+ "product backlog",
161
+ "sprint backlog",
162
+ "increment",
163
+ "definition of done",
164
+ "definition of ready",
165
+ "burndown",
166
+ "velocity",
167
+ "user story",
168
+ "epic",
169
+ ],
170
+ "metric": [
171
+ "velocity",
172
+ "burndown",
173
+ "lead time",
174
+ "cycle time",
175
+ "cfd",
176
+ "cumulative flow",
177
+ "throughput",
178
+ "wip",
179
+ ],
180
+ "anti-pattern": [
181
+ "anti-pattern",
182
+ "mistake",
183
+ "wrong",
184
+ "fail",
185
+ "smell",
186
+ "chaos",
187
+ "bad practice",
188
+ "should not",
189
+ "never",
190
+ ],
191
+ "gamification": [
192
+ "game",
193
+ "points",
194
+ "badge",
195
+ "leaderboard",
196
+ "reward",
197
+ "fun",
198
+ "motivation",
199
+ "kudos",
200
+ "competition",
201
+ ],
202
+ }
203
+ for tag, keywords in keyword_map.items():
204
+ if any(kw in text_lower for kw in keywords):
205
+ tags.add(tag)
206
+ if not tags:
207
+ tags.add("general")
208
+ return sorted(list(tags))
209
+
210
+ def _parse_line(self, line: str) -> Tuple[str, List[str]]:
211
+ """Parse a Gist line: [tag1,tag2] content..."""
212
+ line = line.strip()
213
+ if not line:
214
+ return "", []
215
+ m = re.match(r"^\[([a-zA-Z0-9_, -]+)\]\s*(.+)", line)
216
+ if m:
217
+ tags = [t.strip().lower() for t in m.group(1).split(",")]
218
+ return m.group(2).strip(), tags
219
+ return line, ["general"]
220
+
221
+ def _format_line(self, content: str, tags: List[str]) -> str:
222
+ """Serialize a fact with its primary tag(s) for Gist persistence."""
223
+ primary = [t for t in tags if t != "general"]
224
+ if primary:
225
+ return f"[{','.join(primary)}] {content}"
226
+ return content
227
+
228
+ def check_duplicate(
229
+ self, text: str
230
+ ) -> Tuple[bool, Optional[str], str]:
231
+ """Check if a fact already exists (exact, similar, or contained)."""
232
+ text_clean = text.strip().lower()
233
+ if not text_clean:
234
+ return False, None, ""
235
+ try:
236
+ existing = self.collection.get(include=["documents"])
237
+ docs = existing.get("documents", []) if existing else []
238
+ for doc in docs:
239
+ if not doc:
240
+ continue
241
+ doc_clean = doc.strip().lower()
242
+ if text_clean == doc_clean:
243
+ return True, doc, "exact"
244
+ ratio = difflib.SequenceMatcher(None, text_clean, doc_clean).ratio()
245
+ if ratio > 0.88:
246
+ return True, doc, "similar"
247
+ if len(text_clean) > 25 and (
248
+ text_clean in doc_clean or doc_clean in text_clean
249
+ ):
250
+ return True, doc, "contained"
251
+ return False, None, ""
252
+ except Exception as e:
253
+ logger.error(f"Duplicate check failed: {e}")
254
+ return False, None, ""
255
+
256
+ def preview_feed(self, new_text: str) -> str:
257
+ """Live preview of tags & duplicates before feeding."""
258
+ if not new_text.strip():
259
+ return "<div style='color:#64748b;padding:12px;'>Type something to preview tags & duplicates...</div>"
260
+ lines = [l.strip() for l in new_text.split("\n") if l.strip()]
261
+ parts = []
262
+ for line in lines:
263
+ tags = self.auto_tag(line)
264
+ is_dup, matched, match_type = self.check_duplicate(line)
265
+ tag_badges = "".join(
266
+ [
267
+ f'<span style="background:#1e293b;color:#00d4aa;padding:2px 8px;border-radius:4px;font-size:0.7rem;margin-right:4px;">{t}</span>'
268
+ for t in tags
269
+ ]
270
+ )
271
+ dup_badge = ""
272
+ if is_dup:
273
+ dup_badge = f'<span style="background:rgba(239,68,68,0.15);color:#ef4444;padding:2px 8px;border-radius:4px;font-size:0.7rem;">⚠️ {match_type} duplicate</span>'
274
+ parts.append(
275
+ f"""
276
+ <div style="border-bottom:1px solid #1e293b;padding:8px 0;">
277
+ <div style="color:#cbd5e1;font-size:0.85rem;margin-bottom:4px;">{line}</div>
278
+ <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
279
+ {tag_badges} {dup_badge}
280
+ </div>
281
+ </div>
282
+ """
283
+ )
284
+ return f'<div style="background:#0f172a;border:1px solid #1e293b;border-radius:8px;padding:12px;">{"".join(parts)}</div>'
285
+
286
+ # --- Loading ---
287
+
288
+ def _load_data(self):
289
+ """Load from Gist, fallback to defaults."""
290
+ try:
291
+ response = requests.get(
292
+ self.config.gist_url, timeout=self.config.REQUEST_TIMEOUT
293
+ )
294
+ response.raise_for_status()
295
+
296
+ lines = [line.strip() for line in response.text.split("\n") if line.strip()]
297
+ documents, tags_list = [], []
298
+ for line in lines:
299
+ content, tags = self._parse_line(line)
300
+ if content:
301
+ documents.append(content)
302
+ tags_list.append(tags)
303
+
304
+ if documents:
305
+ self._update_collection(documents, tags_list, replace=True)
306
+ logger.info(f"Loaded {len(documents)} documents from Gist")
307
+ else:
308
+ logger.warning("Gist empty, loading defaults")
309
+ self._load_defaults()
310
+
311
+ except Exception as e:
312
+ logger.warning(f"Could not load from Gist: {e}")
313
+ self._load_defaults()
314
+
315
+ def _load_defaults(self):
316
+ """Fallback Agile knowledge with tags."""
317
+ defaults = [
318
+ ("Sprint Planning is where the Scrum Team selects work from the Product Backlog for the upcoming Sprint", ["ceremony"]),
319
+ ("Daily Standup is a 15-minute time-boxed event for the Development Team to synchronize activities and plan for the next 24 hours", ["ceremony"]),
320
+ ("Sprint Review is held at the end of the Sprint to inspect the Increment and adapt the Product Backlog if needed", ["ceremony"]),
321
+ ("Sprint Retrospective is an opportunity for the Scrum Team to inspect itself and create a plan for improvements for the next Sprint", ["ceremony"]),
322
+ ("Product Owner is responsible for maximizing the value of the product resulting from work of the Development Team", ["role"]),
323
+ ("Scrum Master is responsible for promoting and supporting Scrum by helping everyone understand Scrum theory, practice, rules, and values", ["role"]),
324
+ ("Definition of Done is a shared understanding of what it means for work to be complete, ensuring transparency and quality", ["artifact"]),
325
+ ("Velocity is a measure of the amount of work a Team can tackle during a single Sprint, used for planning not performance measurement", ["metric"]),
326
+ ]
327
+ docs = [d[0] for d in defaults]
328
+ tags = [d[1] for d in defaults]
329
+ self._update_collection(docs, tags, replace=True)
330
+ logger.info(f"Loaded {len(defaults)} default documents")
331
+
332
+ def _update_collection(
333
+ self,
334
+ documents: List[str],
335
+ tags_list: Optional[List[List[str]]] = None,
336
+ replace: bool = False,
337
+ ):
338
+ """Safely update ChromaDB collection with metadata tags."""
339
+ try:
340
+ if replace:
341
+ try:
342
+ existing = self.collection.get()
343
+ if existing and existing.get("ids"):
344
+ self.collection.delete(ids=existing["ids"])
345
+ logger.info(f"Cleared {len(existing['ids'])} existing docs")
346
+ except Exception:
347
+ pass
348
+
349
+ if documents:
350
+ metadatas = []
351
+ for i, doc in enumerate(documents):
352
+ tags = (
353
+ tags_list[i]
354
+ if tags_list and i < len(tags_list)
355
+ else ["general"]
356
+ )
357
+ metadatas.append(
358
+ {
359
+ "source": "gist",
360
+ "index": i,
361
+ "tags": ",".join(tags),
362
+ "hash": hashlib.md5(doc.encode()).hexdigest(),
363
+ }
364
+ )
365
+ self.collection.add(
366
+ documents=documents,
367
+ ids=[
368
+ f"doc_{hashlib.md5(doc.encode()).hexdigest()}"
369
+ for doc in documents
370
+ ],
371
+ metadatas=metadatas,
372
+ )
373
+ logger.info(f"Added {len(documents)} documents")
374
+
375
+ except Exception as e:
376
+ logger.error(f"Failed to update collection: {e}")
377
+ raise
378
+
379
+ def query(self, question: str, n_results: int = None) -> Tuple[List[str], str]:
380
+ """Query knowledge base. Returns: (documents_list, formatted_context_string)"""
381
+ if not question or not question.strip():
382
+ return [], "No question provided."
383
+
384
+ n_results = n_results or self.config.DEFAULT_N_RESULTS
385
+
386
+ try:
387
+ available = self.collection.count()
388
+ n_results = min(n_results, available or 1)
389
+
390
+ results = self.collection.query(
391
+ query_texts=[question.strip()],
392
+ n_results=n_results,
393
+ include=["documents", "distances", "metadatas"],
394
+ )
395
+
396
+ documents = results.get("documents")
397
+ if not documents or not isinstance(documents, list) or len(documents) == 0:
398
+ return [], "No matching Agile knowledge found."
399
+
400
+ docs = documents[0]
401
+ if not docs:
402
+ return [], "No relevant documents found."
403
+
404
+ context_parts = []
405
+ distances = (
406
+ results.get("distances", [[]])[0] if results.get("distances") else [0.5] * len(docs)
407
+ )
408
+ metadatas = (
409
+ results.get("metadatas", [[]])[0] if results.get("metadatas") else [{}] * len(docs)
410
+ )
411
+
412
+ for i, (doc, dist, meta) in enumerate(zip(docs, distances, metadatas), 1):
413
+ relevance = "High" if dist < 0.3 else "Medium" if dist < 0.6 else "Low"
414
+ tags = meta.get("tags", "general")
415
+ context_parts.append(
416
+ f"[Match {i}] (Relevance: {relevance} | Tags: {tags})\n{doc}"
417
+ )
418
+
419
+ return docs, "\n\n".join(context_parts)
420
+
421
+ except Exception as e:
422
+ logger.error(f"Query failed: {e}")
423
+ return [], f"Query error: {str(e)}"
424
+
425
+ # --- Gist Persistence ---
426
+
427
+ def _append_lines_to_gist(self, lines: List[str]) -> str:
428
+ """Core Gist append logic used by bulk feed & quick feed."""
429
+ if not lines:
430
+ return "Nothing to append."
431
+ token = os.environ.get("GITHUB_TOKEN")
432
+ if not token:
433
+ return "GITHUB_TOKEN not found! Add it in HF Spaces Settings > Secrets."
434
+
435
+ try:
436
+ headers = {
437
+ "Authorization": f"token {token}",
438
+ "Accept": "application/vnd.github.v3+json",
439
+ }
440
+
441
+ response = requests.get(
442
+ self.config.gist_api_url,
443
+ headers=headers,
444
+ timeout=self.config.REQUEST_TIMEOUT,
445
+ )
446
+ response.raise_for_status()
447
+
448
+ gist_data = response.json()
449
+ current_content = gist_data["files"][self.config.GIST_FILENAME]["content"]
450
+
451
+ existing_lines = set(
452
+ line.strip() for line in current_content.split("\n") if line.strip()
453
+ )
454
+ to_add = [line for line in lines if line.strip() not in existing_lines]
455
+
456
+ if not to_add:
457
+ return "✅ All facts already exist in the dataset. No duplicates added."
458
+
459
+ updated_content = current_content.rstrip() + "\n" + "\n".join(to_add)
460
+
461
+ payload = {
462
+ "files": {self.config.GIST_FILENAME: {"content": updated_content}}
463
+ }
464
+
465
+ update_resp = requests.patch(
466
+ self.config.gist_api_url,
467
+ headers=headers,
468
+ data=json.dumps(payload),
469
+ timeout=self.config.REQUEST_TIMEOUT,
470
+ )
471
+ update_resp.raise_for_status()
472
+
473
+ # Refresh ChromaDB from updated content
474
+ all_lines = [
475
+ line.strip() for line in updated_content.split("\n") if line.strip()
476
+ ]
477
+ documents, tags_list = [], []
478
+ for line in all_lines:
479
+ content, tags = self._parse_line(line)
480
+ if content:
481
+ documents.append(content)
482
+ tags_list.append(tags)
483
+
484
+ self._update_collection(documents, tags_list, replace=True)
485
+
486
+ return f"🚀 Success! Added {len(to_add)} new fact(s). Total: {len(documents)} documents."
487
+
488
+ except requests.RequestException as e:
489
+ logger.error(f"GitHub API error: {e}")
490
+ return f"GitHub API error: {str(e)}"
491
+ except Exception as e:
492
+ logger.error(f"Update failed: {e}")
493
+ return f"Error: {str(e)}"
494
+
495
+ def update_gist(self, new_text: str) -> str:
496
+ """Bulk update from Feed Dataset tab."""
497
+ if not new_text or not new_text.strip():
498
+ return "Empty input! Please provide some Agile knowledge to feed."
499
+ if len(new_text) > self.config.MAX_FEED_LENGTH:
500
+ return f"Too long! ({len(new_text)} chars). Max: {self.config.MAX_FEED_LENGTH}"
501
+
502
+ raw_lines = [l.strip() for l in new_text.split("\n") if l.strip()]
503
+ formatted = []
504
+ for line in raw_lines:
505
+ tags = self.auto_tag(line)
506
+ formatted.append(self._format_line(line, tags))
507
+
508
+ return self._append_lines_to_gist(formatted)
509
+
510
+ def feed_single_fact(self, text: str, tags_str: str) -> str:
511
+ """One-click feed from Ask advisor panel."""
512
+ if not text or not text.strip():
513
+ return "No fact to feed."
514
+ tags = [t.strip() for t in tags_str.split(",") if t.strip()]
515
+ if not tags:
516
+ tags = self.auto_tag(text)
517
+ formatted = self._format_line(text.strip(), tags)
518
+ return self._append_lines_to_gist([formatted])
519
+
520
+ def get_stats(self) -> dict:
521
+ """Get KB stats for UI display."""
522
+ try:
523
+ return {
524
+ "total_documents": self.collection.count(),
525
+ "collection_name": self.config.COLLECTION_NAME,
526
+ "storage": self.config.CHROMA_PERSIST_DIR,
527
+ "groq_ready": self.groq_client is not None,
528
+ }
529
+ except Exception as e:
530
+ return {"error": str(e)}
531
+
532
+
533
+ # =========================
534
+ # HTML SANITIZATION
535
+ # =========================
536
+
537
+
538
+ def sanitize_html(text: str) -> str:
539
+ """Prevent XSS - strip dangerous tags and attributes"""
540
+ if not text:
541
+ return ""
542
+ text = re.sub(
543
+ r"<script[^>]*>.*?</script>", "", text, flags=re.DOTALL | re.IGNORECASE
544
+ )
545
+ text = re.sub(r'\s*on\w+\s*=\s*["\'][^"\']*["\']', "", text, flags=re.IGNORECASE)
546
+ text = re.sub(r"javascript:", "", text, flags=re.IGNORECASE)
547
+ return text
548
+
549
+
550
+ # =========================
551
+ # SYSTEM PROMPTS
552
+ # =========================
553
+
554
+ SYSTEM_PROMPT = """
555
+ You are Scrum Wizard — a sharp, witty, brutally honest Agile Coach with 10 years of SAFe experience.
556
+ You cut through corporate fluff. You celebrate good Agile. You roast bad Agile with humor and love.
557
+
558
+ CRITICAL RULES:
559
+ - You ALWAYS respond in pure HTML only. No markdown. No plain text. No code fences.
560
+ - ALWAYS show the DATASET FACT CARD first before any explanation.
561
+ - If factual data exists in context, show it clearly and accurately.
562
+ - Fill ALL template placeholders below. Never leave placeholders empty.
563
+
564
+ Your response MUST follow this EXACT structure:
565
+
566
+ <div style="font-family:sans-serif;max-width:800px;margin:0 auto;padding:8px">
567
+
568
+ <!-- DATASET CARD -->
569
+ <div style="background:linear-gradient(135deg,#052e16,#064e3b);border:1px solid rgba(16,185,129,0.25);border-left:5px solid #10b981;border-radius:14px;padding:18px 22px;margin-bottom:16px;box-shadow:0 0 0 1px rgba(16,185,129,0.05)">
570
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
571
+ <div style="width:34px;height:34px;border-radius:10px;background:rgba(16,185,129,0.15);display:flex;align-items:center;justify-content:center;font-size:1rem">📦</div>
572
+ <div>
573
+ <div style="font-size:0.72rem;color:#6ee7b7;font-weight:700;letter-spacing:2px;text-transform:uppercase">Data Retrieved From Dataset</div>
574
+ <div style="font-size:0.75rem;color:#86efac;margin-top:2px">Exact matched Agile knowledge retrieved from vector memory</div>
575
+ </div>
576
+ </div>
577
+ <div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.05);padding:16px 18px;border-radius:12px">
578
+ <div style="color:#ecfdf5;font-size:0.96rem;line-height:1.8;font-weight:500">{dataset_match}</div>
579
+ </div>
580
+ </div>
581
+
582
+ <!-- MAIN EXPLANATION CARD -->
583
+ <div style="background:linear-gradient(135deg,#1a1a2e,#16213e);border-left:4px solid #00d4aa;border-radius:12px;padding:20px 24px;margin-bottom:16px">
584
+ <div style="font-size:0.7rem;color:#00d4aa;font-weight:700;letter-spacing:2px;text-transform:uppercase;margin-bottom:8px">⚡ AgileAdvisor Says</div>
585
+ <div style="font-size:1.1rem;font-weight:700;color:#f1f5f9;margin-bottom:8px">{topic_title}</div>
586
+ <div style="color:#94a3b8;font-size:0.9rem;line-height:1.7">{explanation}</div>
587
+ </div>
588
+
589
+ <!-- KEY POINTS -->
590
+ <div style="background:#111827;border:1px solid #1e293b;border-radius:12px;padding:20px 24px;margin-bottom:16px">
591
+ <div style="font-size:0.7rem;color:#8b5cf6;font-weight:700;letter-spacing:2px;text-transform:uppercase;margin-bottom:14px">📖 What This Actually Means</div>
592
+ <div style="display:flex;flex-direction:column;gap:10px">
593
+ <div style="display:flex;gap:12px;align-items:flex-start">
594
+ <div style="background:#8b5cf6;color:#fff;border-radius:6px;padding:4px 10px;font-size:0.75rem;font-weight:700;white-space:nowrap;margin-top:2px">01</div>
595
+ <div style="color:#cbd5e1;font-size:0.88rem;line-height:1.6">{key_point_1}</div>
596
+ </div>
597
+ <div style="display:flex;gap:12px;align-items:flex-start">
598
+ <div style="background:#8b5cf6;color:#fff;border-radius:6px;padding:4px 10px;font-size:0.75rem;font-weight:700;white-space:nowrap;margin-top:2px">02</div>
599
+ <div style="color:#cbd5e1;font-size:0.88rem;line-height:1.6">{key_point_2}</div>
600
+ </div>
601
+ <div style="display:flex;gap:12px;align-items:flex-start">
602
+ <div style="background:#8b5cf6;color:#fff;border-radius:6px;padding:4px 10px;font-size:0.75rem;font-weight:700;white-space:nowrap;margin-top:2px">03</div>
603
+ <div style="color:#cbd5e1;font-size:0.88rem;line-height:1.6">{key_point_3}</div>
604
+ </div>
605
+ </div>
606
+ </div>
607
+
608
+ <!-- PEOPLE FLOW -->
609
+ <div style="background:#111827;border:1px solid #1e293b;border-radius:12px;padding:20px 24px;margin-bottom:16px">
610
+ <div style="font-size:0.7rem;color:#f59e0b;font-weight:700;letter-spacing:2px;text-transform:uppercase;margin-bottom:16px">👥 Who's Involved & In What Order</div>
611
+ <div style="display:flex;flex-direction:column;gap:0">
612
+ <div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:#0c101a;border:1px solid #1e293b;border-radius:10px;margin-bottom:4px">
613
+ <div style="width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#8b5cf6,#7c3aed);display:flex;align-items:center;justify-content:center;font-size:0.85rem;flex-shrink:0">🎯</div>
614
+ <div>
615
+ <div style="font-weight:700;font-size:0.88rem;color:#f1f5f9">{role_1_name}</div>
616
+ <div style="font-size:0.78rem;color:#64748b;margin-top:2px">{role_1_action}</div>
617
+ </div>
618
+ <div style="margin-left:auto;padding:3px 10px;border-radius:20px;font-size:0.7rem;font-weight:600;background:rgba(139,92,246,0.15);color:#8b5cf6;border:1px solid rgba(139,92,246,0.3)">Step 1</div>
619
+ </div>
620
+ <div style="text-align:center;color:#334155;font-size:1.2rem;margin:2px 0">↓</div>
621
+ <div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:#0c101a;border:1px solid #1e293b;border-radius:10px;margin-bottom:4px">
622
+ <div style="width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#00d4aa,#059669);display:flex;align-items:center;justify-content:center;font-size:0.85rem;flex-shrink:0">🧭</div>
623
+ <div>
624
+ <div style="font-weight:700;font-size:0.88rem;color:#f1f5f9">{role_2_name}</div>
625
+ <div style="font-size:0.78rem;color:#64748b;margin-top:2px">{role_2_action}</div>
626
+ </div>
627
+ <div style="margin-left:auto;padding:3px 10px;border-radius:20px;font-size:0.7rem;font-weight:600;background:rgba(0,212,170,0.15);color:#00d4aa;border:1px solid rgba(0,212,170,0.3)">Step 2</div>
628
+ </div>
629
+ <div style="text-align:center;color:#334155;font-size:1.2rem;margin:2px 0">↓</div>
630
+ <div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:#0c101a;border:1px solid #1e293b;border-radius:10px;margin-bottom:4px">
631
+ <div style="width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#3b82f6,#1d4ed8);display:flex;align-items:center;justify-content:center;font-size:0.85rem;flex-shrink:0">💻</div>
632
+ <div>
633
+ <div style="font-weight:700;font-size:0.88rem;color:#f1f5f9">{role_3_name}</div>
634
+ <div style="font-size:0.78rem;color:#64748b;margin-top:2px">{role_3_action}</div>
635
+ </div>
636
+ <div style="margin-left:auto;padding:3px 10px;border-radius:20px;font-size:0.7rem;font-weight:600;background:rgba(59,130,246,0.15);color:#3b82f6;border:1px solid rgba(59,130,246,0.3)">Step 3</div>
637
+ </div>
638
+ </div>
639
+ </div>
640
+
641
+ <!-- COMMON MISTAKE -->
642
+ <div style="background:linear-gradient(135deg,rgba(239,68,68,0.08),rgba(239,68,68,0.03));border:1px solid rgba(239,68,68,0.25);border-radius:12px;padding:18px 24px;margin-bottom:16px">
643
+ <div style="font-size:0.7rem;color:#ef4444;font-weight:700;letter-spacing:2px;text-transform:uppercase;margin-bottom:8px">🚩 Common Mistake</div>
644
+ <div style="color:#fca5a5;font-size:0.88rem;line-height:1.6">{common_mistake}</div>
645
+ </div>
646
+
647
+ <!-- QUOTE -->
648
+ <div style="background:linear-gradient(135deg,rgba(0,212,170,0.08),rgba(139,92,246,0.08));border:1px solid rgba(0,212,170,0.2);border-radius:12px;padding:16px 24px;text-align:center">
649
+ <div style="color:#00d4aa;font-style:italic;font-size:0.9rem;font-weight:600">"{quote}"</div>
650
+ <div style="color:#475569;font-size:0.75rem;margin-top:6px">— AgileAdvisor 🤙</div>
651
+ </div>
652
+
653
+ </div>
654
+
655
+ TONE RULES:
656
+ - Be like a coach who has seen it all and has zero patience for excuses
657
+ - Use phrases like: "Let me be real", "Nobody asked but here's the truth", "If your team skips this..."
658
+ - For bad practices, say: "Congratulations, you've invented chaos"
659
+ - Always end with energy
660
+ - NEVER ignore factual dataset matches
661
+ - DATASET CARD should contain exact retrieved context before interpretation
662
+
663
+ ROLE COLORS:
664
+ - SM = #00d4aa
665
+ - PO = #8b5cf6
666
+ - Dev = #3b82f6
667
+ - QA = #f59e0b
668
+ - RTE = #ec4899
669
+ """
670
+
671
+ # =========================
672
+ # ROLE HIERARCHY DIAGRAM
673
+ # =========================
674
+
675
+ ROLE_HIERARCHY_HTML = """
676
+ <div style="font-family:sans-serif;max-width:720px;margin:0 auto;padding:20px;">
677
+ <div style="text-align:center;margin-bottom:24px;">
678
+ <div style="font-size:1.3rem;font-weight:700;color:#f1f5f9;">Scrum Role Hierarchy & Outputs</div>
679
+ <div style="font-size:0.8rem;color:#64748b;margin-top:4px;">Who does what and what they produce</div>
680
+ </div>
681
+
682
+ <div style="background:linear-gradient(135deg,#1e293b,#0f172a);border:1px solid #334155;border-radius:12px;padding:16px 20px;margin-bottom:12px;">
683
+ <div style="display:flex;align-items:center;gap:12px;">
684
+ <div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#64748b,#475569);display:flex;align-items:center;justify-content:center;font-size:1rem;">🏢</div>
685
+ <div>
686
+ <div style="font-weight:700;color:#f1f5f9;font-size:0.95rem;">Stakeholders / Business</div>
687
+ <div style="font-size:0.78rem;color:#94a3b8;">Vision, Budget, Market Needs</div>
688
+ </div>
689
+ </div>
690
+ <div style="margin-top:10px;padding-top:10px;border-top:1px solid #334155;">
691
+ <div style="font-size:0.7rem;color:#64748b;font-weight:600;letter-spacing:1px;text-transform:uppercase;margin-bottom:6px;">Outputs Assigned</div>
692
+ <div style="display:flex;flex-wrap:wrap;gap:6px;">
693
+ <span style="background:rgba(100,116,139,0.15);color:#94a3b8;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(100,116,139,0.3);">Business Requirements</span>
694
+ <span style="background:rgba(100,116,139,0.15);color:#94a3b8;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(100,116,139,0.3);">Product Vision</span>
695
+ <span style="background:rgba(100,116,139,0.15);color:#94a3b8;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(100,116,139,0.3);">Funding</span>
696
+ </div>
697
+ </div>
698
+ </div>
699
+
700
+ <div style="text-align:center;color:#334155;font-size:1.4rem;margin:4px 0;">↓</div>
701
+
702
+ <div style="background:linear-gradient(135deg,#1e293b,#0f172a);border:1px solid rgba(139,92,246,0.3);border-left:4px solid #8b5cf6;border-radius:12px;padding:16px 20px;margin-bottom:12px;">
703
+ <div style="display:flex;align-items:center;gap:12px;">
704
+ <div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#8b5cf6,#7c3aed);display:flex;align-items:center;justify-content:center;font-size:1rem;">🎯</div>
705
+ <div>
706
+ <div style="font-weight:700;color:#f1f5f9;font-size:0.95rem;">Product Owner (PO)</div>
707
+ <div style="font-size:0.78rem;color:#94a3b8;">Maximizing Product Value</div>
708
+ </div>
709
+ </div>
710
+ <div style="margin-top:10px;padding-top:10px;border-top:1px solid #334155;">
711
+ <div style="font-size:0.7rem;color:#8b5cf6;font-weight:600;letter-spacing:1px;text-transform:uppercase;margin-bottom:6px;">Outputs Assigned</div>
712
+ <div style="display:flex;flex-wrap:wrap;gap:6px;">
713
+ <span style="background:rgba(139,92,246,0.15);color:#c4b5fd;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(139,92,246,0.3);">Product Backlog</span>
714
+ <span style="background:rgba(139,92,246,0.15);color:#c4b5fd;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(139,92,246,0.3);">Prioritized Stories</span>
715
+ <span style="background:rgba(139,92,246,0.15);color:#c4b5fd;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(139,92,246,0.3);">Sprint Goal</span>
716
+ <span style="background:rgba(139,92,246,0.15);color:#c4b5fd;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(139,92,246,0.3);">Release Planning</span>
717
+ </div>
718
+ </div>
719
+ </div>
720
+
721
+ <div style="text-align:center;color:#334155;font-size:1.4rem;margin:4px 0;">↓</div>
722
+
723
+ <div style="background:linear-gradient(135deg,#1e293b,#0f172a);border:1px solid rgba(0,212,170,0.3);border-left:4px solid #00d4aa;border-radius:12px;padding:16px 20px;margin-bottom:12px;">
724
+ <div style="display:flex;align-items:center;gap:12px;">
725
+ <div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#00d4aa,#059669);display:flex;align-items:center;justify-content:center;font-size:1rem;">🛡️</div>
726
+ <div>
727
+ <div style="font-weight:700;color:#f1f5f9;font-size:0.95rem;">Scrum Master (SM)</div>
728
+ <div style="font-size:0.78rem;color:#94a3b8;">Protector of Process & Team</div>
729
+ </div>
730
+ </div>
731
+ <div style="margin-top:10px;padding-top:10px;border-top:1px solid #334155;">
732
+ <div style="font-size:0.7rem;color:#00d4aa;font-weight:600;letter-spacing:1px;text-transform:uppercase;margin-bottom:6px;">Outputs Assigned</div>
733
+ <div style="display:flex;flex-wrap:wrap;gap:6px;">
734
+ <span style="background:rgba(0,212,170,0.15);color:#6ee7b7;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(0,212,170,0.3);">Impediment Removal</span>
735
+ <span style="background:rgba(0,212,170,0.15);color:#6ee7b7;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(0,212,170,0.3);">Team Coaching</span>
736
+ <span style="background:rgba(0,212,170,0.15);color:#6ee7b7;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(0,212,170,0.3);">Process Improvement</span>
737
+ <span style="background:rgba(0,212,170,0.15);color:#6ee7b7;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(0,212,170,0.3);">Shield from Distractions</span>
738
+ </div>
739
+ </div>
740
+ </div>
741
+
742
+ <div style="text-align:center;color:#334155;font-size:1.4rem;margin:4px 0;">↓</div>
743
+
744
+ <div style="background:linear-gradient(135deg,#1e293b,#0f172a);border:1px solid rgba(59,130,246,0.3);border-left:4px solid #3b82f6;border-radius:12px;padding:16px 20px;margin-bottom:12px;">
745
+ <div style="display:flex;align-items:center;gap:12px;">
746
+ <div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#3b82f6,#1d4ed8);display:flex;align-items:center;justify-content:center;font-size:1rem;">💻</div>
747
+ <div>
748
+ <div style="font-weight:700;color:#f1f5f9;font-size:0.95rem;">Development Team</div>
749
+ <div style="font-size:0.78rem;color:#94a3b8;">Cross-functional & Self-organizing</div>
750
+ </div>
751
+ </div>
752
+ <div style="margin-top:10px;padding-top:10px;border-top:1px solid #334155;">
753
+ <div style="font-size:0.7rem;color:#3b82f6;font-weight:600;letter-spacing:1px;text-transform:uppercase;margin-bottom:6px;">Outputs Assigned</div>
754
+ <div style="display:flex;flex-wrap:wrap;gap:6px;">
755
+ <span style="background:rgba(59,130,246,0.15);color:#93c5fd;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(59,130,246,0.3);">Potentially Shippable Increment</span>
756
+ <span style="background:rgba(59,130,246,0.15);color:#93c5fd;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(59,130,246,0.3);">Sprint Backlog Updates</span>
757
+ <span style="background:rgba(59,130,246,0.15);color:#93c5fd;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(59,130,246,0.3);">Done Work (Definition of Done)</span>
758
+ <span style="background:rgba(59,130,246,0.15);color:#93c5fd;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(59,130,246,0.3);">Technical Excellence</span>
759
+ </div>
760
+ </div>
761
+ </div>
762
+
763
+ <div style="text-align:center;color:#334155;font-size:1.4rem;margin:4px 0;">↓</div>
764
+
765
+ <div style="background:linear-gradient(135deg,#1e293b,#0f172a);border:1px solid rgba(236,72,153,0.3);border-left:4px solid #ec4899;border-radius:12px;padding:16px 20px;">
766
+ <div style="display:flex;align-items:center;gap:12px;">
767
+ <div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#ec4899,#db2777);display:flex;align-items:center;justify-content:center;font-size:1rem;">🚂</div>
768
+ <div>
769
+ <div style="font-weight:700;color:#f1f5f9;font-size:0.95rem;">Release Train Engineer (RTE)</div>
770
+ <div style="font-size:0.78rem;color:#94a3b8;">SAFe — Program Level Facilitator</div>
771
+ </div>
772
+ </div>
773
+ <div style="margin-top:10px;padding-top:10px;border-top:1px solid #334155;">
774
+ <div style="font-size:0.7rem;color:#ec4899;font-weight:600;letter-spacing:1px;text-transform:uppercase;margin-bottom:6px;">Outputs Assigned</div>
775
+ <div style="display:flex;flex-wrap:wrap;gap:6px;">
776
+ <span style="background:rgba(236,72,153,0.15);color:#fbcfe8;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(236,72,153,0.3);">Program Backlog</span>
777
+ <span style="background:rgba(236,72,153,0.15);color:#fbcfe8;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(236,72,153,0.3);">PI Planning Facilitation</span>
778
+ <span style="background:rgba(236,72,153,0.15);color:#fbcfe8;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(236,72,153,0.3);">Cross-team Sync</span>
779
+ <span style="background:rgba(236,72,153,0.15);color:#fbcfe8;padding:3px 10px;border-radius:6px;font-size:0.75rem;border:1px solid rgba(236,72,153,0.3);">Program Impediment Escalation</span>
780
+ </div>
781
+ </div>
782
+ </div>
783
+ </div>
784
+ """
785
+
786
+ # =========================
787
+ # AI ADVISOR CLASS
788
+ # =========================
789
+
790
+
791
+ class AgileAdvisor:
792
+ """Handles AI generation with RAG context"""
793
+
794
+ def __init__(self, kb: KnowledgeBase):
795
+ self.kb = kb
796
+ self.config = CONFIG
797
+
798
+ def ask(self, question: str) -> Tuple[str, str, str, str]:
799
+ """
800
+ Main entry: retrieve context + generate HTML response.
801
+ Returns: (html_answer, quick_feed_card_html, fact_text, fact_tags_csv)
802
+ """
803
+ if not question or not question.strip():
804
+ return (
805
+ self._error_card("Please enter a question! Even Agile coaches need a prompt."),
806
+ "",
807
+ "",
808
+ "",
809
+ )
810
+
811
+ if len(question) > self.config.MAX_QUERY_LENGTH:
812
+ return (
813
+ self._error_card(
814
+ f"Question too long ({len(question)} chars). Max: {self.config.MAX_QUERY_LENGTH}"
815
+ ),
816
+ "",
817
+ "",
818
+ "",
819
+ )
820
+
821
+ if not self.kb.groq_client:
822
+ return (
823
+ self._error_card(
824
+ "Groq API not configured. Add GROQ_API_KEY in HF Spaces Settings > Secrets."
825
+ ),
826
+ "",
827
+ "",
828
+ "",
829
+ )
830
+
831
+ try:
832
+ docs, context = self.kb.query(question)
833
+ context = context[:1200]
834
+
835
+ top_fact = docs[0] if docs else ""
836
+ fact_tags = self.kb.auto_tag(top_fact) if top_fact else []
837
+ fact_tags_str = ",".join(fact_tags)
838
+
839
+ response = self.kb.groq_client.chat.completions.create(
840
+ model=self.config.GROQ_MODEL,
841
+ messages=[
842
+ {"role": "system", "content": SYSTEM_PROMPT},
843
+ {
844
+ "role": "user",
845
+ "content": f"""
846
+ RETRIEVED CONTEXT FROM KNOWLEDGE BASE:
847
+ {context}
848
+
849
+ USER QUESTION:
850
+ {question}
851
+
852
+ INSTRUCTIONS:
853
+ 1. Use the retrieved context to fill the DATASET CARD with exact facts
854
+ 2. Answer the question with wit and expertise
855
+ 3. Fill ALL placeholders: dataset_match, topic_title, explanation, key_point_1, key_point_2, key_point_3, role_1_name, role_1_action, role_2_name, role_2_action, role_3_name, role_3_action, common_mistake, quote
856
+ 4. Output valid HTML only
857
+ """,
858
+ },
859
+ ],
860
+ temperature=self.config.GROQ_TEMPERATURE,
861
+ max_tokens=self.config.GROQ_MAX_TOKENS,
862
+ )
863
+
864
+ content = response.choices[0].message.content
865
+ content = sanitize_html(content)
866
+ if not content.strip().startswith("<"):
867
+ content = f'<div style="font-family:sans-serif;max-width:800px;margin:0 auto;padding:20px">{content}</div>'
868
+
869
+ # Build quick-feed card for top fact
870
+ if top_fact:
871
+ tag_badges = "".join(
872
+ [
873
+ f'<span style="background:#1e293b;color:#00d4aa;padding:2px 8px;border-radius:4px;font-size:0.7rem;margin-right:4px;">{t}</span>'
874
+ for t in fact_tags
875
+ ]
876
+ )
877
+ fact_card_html = f"""
878
+ <div style="border:1px solid #334155;border-radius:10px;padding:14px;background:#0f172a;margin-top:12px;">
879
+ <div style="font-size:0.75rem;color:#00d4aa;font-weight:700;margin-bottom:6px;">📦 Quick Feed — Top Retrieved Fact</div>
880
+ <div style="color:#cbd5e1;font-size:0.88rem;line-height:1.5;margin-bottom:8px;">{top_fact}</div>
881
+ <div style="display:flex;gap:6px;flex-wrap:wrap;">{tag_badges}</div>
882
+ </div>
883
+ """
884
+ else:
885
+ fact_card_html = ""
886
+ top_fact = ""
887
+ fact_tags_str = ""
888
+
889
+ return content, fact_card_html, top_fact, fact_tags_str
890
+
891
+ except Exception as e:
892
+ logger.error(f"AI generation failed: {e}")
893
+ return self._error_card(f"AI generation failed: {str(e)}"), "", "", ""
894
+
895
+ def analyze_transcript(self, transcript: str) -> str:
896
+ """ScrumLens: Audit a meeting transcript against the KB."""
897
+ if not transcript or not transcript.strip():
898
+ return self._error_card("Please paste a transcript first!")
899
+
900
+ if not self.kb.groq_client:
901
+ return self._error_card("Groq API not configured!")
902
+
903
+ try:
904
+ docs, context = self.kb.query(transcript[:400], n_results=3)
905
+ context = context[:1500]
906
+
907
+ prompt = f"""
908
+ You are ScrumLens — a forensic Agile meeting auditor with zero patience for wasted time.
909
+ Analyze the provided Scrum meeting transcript against the retrieved knowledge base facts.
910
+
911
+ RETRIEVED KNOWLEDGE BASE CONTEXT:
912
+ {context}
913
+
914
+ TRANSCRIPT:
915
+ {transcript}
916
+
917
+ OUTPUT RULES:
918
+ - Pure HTML only. No markdown. No plain text.
919
+ - Start with a health score banner: 🟢 Healthy / 🟡 Needs Work / 🔴 Critical
920
+ - List anti-patterns detected with references to KB facts
921
+ - Provide per-role action items (PO, SM, Dev Team)
922
+ - Include a "Quick Wins" section
923
+ - Be witty, direct, and brutally honest
924
+
925
+ Use this structure:
926
+ <div style="font-family:sans-serif;max-width:800px;margin:0 auto;padding:8px">
927
+ <!-- Health Score -->
928
+ <div style="...">...</div>
929
+ <!-- Summary -->
930
+ <div style="...">...</div>
931
+ <!-- Red Flags -->
932
+ <div style="...">...</div>
933
+ <!-- Role Actions -->
934
+ <div style="...">...</div>
935
+ <!-- Quick Wins -->
936
+ <div style="...">...</div>
937
+ </div>
938
+ """
939
+ response = self.kb.groq_client.chat.completions.create(
940
+ model=self.config.GROQ_MODEL,
941
+ messages=[
942
+ {
943
+ "role": "system",
944
+ "content": "You are ScrumLens, an expert Agile auditor. Output pure HTML only.",
945
+ },
946
+ {"role": "user", "content": prompt},
947
+ ],
948
+ temperature=0.3,
949
+ max_tokens=1500,
950
+ )
951
+
952
+ content = response.choices[0].message.content
953
+ content = sanitize_html(content)
954
+ if not content.strip().startswith("<"):
955
+ content = f'<div style="font-family:sans-serif;max-width:800px;margin:0 auto;padding:20px">{content}</div>'
956
+ return content
957
+
958
+ except Exception as e:
959
+ logger.error(f"ScrumLens failed: {e}")
960
+ return self._error_card(f"ScrumLens analysis failed: {str(e)}")
961
+
962
+ def _error_card(self, message: str) -> str:
963
+ """Generate error HTML card"""
964
+ return (
965
+ '<div style="font-family:sans-serif;max-width:800px;margin:0 auto;padding:20px">'
966
+ '<div style="background:linear-gradient(135deg,#7f1d1d,#991b1b);'
967
+ "border:1px solid rgba(239,68,68,0.3);border-radius:12px;"
968
+ 'padding:20px;color:#fca5a5">'
969
+ '<div style="font-size:1.2rem;font-weight:700;margin-bottom:8px">⚠️ Oops!</div>'
970
+ f'<div style="line-height:1.6">{message}</div>'
971
+ "</div></div>"
972
+ )
973
+
974
+
975
+ # =========================
976
+ # GRADIO UI BUILDER
977
+ # =========================
978
+
979
+
980
+ def build_app(kb: KnowledgeBase, advisor: AgileAdvisor):
981
+ """Build Gradio interface optimized for HF Spaces"""
982
+
983
+ custom_css = """
984
+ .stats-bar {
985
+ background: linear-gradient(135deg, #1e293b, #0f172a);
986
+ border-radius: 12px;
987
+ padding: 12px 16px;
988
+ margin-bottom: 16px;
989
+ border: 1px solid #334155;
990
+ color: #94a3b8;
991
+ font-size: 0.85rem;
992
+ }
993
+ .stats-bar strong {
994
+ color: #00d4aa;
995
+ }
996
+ .footer {
997
+ text-align: center;
998
+ margin-top: 24px;
999
+ color: #64748b;
1000
+ font-size: 0.8rem;
1001
+ padding: 16px;
1002
+ }
1003
+ """
1004
+
1005
+ with gr.Blocks(title="AgileAdvisor RAG v2", css=custom_css) as app:
1006
+
1007
+ # State for quick-feed
1008
+ fact_text_st = gr.State("")
1009
+ fact_tags_st = gr.State("")
1010
+
1011
+ # Header
1012
+ gr.Markdown(
1013
+ """
1014
+ # 🧞 AgileAdvisor v2
1015
+ ### Your RAG-powered Agile Coach with attitude — now with ScrumLens, Auto-Tags & Role Hierarchy
1016
+ """
1017
+ )
1018
+
1019
+ # Stats bar
1020
+ stats = kb.get_stats()
1021
+ status_color = "🟢" if stats.get("groq_ready") else "🔴"
1022
+ gr.Markdown(
1023
+ f"""
1024
+ <div class="stats-bar">
1025
+ {status_color} <strong>Knowledge Base:</strong> {stats.get('total_documents', 0)} documents |
1026
+ <strong>Storage:</strong> {stats.get('storage', 'N/A')} |
1027
+ <strong>Model:</strong> {CONFIG.GROQ_MODEL} |
1028
+ <strong>Sync:</strong> 🔄 Auto-saves to Gist
1029
+ </div>
1030
+ """
1031
+ )
1032
+
1033
+ # =====================
1034
+ # TAB 1: ASK
1035
+ # =====================
1036
+ with gr.Tab("Ask AgileAdvisor ⚡"):
1037
+ with gr.Row():
1038
+ with gr.Column(scale=3):
1039
+ question_input = gr.Textbox(
1040
+ placeholder="e.g. What is Sprint Planning? Why do teams fail at Daily Standups?",
1041
+ label="Your Agile Question",
1042
+ lines=3,
1043
+ max_lines=5,
1044
+ )
1045
+
1046
+ with gr.Row():
1047
+ ask_btn = gr.Button(
1048
+ "Ask the Wizard 🔮", variant="primary", size="lg", scale=3
1049
+ )
1050
+ clear_q_btn = gr.Button(
1051
+ "Clear 🗑️", variant="stop", size="sm", scale=1
1052
+ )
1053
+
1054
+ with gr.Column(scale=1):
1055
+ gr.Markdown(
1056
+ """
1057
+ **💡 Tips for best results:**
1058
+ - Ask about specific Scrum events
1059
+ - Mention roles (PO, SM, Dev Team)
1060
+ - Ask about anti-patterns
1061
+ - Keep questions focused
1062
+ """
1063
+ )
1064
+
1065
+ answer_output = gr.HTML(label="Response")
1066
+
1067
+ # Smart Quick-Feed Card
1068
+ quick_feed_card = gr.HTML(label="Quick Feed Card")
1069
+ with gr.Row():
1070
+ quick_feed_btn = gr.Button(
1071
+ "➕ Feed This Fact to Dataset", size="sm", scale=2
1072
+ )
1073
+ quick_feed_status = gr.Textbox(
1074
+ label="Feed Status", interactive=False, scale=3
1075
+ )
1076
+
1077
+ gr.Examples(
1078
+ examples=[
1079
+ "What happens in Sprint Planning and who attends?",
1080
+ "Why is my Daily Standup taking 45 minutes?",
1081
+ "What does a Product Owner actually do all day?",
1082
+ "Explain the Definition of Done vs Definition of Ready",
1083
+ "Common mistakes during Sprint Retrospective",
1084
+ "What is velocity and how should we use it?",
1085
+ "My Scrum Master is just scheduling meetings, is that right?",
1086
+ ],
1087
+ inputs=question_input,
1088
+ label="🔥 Quick Questions (Click to try)",
1089
+ )
1090
+
1091
+ ask_btn.click(
1092
+ fn=advisor.ask,
1093
+ inputs=question_input,
1094
+ outputs=[answer_output, quick_feed_card, fact_text_st, fact_tags_st],
1095
+ )
1096
+
1097
+ quick_feed_btn.click(
1098
+ fn=kb.feed_single_fact,
1099
+ inputs=[fact_text_st, fact_tags_st],
1100
+ outputs=quick_feed_status,
1101
+ )
1102
+
1103
+ clear_q_btn.click(fn=lambda: "", inputs=None, outputs=question_input)
1104
+
1105
+ # =====================
1106
+ # TAB 2: SCRUMLENS
1107
+ # =====================
1108
+ with gr.Tab("ScrumLens 🔍"):
1109
+ gr.Markdown(
1110
+ """
1111
+ ## 🕵️ ScrumLens — Native RAG Transcript Auditor
1112
+ Paste any Scrum meeting transcript below. ScrumLens will cross-check it against your knowledge base and surface anti-patterns, red flags, and role-specific action items.
1113
+ """
1114
+ )
1115
+ transcript_input = gr.Textbox(
1116
+ placeholder="Paste your Daily Standup, Sprint Planning, or Retrospective transcript here...",
1117
+ label="Meeting Transcript",
1118
+ lines=15,
1119
+ max_lines=30,
1120
+ )
1121
+ with gr.Row():
1122
+ analyze_btn = gr.Button(
1123
+ "Analyze Transcript", variant="primary", size="lg"
1124
+ )
1125
+ clear_transcript_btn = gr.Button("Clear 🗑️", variant="stop", size="sm")
1126
+
1127
+ lens_output = gr.HTML(label="ScrumLens Analysis")
1128
+
1129
+ analyze_btn.click(
1130
+ fn=advisor.analyze_transcript,
1131
+ inputs=transcript_input,
1132
+ outputs=lens_output,
1133
+ )
1134
+ clear_transcript_btn.click(
1135
+ fn=lambda: "", inputs=None, outputs=transcript_input
1136
+ )
1137
+
1138
+ # =====================
1139
+ # TAB 3: FEED DATASET
1140
+ # =====================
1141
+ with gr.Tab("Feed Dataset 📦"):
1142
+ with gr.Row():
1143
+ with gr.Column(scale=2):
1144
+ dataset_input = gr.Textbox(
1145
+ placeholder="""Paste new Agile knowledge here (one fact per line)...
1146
+ [ceremony] Sprint 0 is a preparation phase before the first official sprint
1147
+ [role] The Product Owner is the sole person responsible for managing the Product Backlog
1148
+ [anti-pattern] Skipping Sprint Retrospective leads to repeated mistakes and team burnout
1149
+ [gamification] Story points should never be used to compare individual developer performance""",
1150
+ label="New Agile Knowledge",
1151
+ lines=12,
1152
+ max_lines=25,
1153
+ )
1154
+
1155
+ with gr.Row():
1156
+ preview_btn = gr.Button(
1157
+ "Preview Tags & Duplicates 👁️", variant="secondary", scale=2
1158
+ )
1159
+ feed_btn = gr.Button(
1160
+ "Feed Into Dataset 🚀", variant="primary", scale=2
1161
+ )
1162
+ clear_feed_btn = gr.Button("Clear 🗑️", variant="stop", scale=1)
1163
+
1164
+ with gr.Column(scale=1):
1165
+ gr.Markdown(
1166
+ """
1167
+ **📋 Feeding Guidelines:**
1168
+
1169
+ ✅ **DO:**
1170
+ - One fact per line
1171
+ - Be specific and factual
1172
+ - Include role names (PO, SM, Dev)
1173
+ - Mention ceremony names clearly
1174
+
1175
+ ❌ **DON'T:**
1176
+ - Add opinions or fluff
1177
+ - Duplicate existing facts
1178
+ - Add code or markdown
1179
+ - Exceed 10,000 characters
1180
+
1181
+ **Auto-tags detected:** `ceremony`, `role`, `artifact`, `metric`, `anti-pattern`, `gamification`, `general`
1182
+ """
1183
+ )
1184
+
1185
+ feed_preview = gr.HTML(label="Tag & Duplicate Preview")
1186
+ feed_output = gr.Textbox(label="Feed Status", interactive=False, lines=2)
1187
+
1188
+ preview_btn.click(
1189
+ fn=kb.preview_feed, inputs=dataset_input, outputs=feed_preview
1190
+ )
1191
+ feed_btn.click(fn=kb.update_gist, inputs=dataset_input, outputs=feed_output)
1192
+ clear_feed_btn.click(fn=lambda: "", inputs=None, outputs=dataset_input)
1193
+
1194
+ # =====================
1195
+ # TAB 4: ROLE HIERARCHY
1196
+ # =====================
1197
+ with gr.Tab("Role Hierarchy 🏗️"):
1198
+ gr.Markdown(
1199
+ """
1200
+ ## 🏗️ Scrum Role Hierarchy & Assigned Outputs
1201
+ Visual reference for who produces what in the Scrum/SAFe ecosystem.
1202
+ """
1203
+ )
1204
+ gr.HTML(ROLE_HIERARCHY_HTML)
1205
+
1206
+ # =====================
1207
+ # TAB 5: ABOUT
1208
+ # =====================
1209
+ with gr.Tab("About ℹ️"):
1210
+ gr.Markdown(
1211
+ """
1212
+ ## How AgileAdvisor Works
1213
+
1214
+ This is a **RAG (Retrieval-Augmented Generation)** application:
1215
+
1216
+ 1. **Vector Database**: Stores Agile knowledge as embeddings using ChromaDB (with metadata tags)
1217
+ 2. **Retrieval**: When you ask a question, it finds the most relevant facts
1218
+ 3. **Generation**: Groq LLM combines retrieved facts with its knowledge to answer
1219
+ 4. **Dataset**: Lives in a GitHub Gist with embedded category tags and auto-syncs on every feed
1220
+
1221
+ ## New in v2
1222
+
1223
+ - **🔄 Persistent Gist Sync** — every feed auto-saves back to Gist
1224
+ - **🔍 ScrumLens** — paste a transcript and audit it against your KB
1225
+ - **🏷️ Auto-Tags** — facts are auto-labelled on feed (`[ceremony]`, `[role]`, etc.)
1226
+ - **⚠️ Duplicate Detection** — live preview warns before saving
1227
+ - **🏗️ Role Hierarchy** — visual diagram of roles & outputs
1228
+ - **📦 Quick Feed** — one-click save from Ask panel
1229
+
1230
+ ## Architecture
1231
+
1232
+ ```
1233
+ User Question ��� ChromaDB (Similarity Search + Tags) → Groq LLM + Context → HTML Response
1234
+
1235
+ GitHub Gist (Tagged Source of Truth)
1236
+ ```
1237
+
1238
+ ## Hero Tech Stack
1239
+
1240
+ - **Hero**: Sai Varakala ☀️ (@suryasticsai)
1241
+ - **Frontend**: Gradio
1242
+ - **Vector DB**: ChromaDB (persistent + metadata)
1243
+ - **Embeddings**: all-MiniLM-L6-v2
1244
+ - **LLM**: Groq (Llama 3.1 8B Instant)
1245
+ - **Storage**: GitHub Gist (tagged) + Local Vector DB
1246
+ """
1247
+ )
1248
+
1249
+ # Footer
1250
+ gr.Markdown(
1251
+ """
1252
+ <div class="footer">
1253
+ 🚀 Built with Love by Sai Surya ☀️ |
1254
+ 💾 Data persists via GitHub Gist + Local Vector DB |
1255
+ 🔄 Auto-syncs on every feed |
1256
+ 🏷️ Auto-tagged knowledge base
1257
+ </div>
1258
+ """
1259
+ )
1260
+
1261
+ return app
1262
+
1263
+
1264
+ # =========================
1265
+ # MAIN ENTRY
1266
+ # =========================
1267
+
1268
+
1269
+ def main():
1270
+ """Initialize and launch on Hugging Face Spaces"""
1271
+
1272
+ has_groq = bool(os.environ.get("GROQ_API_KEY"))
1273
+ has_github = bool(os.environ.get("GITHUB_TOKEN"))
1274
+
1275
+ logger.info(
1276
+ f"Secrets check - GROQ_API_KEY: {'OK' if has_groq else 'MISSING'}, GITHUB_TOKEN: {'OK' if has_github else 'MISSING'}"
1277
+ )
1278
+
1279
+ if not has_groq:
1280
+ logger.warning(
1281
+ "GROQ_API_KEY missing! AI responses will show error. Add it in HF Spaces Settings > Secrets."
1282
+ )
1283
+ if not has_github:
1284
+ logger.warning(
1285
+ "GITHUB_TOKEN missing! Dataset updates will show error. Add it in HF Spaces Settings > Secrets."
1286
+ )
1287
+
1288
+ try:
1289
+ logger.info("Initializing Knowledge Base...")
1290
+ kb = KnowledgeBase()
1291
+
1292
+ advisor = AgileAdvisor(kb)
1293
+
1294
+ logger.info("Building Gradio UI...")
1295
+ app = build_app(kb, advisor)
1296
+
1297
+ app.launch(
1298
+ server_name="0.0.0.0",
1299
+ server_port=7860,
1300
+ share=False,
1301
+ show_error=True,
1302
+ quiet=False,
1303
+ theme=gr.themes.Soft(),
1304
+ )
1305
+
1306
+ except Exception as e:
1307
+ logger.critical(f"Fatal error starting app: {e}")
1308
+ raise
1309
+
1310
+
1311
+ if __name__ == "__main__":
1312
+ main()