import gradio as gr import chromadb from chromadb.utils import embedding_functions from groq import Groq import os import json import datetime import re # --- Knowledge Base Setup --- documents = [ "Sprint Planning is a Scrum ceremony where the Product Owner presents prioritized backlog items, the Scrum Master facilitates the session, and the Development Team estimates and commits to sprint work. Output includes Sprint Goal, Sprint Backlog, and committed user stories.", "Daily Standup is a 15-minute Agile sync facilitated by the Scrum Master where Developers discuss completed work, upcoming tasks, and blockers. Output includes blocker visibility, progress tracking, and daily alignment.", "Sprint Review is conducted by the Scrum Team with stakeholders to demonstrate completed increments. Product Owner validates delivered work and gathers feedback. Output includes stakeholder feedback, accepted stories, and backlog refinement inputs.", "Sprint Retrospective is facilitated by the Scrum Master to identify improvements, celebrate wins, and resolve team pain points. Output includes actionable improvements, retrospective notes, and process enhancement ideas.", "Bornfire Retrospective Talks were introduced as interactive retrospective discussions focused on honesty, emotional transparency, team bonding, and psychological safety. Scrum Masters facilitated conversations while team members shared learnings and concerns. Output included trust building, actionable feedback, and stronger collaboration.", "Velocity measures the amount of work completed by a Scrum Team during a sprint using story points. Scrum Masters track velocity while Product Owners use it for forecasting future sprint capacity. Output includes sprint predictability and planning insights.", "Definition of Done is a shared quality agreement created collaboratively by Developers, Product Owner, and Scrum Master. Output ensures stories meet quality standards before completion.", "PI Planning is a SAFe event where Release Train Engineers, Product Management, Scrum Masters, Product Owners, Business Owners, and Agile Teams align on Program Increment objectives. Output includes PI Objectives, dependency mapping, and team commitments.", "WSJF stands for Weighted Shortest Job First, a SAFe prioritization method usually driven by Product Management and Business Owners. Output includes prioritized backlog sequencing based on business value and duration.", "A Scrum Master facilitates Agile ceremonies, removes impediments, tracks team health, coaches Agile practices, manages delivery coordination, and enables continuous improvement. Output includes smoother delivery flow, improved collaboration, and Agile maturity.", "A Product Owner owns backlog prioritization, defines acceptance criteria, aligns business goals, and clarifies requirements for teams. Output includes refined backlog, prioritized user stories, and business alignment.", "Developers are responsible for designing, coding, testing, reviewing, and delivering working software increments during a sprint. Output includes completed features, technical improvements, and production-ready deliverables.", "Release Train Engineers coordinate Agile Release Trains in SAFe environments, manage cross-team dependencies, support PI Planning, and track ART progress. Output includes ART alignment, dependency management, and program-level delivery tracking.", "Sai Varakala expressed gratitude towards Synergeons for collaboration, dedication, learning culture, and shared memories across Agile journeys.", "Pokemon Sprint Card Game transformed Agile execution into a gamified adventure where team members earned Pokemon cards based on sprint performance, collaboration, story closures, and engagement. Scrum Masters managed gameplay while teams participated in sprint activities. Output included team bonding, engagement, motivation, and improved sprint participation.", "Pokemon cards symbolized resilience, teamwork, and camaraderie. Trainers collected cards as memorable artifacts representing sprint contributions and achievements.", "Sprint Agile Economy introduced concepts like PikaCoins, StoryStocks, SprintMarket, and investment-based gamification mechanics to encourage Agile engagement. Scrum Masters experimented with economy balancing while participants interacted with the framework. Output included experimentation in Agile gamification and learning around behavioral systems.", "Sprint Agile Economy was later discontinued due to complexity, unfair taxation, lack of impact, and micro-management concerns. Output from retrospectives suggested focusing on simpler and more meaningful engagement frameworks.", "EnvyLevelUp was introduced as a pilot self-development and productivity initiative focused on growth, learning, and personal improvement within Agile teams.", "Synergeon Learners channel was created on November 25th, 2024 for collaborative learning, resource sharing, Agile discussions, and planning activities.", "The channel supported resources sharing, Q&A forums, retrospective discussions, planning sessions, JavaScript learning, and Agile knowledge exchange.", "Festival Of Lights was the Agile iteration theme for Sprint 4.1.", "Gracious Sunshine was the Agile iteration theme for Sprint 4.2.", "Battle of Kurukshetra was the Agile iteration theme for Sprint 4.3.", "Spirit of Game was the Agile iteration theme for Sprint 4.4.", "Christmas Parade was the Agile iteration theme for Sprint 4.5.", "New Year Vibe was the Agile iteration theme for Sprint 4.IP.", "JavaScript learning sessions introduced concepts such as variables, let, const, and var declarations to learners within the Synergeon community.", "Utility Toolkit initiatives were introduced to automate repetitive Scrum Master and Agile coordination activities using modern tooling and lightweight automation approaches.", "Jira Sprint Summary Generator automated sprint reporting and progress summarization responsibilities usually handled by Scrum Masters. Output included faster reporting and reduced manual effort.", "Slack Summary Poster automated Agile communication updates across channels and teams. Output included improved visibility and streamlined coordination.", "WSR Mail Generator automated Weekly Status Report creation using HTML templates and Agile metrics. Output included professional reporting and reduced reporting overhead.", "Jira Token Hyperlink Utility simplified Jira ticket referencing and navigation workflows for Agile teams. Output included improved accessibility and faster navigation.", "Sprint Burndown Calculator automated sprint progress calculations and visualization support for Scrum Masters. Output included sprint forecasting and delivery transparency.", "Retro Action Tracker automated retrospective action item tracking and follow-up reminders. Output included accountability and continuous improvement tracking.", "Dynamic Jira dashboards were maintained for INC, FF, TT, and AOS teams to track delivery metrics, sprint progress, impediments, and team health.", "IT2.5 A Moment for Unity focused on compassion, teamwork, and resilience after the Air India AI-171 tragedy while encouraging emotional solidarity among team members.", "IT2.IP Bonalu Festive Vibe celebrated Hyderabad culture and marked successful completion of PI 2025.2 with gratitude and celebration.", "IT3.1 Dancing Peacock emphasized agility, resilience, and vibrant collaboration during technical and operational challenges.", "IT3.2 Shooting Stars celebrated milestones, weddings, birthdays, and team achievements while maintaining delivery momentum.", "IT3.3 Monsoon Rainbow celebrated Ganesh Chaturthi, zero impediments, and recognition through Best Team Award achievements.", "IT3.4 Aquatic Symphony introduced ReCall Play retrospective activities and innovative team engagement concepts.", "IT3.5 Envy Master focused on mentorship, innovation, self-growth, and SM-ART Factory initiatives.", "IT3.IP Oktober Grand Festival celebrated Dussehra while preparing teams for future Program Increment goals and planning.", "IT4.1 Festival of Lights promoted positivity, collaboration, AI-assisted productivity, and adoption of tools like Co-pilot within development workflows.", "AWS Cloud Practitioner Certification resources and AWS learning materials were shared within the Synergeon Learners ecosystem to encourage cloud learning.", "Retrospective agendas included Daily Standup planning, Synergeon Learning planning, IdeaTalks, Sprint Card Game discussions, planned leave tracking, and collaboration improvement discussions.", "Thunder Sprint statistics included Pokemon such as Regidrago, Arcanine, Umbreon, Snorlax, Dragonite, Pikachu, Palkia, Flareon, and Garchomp representing sprint performance and achievements.", "The Synergeon community focused on combining Agile practices, gamification, learning culture, automation, emotional intelligence, and collaboration into a memorable and innovative Agile ecosystem." ] db = chromadb.Client() emb = embedding_functions.DefaultEmbeddingFunction() collection = db.get_or_create_collection(name="agile_knowledge", embedding_function=emb) if collection.count() == 0: collection.add(documents=documents, ids=[f"doc_{i}" for i in range(len(documents))]) # Fetch API key securely from HuggingFace Secrets GROQ_API_KEY = os.getenv("GROQ_API_KEY") if not GROQ_API_KEY: raise ValueError("Missing GROQ_API_KEY") groq_client = Groq(api_key=GROQ_API_KEY) # --- System Prompt for Structured Extraction --- SYSTEM_PROMPT = """ You are ScrumLens Analyzer. Your job is to analyze a conversation transcript and extract key Agile insights. For long transcripts, analyze ALL messages but keep each chain item concise. Return ONLY a JSON object with the following structure: { "stats": { "messages": 0, "actions": 0, "decisions": 0, "health": 100 }, "roles": { "decision_maker": "", "facilitator": "", "dev_lead": "", "qa_lead": "" }, "chain": [ { "user": "Name", "role": "Role", "time": "HH:MM AM", "text": "...", "tag": "Decision/Action/Risk/Blocker/Idea/None" } ], "topics": ["Topic 1", "Topic 2"], "health_status": "Sprint looks healthy...", "actions": [ { "text": "Action...", "owner": "Name" } ], "decisions": [ { "text": "Decision...", "owner": "Name" } ], "risks": [ "Risk..." ] } """ # --- Robust JSON Repair --- def safe_json_loads(text): """Parse JSON with multiple fallback strategies.""" text = text.strip() try: return json.loads(text) except json.JSONDecodeError: pass match = re.search(r"```(?:json)?\s*(.*?)```", text, re.DOTALL) if match: try: return json.loads(match.group(1).strip()) except: pass start = text.find("{") end = text.rfind("}") if start != -1 and end != -1 and end > start: try: return json.loads(text[start:end+1]) except: pass return None # --- Transcript Chunking --- def parse_transcript_lines(text): """Parse raw transcript into structured lines.""" lines = [] pattern = r"\[?(\d{1,2}:\d{2}\s*(?:AM|PM|am|pm)?)\]?\s*([A-Za-z\s]+?):\s*(.+)" for raw_line in text.strip().split("\n"): raw_line = raw_line.strip() if not raw_line: continue m = re.match(pattern, raw_line) if m: time, user, msg = m.groups() lines.append({"time": time.strip(), "user": user.strip(), "text": msg.strip()}) else: m2 = re.match(r"([A-Za-z\s]+?):\s*(.+)", raw_line) if m2: user, msg = m2.groups() lines.append({"time": "", "user": user.strip(), "text": msg.strip()}) else: lines.append({"time": "", "user": "Unknown", "text": raw_line}) return lines def chunk_lines(lines, max_per_chunk=35): for i in range(0, len(lines), max_per_chunk): yield lines[i:i+max_per_chunk] def lines_to_text(lines): return "\n".join([ f"[{l.get('time','')}] {l['user']}: {l['text']}" for l in lines ]) # --- Analysis Core --- def analyze_chunk(chunk_text, context, progress=None): try: response = groq_client.chat.completions.create( model="llama-3.1-8b-instant", messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": f"Context: {context}\n\nAnalyze this conversation segment:\n{chunk_text}"} ], temperature=0.1, response_format={"type": "json_object"}, max_tokens=4096 ) raw = response.choices[0].message.content return safe_json_loads(raw) except Exception as e: print(f"Chunk analysis error: {e}") return None def merge_analyses(results): if not results: return None if len(results) == 1: return results[0] merged = { "stats": {"messages": 0, "actions": 0, "decisions": 0, "health": 75}, "roles": {"decision_maker": "", "facilitator": "", "dev_lead": "", "qa_lead": ""}, "chain": [], "topics": [], "health_status": "", "actions": [], "decisions": [], "risks": [] } health_scores = [] for r in results: if not isinstance(r, dict): continue if isinstance(r.get("chain"), list): merged["chain"].extend(r["chain"]) if isinstance(r.get("actions"), list): merged["actions"].extend(r["actions"]) if isinstance(r.get("decisions"), list): merged["decisions"].extend(r["decisions"]) if isinstance(r.get("risks"), list): merged["risks"].extend(r["risks"]) if isinstance(r.get("topics"), list): merged["topics"].extend(r["topics"]) for rk in ["decision_maker", "facilitator", "dev_lead", "qa_lead"]: val = r.get("roles", {}).get(rk, "") if val and not merged["roles"].get(rk): merged["roles"][rk] = val h = r.get("stats", {}).get("health", 75) health_scores.append(h) if r.get("health_status"): merged["health_status"] = r["health_status"] seen = set() unique_chain = [] for msg in merged["chain"]: key = f"{msg.get('user','')}:{msg.get('text','')}:{msg.get('time','')}" if key not in seen: seen.add(key) unique_chain.append(msg) merged["chain"] = unique_chain merged["topics"] = list(dict.fromkeys(merged["topics"])) merged["risks"] = list(dict.fromkeys(merged["risks"])) merged["stats"]["messages"] = len(unique_chain) merged["stats"]["actions"] = len(merged["actions"]) merged["stats"]["decisions"] = len(merged["decisions"]) merged["stats"]["health"] = int(sum(health_scores)/len(health_scores)) if health_scores else 75 return merged def apply_fallbacks(data): data.setdefault("stats", {"messages": 0, "actions": 0, "decisions": 0, "health": 75}) data.setdefault("roles", {}) data.setdefault("chain", []) data.setdefault("topics", []) data.setdefault("health_status", "No health summary available.") data.setdefault("actions", []) data.setdefault("decisions", []) data.setdefault("risks", []) data["roles"].setdefault("decision_maker", "") data["roles"].setdefault("facilitator", "") data["roles"].setdefault("dev_lead", "") data["roles"].setdefault("qa_lead", "") data["stats"]["messages"] = len(data["chain"]) data["stats"]["actions"] = len(data["actions"]) data["stats"]["decisions"] = len(data["decisions"]) return data # --- Dashboard Rendering (uses CSS variables for theme support) --- def render_dashboard(data): css = """ """ stats_html = f'
{data["stats"]["messages"]}
Messages
{data["stats"]["actions"]}
Actions
{data["stats"]["decisions"]}
Decisions
{data["stats"]["health"]}%
Health
' chain_html = '
Conversation Chain
' for msg in data['chain']: initials = "".join([n[0] for n in msg['user'].split()])[:2].upper() tag = msg.get("tag", "None") tag_class = f"tag-{tag.lower()}" if tag.lower() not in ["none", ""] else "" tag_html = f'
^ {tag}
' if tag_class else "" chain_html += f'
{initials}
{msg["user"]} ({msg.get("role","")}){msg.get("time","")}
{msg["text"]}
{tag_html}
' chain_html += "
" topics_html = '
Topics
' for t in data['topics']: topics_html += f'# {t}' topics_html += "
" health_html = f'
Sprint Health
โœ“ {data["health_status"]}
' actions_html = '
Actions
' for a in data['actions']: owner = a.get("owner", "Unassigned") actions_html += f'
โšก
{a["text"]} @{owner}
' if not data['actions']: actions_html += '
No actions detected
' actions_html += '
' decisions_html = '
Decisions
' for d in data['decisions']: owner = d.get("owner", "Unassigned") decisions_html += f'
๐Ÿ“Œ
{d["text"]} @{owner}
' if not data['decisions']: decisions_html += '
No decisions detected
' decisions_html += '
' risks_html = '
Risks & Blockers
' for r in data['risks']: risks_html += f'
๐Ÿšฉ
{r}
' if not data['risks']: risks_html += '
No risks detected
' risks_html += '
' return f'
{css}{stats_html}{chain_html}{topics_html}{health_html}
{actions_html}{decisions_html}{risks_html}
' # --- Main Analysis Flow --- def analyze_conversation(text, progress=gr.Progress()): if not text.strip(): return None, "", "", "", "", "" lines = parse_transcript_lines(text) if not lines: return ( None, "

