Spaces:
Sleeping
Sleeping
Download web_app/app.py from Shivam4096/Financial2API: direct link, hf CLI and curl.
- Browser
- Download file 27.4 kB
-
https://huggingface.co/spaces/Shivam4096/Financial2API/resolve/main/web_app/app.py
- Command line
-
hf download hf://spaces/Shivam4096/Financial2API/web_app/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Shivam4096/Financial2API/resolve/main/web_app/app.py
27.4 kB
| """ | |
| web_app/app.py -- Cloud-hosted Streamlit Portal for Financial Analysis. | |
| Provides a rich dark UI dashboard to scrape, compute, analyze and download reports | |
| for Excel, Word, and PowerPoint formats using user-supplied LLM credentials. | |
| """ | |
| import os | |
| import sys | |
| import time | |
| import datetime | |
| from pathlib import Path | |
| import streamlit as st | |
| from dotenv import load_dotenv | |
| # Load environment variables early so keys can be hidden from UI | |
| _ENV_PATH = Path(__file__).parent.parent / ".env" | |
| load_dotenv(_ENV_PATH) | |
| # Setup sys.path so local packages can be resolved from parent directory | |
| PARENT_DIR = Path(__file__).parent.parent | |
| sys.path.insert(0, str(PARENT_DIR)) | |
| # Import builders and pipeline | |
| from pipeline.resolver import resolve | |
| from pipeline.fetcher import fetch_all | |
| from pipeline.peer_selector import select_peers | |
| from pipeline.calculator import calculate | |
| from pipeline.analyst import generate_narrative | |
| from builders.build_excel import build_workbook | |
| from builders.build_docx import build_document | |
| from builders.build_pptx import build_presentation | |
| from web_app.database import save_audit_log, get_audit_logs | |
| # Streamlit Page Config | |
| st.set_page_config( | |
| page_title="Financial Statement Analyzer Cloud Portal", | |
| page_icon="π", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Dark Theme Glassmorphism Custom CSS Injection | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap'); | |
| html, body, [class*="css"] { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| .main { | |
| background-color: #0A0F1D; | |
| color: #F3F4F6; | |
| } | |
| /* Sidebar styling */ | |
| [data-testid="stSidebar"] { | |
| background-color: #0F162A; | |
| border-right: 1px solid #1E293B; | |
| } | |
| /* Title and Subtitle */ | |
| .dashboard-title { | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, #60A5FA, #34D399); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 0.2rem; | |
| } | |
| .dashboard-subtitle { | |
| font-size: 1.1rem; | |
| font-weight: 300; | |
| color: #9CA3AF; | |
| margin-bottom: 2rem; | |
| } | |
| /* Glowing card containers */ | |
| .glass-card { | |
| background: rgba(30, 41, 59, 0.45); | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| backdrop-filter: blur(12px); | |
| margin-bottom: 1.5rem; | |
| box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); | |
| } | |
| .status-log { | |
| background-color: #0D1117 !important; | |
| border: 1px solid #1F242C !important; | |
| border-radius: 8px !important; | |
| color: #C9D1D9 !important; | |
| font-family: 'Consolas', monospace !important; | |
| padding: 12px; | |
| max-height: 380px; | |
| overflow-y: auto; | |
| } | |
| /* Primary buttons styling */ | |
| div.stButton > button:first-child { | |
| background: linear-gradient(135deg, #3B82F6, #10B981); | |
| border: none; | |
| color: white; | |
| font-weight: 600; | |
| padding: 0.6rem 2rem; | |
| border-radius: 8px; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| box-shadow: 0 0 15px rgba(59, 130, 246, 0.4); | |
| } | |
| div.stButton > button:first-child:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 0 25px rgba(16, 185, 129, 0.6); | |
| color: white; | |
| } | |
| /* Checkbox layouts styling */ | |
| .stCheckbox > label { | |
| color: #D1D5DB !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # -- Sidebar -- Connect Your AI ------------------------------------------ | |
| st.sidebar.markdown(""" | |
| <h2 style='color:#60A5FA;margin-top:0;'>π€ Connect Your AI</h2> | |
| <p style='font-size:0.8rem;color:#9CA3AF;'> | |
| Bring your own key. Keys live only in your browser session -- never stored on the server. | |
| </p> | |
| """, unsafe_allow_html=True) | |
| st.sidebar.markdown("---") | |
| st.sidebar.markdown("<h3 style='color:#34D399;margin-top:0;'>π€ User Profile</h3>", unsafe_allow_html=True) | |
| username = st.sidebar.text_input("Username / Email", value="", placeholder="name@company.com", help="Mandatory for audit trail") | |
| st.sidebar.markdown("---") | |
| # -- FREE OPTIONS ------------------------------------------------------- | |
| st.sidebar.markdown(""" | |
| <div style='background:rgba(16,185,129,0.08);border:1px solid rgba(16,185,129,0.25); | |
| border-radius:8px;padding:0.6rem 0.85rem;margin-bottom:0.5rem;'> | |
| <span style='font-size:0.75rem;font-weight:700;color:#34D399;letter-spacing:0.06em;'>π FREE -- NO CREDIT CARD</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.sidebar.markdown(""" | |
| **NVIDIA NIM** *(Llama 3.1 405B / Nemotron)* | |
| <a href='https://build.nvidia.com/explore/discover' target='_blank' style='font-size:0.75rem;color:#34D399;'>Get free key -> build.nvidia.com</a> | |
| """, unsafe_allow_html=True) | |
| nvidia_key = st.sidebar.text_input("NVIDIA API Key", value=os.environ.get("NVIDIA_API_KEY", ""), type="password", placeholder="nvapi-...", key="inp_nvidia") | |
| st.sidebar.markdown(""" | |
| **Groq** *(Llama 3.3 70B -- fast & free)* | |
| <a href='https://console.groq.com/keys' target='_blank' style='font-size:0.75rem;color:#34D399;'>Get free key -> console.groq.com</a> | |
| """, unsafe_allow_html=True) | |
| groq_key = st.sidebar.text_input("Groq API Key", value=os.environ.get("GROQ_API_KEY", ""), type="password", placeholder="gsk_...", key="inp_groq") | |
| st.sidebar.markdown(""" | |
| **Google Gemini** *(Gemini 2.0 Flash -- free tier)* | |
| <a href='https://aistudio.google.com/app/apikey' target='_blank' style='font-size:0.75rem;color:#34D399;'>Get free key -> aistudio.google.com</a> | |
| """, unsafe_allow_html=True) | |
| gemini_key = st.sidebar.text_input("Gemini API Key", value=os.environ.get("GEMINI_API_KEY", ""), type="password", placeholder="AIza...", key="inp_gemini") | |
| st.sidebar.markdown(""" | |
| **Hugging Face** *(Serverless API)* | |
| <a href='https://huggingface.co/settings/tokens' target='_blank' style='font-size:0.75rem;color:#34D399;'>Get free key -> huggingface.co</a> | |
| """, unsafe_allow_html=True) | |
| hf_key = st.sidebar.text_input("Hugging Face API Key", value=os.environ.get("HF_API_KEY", ""), type="password", placeholder="hf_...", key="inp_hf") | |
| # -- PAID OPTIONS ------------------------------------------------------- | |
| st.sidebar.markdown(""" | |
| <div style='background:rgba(99,102,241,0.08);border:1px solid rgba(99,102,241,0.25); | |
| border-radius:8px;padding:0.6rem 0.85rem;margin:0.5rem 0;'> | |
| <span style='font-size:0.75rem;font-weight:700;color:#818CF8;letter-spacing:0.06em;'>π³ OPTIONAL -- PAID APIS</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with st.sidebar.expander("Claude (Anthropic) -- paid", expanded=False): | |
| st.markdown("<a href='https://console.anthropic.com/' target='_blank' style='font-size:0.75rem;color:#818CF8;'>Get key -> console.anthropic.com</a>", unsafe_allow_html=True) | |
| claude_key = st.text_input("Claude API Key", value=os.environ.get("CLAUDE_API_KEY", ""), type="password", placeholder="sk-ant-...", key="inp_claude") | |
| with st.sidebar.expander("ChatGPT (OpenAI) -- paid", expanded=False): | |
| st.markdown("<a href='https://platform.openai.com/api-keys' target='_blank' style='font-size:0.75rem;color:#818CF8;'>Get key -> platform.openai.com</a>", unsafe_allow_html=True) | |
| openai_key = st.text_input("OpenAI API Key", value=os.environ.get("OPENAI_API_KEY", ""), type="password", placeholder="sk-proj-...", key="inp_openai") | |
| st.sidebar.markdown("---") | |
| # -- Main Dashboard Layout ---------------------------------------------- | |
| st.markdown("<h1 class='dashboard-title'>π Financial Statement Analyzer</h1>", unsafe_allow_html=True) | |
| st.markdown("<p class='dashboard-subtitle'>Multi-model AI insights scraper and report compilation portal</p>", unsafe_allow_html=True) | |
| st.subheader("Analysis Parameters") | |
| # -- Ticker Search & Suggestion Logic -- | |
| search_col1, search_col2 = st.columns([3, 1]) | |
| with search_col1: | |
| search_query = st.text_input("Search Company Name or Ticker", placeholder="e.g. TCS, Reliance, AAPL, MSFT") | |
| with search_col2: | |
| search_clicked = st.button("π Find", use_container_width=True) | |
| ticker_input = search_query # default if no suggestions | |
| if search_clicked and search_query: | |
| with st.spinner("Searching..."): | |
| try: | |
| import yahooquery as yq | |
| res = yq.search(search_query) | |
| quotes = [q for q in res.get("quotes", []) if q.get("quoteType") in ("EQUITY", "MUTUALFUND", "ETF")] | |
| st.session_state["ticker_suggestions"] = quotes | |
| except Exception: | |
| st.session_state["ticker_suggestions"] = [] | |
| if "ticker_suggestions" in st.session_state and st.session_state["ticker_suggestions"]: | |
| options = {} | |
| for q in st.session_state["ticker_suggestions"]: | |
| name = q.get('longname') or q.get('shortname') or q.get('symbol') | |
| label = f"{name} ({q['symbol']}) - {q.get('exchange', 'Unknown')}" | |
| options[label] = q['symbol'] | |
| options[f"Use exact input: '{search_query}'"] = search_query | |
| selected_label = st.selectbox("Confirm Company:", list(options.keys())) | |
| ticker_input = options[selected_label] | |
| col_years, col_refresh = st.columns([1, 1]) | |
| with col_years: | |
| analysis_years = st.selectbox("Analysis Horizon", options=[5, 7, 10], index=0) | |
| with col_refresh: | |
| force_refresh = st.checkbox("Bypass Cache (Force Scrape)", value=False) | |
| st.markdown("<b>Download Formats:</b>", unsafe_allow_html=True) | |
| chk_excel = st.checkbox("Excel Analysis Workbook (.xlsx)", value=True) | |
| chk_word = st.checkbox("Word Summary Report (.docx)", value=True) | |
| chk_pptx = st.checkbox("PowerPoint Pitch Deck (.pptx)", value=True) | |
| # -- AI Engine selection -- auto-disable if key not supplied | |
| st.markdown("<b>AI Engines:</b>", unsafe_allow_html=True) | |
| _hint = "<span style='font-size:0.72rem;color:#9CA3AF;'>(add key in sidebar ->)</span>" | |
| engines = [] | |
| if st.checkbox( | |
| "π NVIDIA NIM -- Llama 3.1 405B", | |
| value=bool(nvidia_key), | |
| disabled=not nvidia_key, | |
| help="Free credits. Get key at build.nvidia.com" | |
| ): | |
| engines.append("nvidia") | |
| if st.checkbox( | |
| "π Groq -- Llama 3.3 70B", | |
| value=bool(groq_key), | |
| disabled=not groq_key, | |
| help="Free. Get key at console.groq.com" | |
| ): | |
| engines.append("groq") | |
| if st.checkbox( | |
| "π Hugging Face -- DeepSeek-R1", | |
| value=bool(hf_key), | |
| disabled=not hf_key, | |
| help="Free via Hugging Face Inference API" | |
| ): | |
| engines.append("huggingface") | |
| if st.checkbox( | |
| "π Gemini 2.0 Flash (Google)", | |
| value=bool(gemini_key), | |
| disabled=not gemini_key, | |
| help="Free tier. Get key at aistudio.google.com" | |
| ): | |
| engines.append("gemini") | |
| if st.checkbox( | |
| "π³ Claude Haiku (Anthropic)", | |
| value=False, | |
| disabled=not claude_key, | |
| help="Paid. Add key in sidebar to enable." | |
| ): | |
| engines.append("claude") | |
| if st.checkbox( | |
| "π³ ChatGPT -- GPT-4o mini (OpenAI)", | |
| value=False, | |
| disabled=not openai_key, | |
| help="Paid. Add key in sidebar to enable." | |
| ): | |
| engines.append("openai") | |
| if not groq_key and not gemini_key: | |
| st.info("π‘ Add a **free Groq key** in the sidebar to enable AI narratives.", icon="π") | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| start_btn = st.button("Begin Financial Scrape & Analysis") | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| progress_bar_container = st.empty() | |
| status_container = st.empty() | |
| metrics_container = st.empty() | |
| download_container = st.empty() | |
| # -- Analysis Runner --------------------------------------------------- | |
| if start_btn: | |
| if not username or "@" not in username: | |
| st.error("Please enter a valid email address in the User Profile section before analyzing.") | |
| elif not ticker_input: | |
| st.error("Please enter a stock ticker or company name.") | |
| elif not (chk_excel or chk_word or chk_pptx): | |
| st.error("Please select at least one download format checkbox.") | |
| else: | |
| # Inject credentials into all modules that cache them at import time | |
| import config | |
| import pipeline.analyst | |
| import pipeline.key_validator | |
| config.GEMINI_API_KEY = gemini_key | |
| config.CLAUDE_API_KEY = claude_key | |
| config.OPENAI_API_KEY = openai_key | |
| config.GROQ_API_KEY = groq_key | |
| config.NVIDIA_API_KEY = nvidia_key | |
| config.HF_API_KEY = hf_key | |
| os.environ["GEMINI_API_KEY"] = gemini_key | |
| os.environ["CLAUDE_API_KEY"] = claude_key | |
| os.environ["OPENAI_API_KEY"] = openai_key | |
| os.environ["NVIDIA_API_KEY"] = nvidia_key | |
| os.environ["HF_API_KEY"] = hf_key | |
| pipeline.analyst.GEMINI_API_KEY = gemini_key | |
| pipeline.analyst.CLAUDE_API_KEY = claude_key | |
| pipeline.analyst.OPENAI_API_KEY = openai_key | |
| pipeline.analyst.GROQ_API_KEY = groq_key | |
| pipeline.analyst.NVIDIA_API_KEY = nvidia_key | |
| pipeline.key_validator.GEMINI_API_KEY = gemini_key | |
| pipeline.key_validator.CLAUDE_API_KEY = claude_key | |
| pipeline.key_validator.OPENAI_API_KEY = openai_key | |
| pipeline.key_validator.GROQ_API_KEY = groq_key | |
| pipeline.key_validator.NVIDIA_API_KEY = nvidia_key | |
| # Push into environment for any sub-process | |
| for k, v in [("GEMINI_API_KEY", gemini_key), ("CLAUDE_API_KEY", claude_key), | |
| ("OPENAI_API_KEY", openai_key), ("GROQ_API_KEY", groq_key), | |
| ("NVIDIA_API_KEY", nvidia_key), ("HF_API_KEY", hf_key)]: | |
| if v: os.environ[k] = v | |
| # Validate that selected engines have keys | |
| failed_validation = False | |
| engine_key_map = { | |
| "nvidia": (nvidia_key, "NVIDIA"), | |
| "groq": (groq_key, "Groq"), | |
| "gemini": (gemini_key, "Gemini"), | |
| "claude": (claude_key, "Claude"), | |
| "openai": (openai_key, "ChatGPT"), | |
| "huggingface": (hf_key, "Hugging Face"), | |
| } | |
| for engine in engines: | |
| key, label = engine_key_map[engine] | |
| if not key: | |
| st.error(f"Please supply a {label} API Key or uncheck the {label} model.") | |
| failed_validation = True | |
| if not failed_validation: | |
| try: | |
| progress_bar = progress_bar_container.progress(0, text="Initializing analysis...") | |
| with status_container.status("Analysis Execution Status", expanded=True) as status_box: | |
| def log_fn(msg): | |
| status_box.write(msg) | |
| log_fn(f"Starting analysis for: '{ticker_input}'") | |
| log_fn(f"Time: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") | |
| # 1. Resolve Company Info | |
| progress_bar.progress(14, text="[1/7] Resolving company metadata...") | |
| log_fn("\n[1/7] Resolving company metadata ...") | |
| meta = resolve(ticker_input, log_fn=log_fn) | |
| log_fn(f" [OK] {meta.name} | {meta.ticker} | {meta.exchange}") | |
| # 2. Fetch Statements | |
| progress_bar.progress(28, text="[2/7] Scrape financial statements...") | |
| log_fn("\n[2/7] Scrape financial statements ...") | |
| fin_data = fetch_all(meta, force_refresh=force_refresh, log_fn=log_fn) | |
| # 3. Peers Comparison Selection | |
| progress_bar.progress(42, text="[3/7] Benchmarking peer selection...") | |
| log_fn("\n[3/7] Benchmarking peer selection ...") | |
| fin_data["peers"] = select_peers(meta, log_fn=log_fn) | |
| # 4. Math ratios calculations | |
| progress_bar.progress(57, text="[4/7] Computing financial ratios...") | |
| log_fn("\n[4/7] Computing financial ratios & DuPont decomposition ...") | |
| fin_data = calculate(fin_data, meta.to_dict()) | |
| log_fn(" [OK] Completed ratio math computations") | |
| # 5. AI Synthesis Narrative | |
| progress_bar.progress(71, text="[5/7] Requesting AI Model narrative...") | |
| log_fn("\n[5/7] Requesting AI Model narrative generation ...") | |
| narrative = generate_narrative(fin_data, meta.to_dict(), engines, log_fn=log_fn) | |
| fin_data.update(narrative) | |
| # 6. Generate outputs | |
| progress_bar.progress(85, text="[6/7] Building output report files...") | |
| log_fn("\n[6/7] Building output report files ...") | |
| fin_data["company"] = { | |
| "name": meta.name, "ticker": meta.ticker, "exchange": meta.exchange, | |
| "currency": fin_data.get("currency", meta.currency), | |
| "units": fin_data.get("units", "millions"), | |
| "fiscal_year_end": "", | |
| "industry": meta.industry, "sector": meta.sector, | |
| "business_description": meta.description, | |
| "report_date": datetime.date.today().isoformat(), | |
| "analysis_period_years": len(fin_data.get("years", [])), | |
| "share_price": meta.share_price, "market_cap": meta.mkt_cap, | |
| } | |
| fin_data["models_used"] = engines | |
| fin_data.setdefault("sources", []) | |
| ticker_safe = meta.ticker.replace(".", "_").replace("/", "_") | |
| timestamp_suffix = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") | |
| # Exporters path settings | |
| excel_bytes = None | |
| word_bytes = None | |
| pptx_bytes = None | |
| out_dir = PARENT_DIR / "outputs" | |
| os.makedirs(str(out_dir), exist_ok=True) | |
| # Build files in memory / disk | |
| if chk_excel: | |
| xlsx_path = out_dir / f"{ticker_safe}_financial_analysis_{timestamp_suffix}.xlsx" | |
| build_workbook(fin_data, str(xlsx_path)) | |
| with open(xlsx_path, "rb") as f: | |
| excel_bytes = f.read() | |
| log_fn(f" [OK] Created Excel analysis: {xlsx_path.name}") | |
| if chk_word: | |
| docx_path = out_dir / f"{ticker_safe}_financial_analysis_{timestamp_suffix}.docx" | |
| build_document(fin_data, str(docx_path)) | |
| with open(docx_path, "rb") as f: | |
| word_bytes = f.read() | |
| log_fn(f" [OK] Created Word summary: {docx_path.name}") | |
| if chk_pptx: | |
| pptx_path = out_dir / f"{ticker_safe}_financial_analysis_{timestamp_suffix}.pptx" | |
| build_presentation(fin_data, str(pptx_path)) | |
| with open(pptx_path, "rb") as f: | |
| pptx_bytes = f.read() | |
| log_fn(f" [OK] Created PowerPoint Presentation: {pptx_path.name}") | |
| progress_bar.progress(100, text="[7/7] Financial analysis process complete!") | |
| log_fn(f"\n[7/7] β Financial analysis process complete!") | |
| status_box.update(state="complete", expanded=False) | |
| # -- Display Success Metrics | |
| st.toast('Analysis successfully completed!', icon='π') | |
| with metrics_container.container(): | |
| st.markdown("### π Quick Insights", unsafe_allow_html=True) | |
| mc1, mc2, mc3, mc4 = st.columns(4) | |
| mc1.metric("Company", meta.name, meta.ticker) | |
| # Format Price | |
| price_val = meta.share_price | |
| price_str = f"{fin_data.get('currency', '')} {price_val:,.2f}" if price_val else "N/A" | |
| mc2.metric("Share Price", price_str) | |
| # Format Market Cap | |
| mc_val = meta.mkt_cap | |
| mc_str = "N/A" | |
| if mc_val: | |
| if mc_val > 1e12: | |
| mc_str = f"{fin_data.get('currency', '')} {mc_val/1e12:,.2f} Trillion" | |
| elif mc_val > 1e9: | |
| mc_str = f"{fin_data.get('currency', '')} {mc_val/1e9:,.2f} Billion" | |
| else: | |
| mc_str = f"{fin_data.get('currency', '')} {mc_val/1e6:,.2f} Million" | |
| mc3.metric("Market Cap", mc_str) | |
| # Data quality badge | |
| dq = fin_data.get("data_quality", 0) | |
| if dq >= 80: | |
| dq_color, dq_label = "#22c55e", "Excellent" | |
| elif dq >= 55: | |
| dq_color, dq_label = "#f59e0b", "Partial" | |
| else: | |
| dq_color, dq_label = "#ef4444", "Low" | |
| mc4.markdown( | |
| f"""<div style='background:rgba(255,255,255,0.05);border-radius:12px; | |
| padding:14px 12px;text-align:center;border:1px solid rgba(255,255,255,0.1)'> | |
| <div style='font-size:0.75rem;color:#9CA3AF;margin-bottom:4px'>Data Quality</div> | |
| <div style='font-size:1.6rem;font-weight:700;color:{dq_color}'>{dq}%</div> | |
| <div style='font-size:0.75rem;color:{dq_color}'>{dq_label}</div> | |
| </div>""", | |
| unsafe_allow_html=True | |
| ) | |
| # Source attribution | |
| src = fin_data.get("source", "") | |
| if src: | |
| st.caption(f"Data sources: {src}") | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| # -- Generate download buttons in UI | |
| with download_container.container(): | |
| st.success("Download ready!") | |
| dl_col1, dl_col2, dl_col3, dl_col4 = st.columns(4) | |
| download_files = [] | |
| if excel_bytes: | |
| dl_col1.download_button( | |
| label="π₯ Download Excel", | |
| data=excel_bytes, | |
| file_name=f"{ticker_safe}_analysis.xlsx", | |
| mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |
| use_container_width=True | |
| ) | |
| download_files.append((f"{ticker_safe}_analysis.xlsx", excel_bytes)) | |
| if word_bytes: | |
| dl_col2.download_button( | |
| label="π Download Word", | |
| data=word_bytes, | |
| file_name=f"{ticker_safe}_report.docx", | |
| mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
| use_container_width=True | |
| ) | |
| download_files.append((f"{ticker_safe}_report.docx", word_bytes)) | |
| if pptx_bytes: | |
| dl_col3.download_button( | |
| label="π Download PPTX", | |
| data=pptx_bytes, | |
| file_name=f"{ticker_safe}_deck.pptx", | |
| mime="application/vnd.openxmlformats-officedocument.presentationml.presentation", | |
| use_container_width=True | |
| ) | |
| download_files.append((f"{ticker_safe}_deck.pptx", pptx_bytes)) | |
| if len(download_files) > 1: | |
| import zipfile | |
| import io | |
| zip_buffer = io.BytesIO() | |
| with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file: | |
| for file_name, data in download_files: | |
| zip_file.writestr(file_name, data) | |
| dl_col4.download_button( | |
| label="π¦ Download All (ZIP)", | |
| data=zip_buffer.getvalue(), | |
| file_name=f"{ticker_safe}_all_reports.zip", | |
| mime="application/zip", | |
| use_container_width=True | |
| ) | |
| # Write search activity log to audit table | |
| selected_formats = [f for f, ok in [("xlsx", chk_excel), ("docx", chk_word), ("pptx", chk_pptx)] if ok] | |
| save_audit_log( | |
| user_email=username, | |
| company_ticker=meta.ticker, | |
| company_name=meta.name, | |
| selected_formats=selected_formats, | |
| ai_models=engines, | |
| status="completed" | |
| ) | |
| except Exception as e: | |
| log_fn(f"\nβ Pipeline failed: {e}") | |
| import traceback | |
| log_fn(traceback.format_exc()) | |
| st.error(f"Analysis failed: {e}") | |
| # Save audit log failure | |
| try: | |
| selected_formats = [f for f, ok in [("xlsx", chk_excel), ("docx", chk_word), ("pptx", chk_pptx)] if ok] | |
| save_audit_log( | |
| user_email=username, | |
| company_ticker=ticker_input, | |
| company_name="N/A", | |
| selected_formats=selected_formats, | |
| ai_models=engines, | |
| status="failed" | |
| ) | |
| except: | |
| pass | |
| # -- Audit History Panel Table ------------------------------------------- | |
| st.markdown("---") | |
| st.markdown("<h3 style='color:#60A5FA;margin-top:0;'>π Query Audit Trail & History</h3>", unsafe_allow_html=True) | |
| st.markdown("<p style='font-size:0.85rem;color:#9CA3AF;'>Past analysis runs requested by team members.</p>", unsafe_allow_html=True) | |
| history_logs = get_audit_logs() | |
| if history_logs: | |
| st.table(history_logs) | |
| else: | |
| st.info("No queries have been run yet.") | |