Spaces:
Sleeping
Sleeping
| import os | |
| import html | |
| import streamlit as st | |
| import pandas as pd | |
| from atlassian import Jira | |
| import requests | |
| from openai import OpenAI | |
| from datetime import date, timedelta | |
| # ------------------------- | |
| # Environment-based secrets | |
| # ------------------------- | |
| JIRA_URL = os.getenv("JIRA_URL") | |
| JIRA_USERNAME = os.getenv("JIRA_USERNAME") | |
| JIRA_API_TOKEN = os.getenv("JIRA_API_TOKEN") | |
| ZENDESK_EMAIL = os.getenv("ZENDESK_EMAIL") | |
| ZENDESK_SUBDOMAIN = os.getenv("ZENDESK_SUBDOMAIN") | |
| ZENDESK_API_KEY = os.getenv("ZENDESK_API_KEY") | |
| OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") | |
| client = OpenAI(api_key=OPENAI_API_KEY) | |
| # ------------------------- | |
| # JIRA Client | |
| # ------------------------- | |
| jira = Jira(url=JIRA_URL, username=JIRA_USERNAME, password=JIRA_API_TOKEN) | |
| # ------------------------- | |
| # OpenAI Summarization | |
| # ------------------------- | |
| def summarize_ticket(text: str) -> str: | |
| if not text: | |
| return "No description" | |
| prompt = ( | |
| "Summarize this Zendesk ticket in 1–3 sentences:\n\n" + text + "\n\nSummary:" | |
| ) | |
| resp = client.chat.completions.create( | |
| model="gpt-4o-mini", | |
| messages=[{"role": "user", "content": prompt}], | |
| temperature=0.3, | |
| max_tokens=150, | |
| ) | |
| return resp.choices[0].message.content.strip() | |
| # ------------------------- | |
| # Zendesk Search Function | |
| # ------------------------- | |
| def search_zendesk_tickets(site_name: str, keyword: str) -> pd.DataFrame: | |
| terms = [] | |
| if site_name: | |
| terms.append(f'"{site_name}"') | |
| if keyword: | |
| terms.append(f'"{keyword}"') | |
| query_str = " ".join(terms) | |
| url = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/search.json" | |
| params = {"query": f"type:ticket {query_str}", "include": "users"} | |
| auth = (f"{ZENDESK_EMAIL}/token", ZENDESK_API_KEY) | |
| resp = requests.get(url, auth=auth, params=params) | |
| if not resp.ok: | |
| st.error(f"Zendesk error {resp.status_code}") | |
| return pd.DataFrame() | |
| tickets = resp.json().get("results", []) | |
| rows = [] | |
| for t in tickets: | |
| rows.append( | |
| { | |
| "ID": t["id"], | |
| "Subject": html.escape(t.get("subject", "")), | |
| "Status": html.escape(t.get("status", "")), | |
| "Created At": t.get("created_at", ""), | |
| "Updated At": t.get("updated_at", ""), | |
| "Description": t.get("description", ""), # keep for summary | |
| } | |
| ) | |
| df = pd.DataFrame(rows) | |
| # generate summaries and attach as new column | |
| df["OpenAI Ticket Summary"] = df["Description"].apply(summarize_ticket) | |
| return df | |
| # ------------------------- | |
| # Jira Search Function | |
| # ------------------------- | |
| def search_jira_issues( | |
| site_name: str, keyword: str, start_date: date, end_date: date | |
| ) -> pd.DataFrame: | |
| # Build JQL clauses | |
| clauses = [] | |
| if site_name: | |
| clauses.append(f'text ~ "{site_name}"') | |
| if keyword: | |
| clauses.append(f'text ~ "{keyword}"') | |
| clauses.append(f'created >= "{start_date.isoformat()}"') | |
| clauses.append(f'created <= "{end_date.isoformat()}"') | |
| jql = " AND ".join(clauses) | |
| # Execute the JQL query, limiting to 100 issues | |
| resp = jira.jql(jql, limit=100) | |
| issues = resp.get("issues", []) | |
| rows = [] | |
| for issue in issues: | |
| f = issue["fields"] | |
| rows.append( | |
| { | |
| "Key": issue["key"], | |
| "Summary": html.escape(f.get("summary", "")), | |
| "Status": html.escape(f.get("status", {}).get("name", "")), | |
| "Created At": f.get("created", ""), | |
| "Updated At": f.get("updated", ""), | |
| } | |
| ) | |
| return pd.DataFrame(rows) | |
| # ------------------------- | |
| # App Config | |
| # ------------------------- | |
| st.set_page_config(layout="wide") | |
| st.title("Unified Support Dashboard") | |
| if "zendesk_df" not in st.session_state: | |
| st.session_state.zendesk_df = pd.DataFrame() | |
| if "jira_df" not in st.session_state: | |
| st.session_state.jira_df = pd.DataFrame() | |
| # ------------------------- | |
| # Main: Tabs | |
| # ------------------------- | |
| tabs = st.tabs(["Zendesk Lookup", "Jira Lookup"]) | |
| # ---- Tab 1: Zendesk ---- | |
| with tabs[0]: | |
| st.header("Zendesk Lookup") | |
| site_input = st.text_input( | |
| "Site Name", placeholder="example.com", key="zendesk_site" | |
| ) | |
| keyword_input = st.text_input( | |
| "Keyword", placeholder="timeout", key="zendesk_keyword" | |
| ) | |
| start_input = st.date_input( | |
| "Created After", value=date.today() - timedelta(days=7), key="zendesk_start" | |
| ) | |
| end_input = st.date_input("Created Before", value=date.today(), key="zendesk_end") | |
| if st.button("Search Zendesk Tickets", key="zendesk_search"): | |
| st.session_state.zendesk_df = search_zendesk_tickets(site_input, keyword_input) | |
| df_z = st.session_state.zendesk_df.copy() | |
| if not df_z.empty: | |
| # parse & filter dates | |
| df_z["Created At"] = pd.to_datetime(df_z["Created At"]) | |
| df_z["Updated At"] = pd.to_datetime(df_z["Updated At"]) | |
| mask = (df_z["Created At"].dt.date >= start_input) & ( | |
| df_z["Created At"].dt.date <= end_input | |
| ) | |
| df_z = df_z.loc[mask] | |
| # sort by Created At descending | |
| df_z = df_z.sort_values("Created At", ascending=False) | |
| # format timestamps 12-hour | |
| df_z["Created At"] = ( | |
| df_z["Created At"].dt.strftime("%Y-%m-%d %I:%M %p").str.lower() | |
| ) | |
| df_z["Updated At"] = ( | |
| df_z["Updated At"].dt.strftime("%Y-%m-%d %I:%M %p").str.lower() | |
| ) | |
| # hyperlink ID | |
| base_url = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com/agent/tickets" | |
| df_z["ID"] = df_z["ID"].apply( | |
| lambda x: f'<a href="{base_url}/{x}" target="_blank">{x}</a>' | |
| ) | |
| # render fixed-height table including the new summary column | |
| html_tbl = df_z.to_html( | |
| index=False, | |
| escape=False, | |
| columns=[ | |
| "ID", | |
| "Subject", | |
| "Status", | |
| "Created At", | |
| "Updated At", | |
| "OpenAI Ticket Summary", | |
| ], | |
| ) | |
| scrollable = f""" | |
| <div style="height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 4px;"> | |
| {html_tbl} | |
| </div> | |
| """ | |
| st.markdown(scrollable, unsafe_allow_html=True) | |
| # ---- Tab 2: Jira ---- | |
| with tabs[1]: | |
| st.header("Jira Lookup") | |
| site_input = st.text_input("Site Name", placeholder="example.com", key="jira_site") | |
| keyword_input = st.text_input("Keyword", placeholder="timeout", key="jira_keyword") | |
| start_input = st.date_input( | |
| "Created After", value=date.today() - timedelta(days=7), key="jira_start" | |
| ) | |
| end_input = st.date_input("Created Before", value=date.today(), key="jira_end") | |
| if st.button("Search Jira Issues", key="jira_search"): | |
| st.session_state.jira_df = search_jira_issues( | |
| site_input, keyword_input, start_input, end_input | |
| ) | |
| df_j = st.session_state.jira_df.copy() | |
| if not df_j.empty: | |
| # parse & sort by Created At descending | |
| df_j["Created At"] = pd.to_datetime(df_j["Created At"]) | |
| df_j["Updated At"] = pd.to_datetime(df_j["Updated At"]) | |
| df_j = df_j.sort_values("Created At", ascending=False) | |
| # 12-hour fmt with am/pm | |
| df_j["Created At"] = ( | |
| df_j["Created At"].dt.strftime("%Y-%m-%d %I:%M %p").str.lower() | |
| ) | |
| df_j["Updated At"] = ( | |
| df_j["Updated At"].dt.strftime("%Y-%m-%d %I:%M %p").str.lower() | |
| ) | |
| # hyperlink the key to the JIRA issue | |
| base_jira = JIRA_URL.rstrip("/") | |
| df_j["Key"] = df_j["Key"].apply( | |
| lambda k: f'<a href="{base_jira}/browse/{k}" target="_blank">{k}</a>' | |
| ) | |
| # render as fixed-height, scrollable HTML table | |
| html_tbl = df_j.to_html( | |
| index=False, | |
| escape=False, | |
| columns=["Key", "Summary", "Status", "Created At", "Updated At"], | |
| ) | |
| scrollable = f""" | |
| <div style="height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 4px;"> | |
| {html_tbl} | |
| </div> | |
| """ | |
| st.markdown(scrollable, unsafe_allow_html=True) | |