Parse Error

Could not parse transcript lines.

", "", "", "", "" ) query_sample = text[:800] try: results = collection.query(query_texts=[query_sample], n_results=3) context = "\n".join(results["documents"][0]) if results and results.get("documents") and results["documents"] else "" except Exception as e: context = "" print(f"Chroma query error: {e}") all_results = [] chunks = list(chunk_lines(lines, max_per_chunk=35)) total_chunks = len(chunks) for idx, chunk in enumerate(chunks): progress((idx + 0.5) / total_chunks, desc=f"Analyzing chunk {idx+1}/{total_chunks}...") chunk_text = lines_to_text(chunk) result = analyze_chunk(chunk_text, context) if result: all_results.append(result) progress((idx + 1) / total_chunks, desc=f"Chunk {idx+1}/{total_chunks} done") if not all_results: return ( None, """

Analysis Failed

Could not analyze the conversation. The input may be too long, malformed, or the API is unavailable.

Tip: Try a shorter segment or verify your GROQ_API_KEY.

""", "", "", "", "" ) data = merge_analyses(all_results) data = apply_fallbacks(data) dashboard = render_dashboard(data) return ( data, dashboard, data["roles"]["decision_maker"], data["roles"]["facilitator"], data["roles"]["dev_lead"], data["roles"]["qa_lead"] ) # --- Export --- def export_report(data_state): if not data_state: return None html_content = render_dashboard(data_state) filename = f"scrumlens_report_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.html" with open(filename, "w", encoding="utf-8") as f: f.write(f""" ScrumLens Report {html_content}""") return filename # --- Theme Toggle JS --- THEME_TOGGLE_JS = """ () => { const body = document.body; const isLight = body.classList.contains('light-mode'); if (isLight) { body.classList.remove('light-mode'); return 'โ˜€๏ธ Light Mode'; } else { body.classList.add('light-mode'); return '๐ŸŒ™ Dark Mode'; } } """ # --- Gradio UI --- APP_CSS = """ :root { --sl-bg: #0b0f1a; --sl-card: #111827; --sl-border: #1e293b; --sl-text: #f1f5f9; --sl-text-sec: #cbd5e1; --sl-muted: #64748b; --sl-accent: #00d4aa; --sl-purple: #8b5cf6; --sl-blue: #3b82f6; --sl-red: #ef4444; --sl-orange: #f59e0b; } body.light-mode { --sl-bg: #ffffff; --sl-card: #f8fafc; --sl-border: #e2e8f0; --sl-text: #0f172a; /* dark text */ --sl-text-sec: #334155; /* dark secondary text */ --sl-muted: #64748b; --sl-accent: #059669; --sl-purple: #7c3aed; --sl-blue: #2563eb; --sl-red: #dc2626; --sl-orange: #d97706; } .gradio-container { background-color: var(--sl-bg) !important; color: var(--sl-text) !important; /* <-- make sure !important is here */ transition: background-color 0.3s ease, color 0.3s ease; } .gradio-container textarea, .gradio-container input { background-color: var(--sl-card) !important; color: var(--sl-text) !important; border-color: var(--sl-border) !important; } .gradio-container label { color: var(--sl-text-sec) !important; } .gradio-container button.primary { background: linear-gradient(135deg, var(--sl-accent), var(--sl-blue)) !important; color: white !important; border: none !important; } .gradio-container button.secondary { background: var(--sl-card) !important; color: var(--sl-text) !important; border: 1px solid var(--sl-border) !important; } /* Force ALL text inside Gradio to use the theme color */ .gradio-container, .gradio-container * { color: var(--sl-text) !important; } .gradio-container button, .gradio-container a { color: inherit !important; } .gradio-container .footer-wrap { text-align: center; padding: 24px 16px; margin-top: 24px; border-top: 1px solid var(--sl-border); color: var(--sl-muted); font-size: 0.75rem; line-height: 1.6; transition: all 0.3s ease; } .gradio-container .footer-wrap a { color: var(--sl-accent); text-decoration: none; } .gradio-container .footer-wrap a:hover { text-decoration: underline; } .gradio-container .header-row { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; } """ with gr.Blocks(theme=gr.themes.Soft(), css=APP_CSS) as demo: data_state = gr.State() with gr.Row(elem_classes="header-row"): gr.Markdown("# ๐Ÿ” ScrumLens v0.5\n### CHAOS 2 CLARITY | Long-input ready") theme_btn = gr.Button("โ˜€๏ธ Light Mode", size="sm", variant="secondary") with gr.Row(): with gr.Column(scale=4): input_text = gr.Textbox( label="โ— PASTE CONVERSATION", placeholder="[10:15 AM] Raj: Let's delay the release...", lines=12 ) with gr.Row(): analyze_btn = gr.Button("๐Ÿ” Analyze", variant="primary") export_btn = gr.Button("๐Ÿ“„ Export HTML Report", variant="secondary") with gr.Column(scale=2): gr.Markdown("โ— TEAM ROLES & FOCUS") with gr.Row(): dm_box = gr.Textbox(label="DECISION MAKER", interactive=False) fa_box = gr.Textbox(label="FACILITATOR", interactive=False) with gr.Row(): dl_box = gr.Textbox(label="DEV LEAD", interactive=False) ql_box = gr.Textbox(label="QA LEAD", interactive=False) output_html = gr.HTML(label="Analysis Results") export_file = gr.File(label="Download Report") # Theme toggle (pure JS โ€” no Python round-trip needed) theme_btn.click(fn=None, inputs=None, outputs=[theme_btn], js=THEME_TOGGLE_JS) analyze_btn.click( fn=analyze_conversation, inputs=[input_text], outputs=[data_state, output_html, dm_box, fa_box, dl_box, ql_box] ) export_btn.click( fn=export_report, inputs=[data_state], outputs=[export_file] ) # Footer gr.Markdown(""" """) if __name__ == "__main__": demo.launch()