""" 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(""" """, unsafe_allow_html=True) # -- Sidebar -- Connect Your AI ------------------------------------------ st.sidebar.markdown("""

šŸ¤– Connect Your AI

Bring your own key. Keys live only in your browser session -- never stored on the server.

""", unsafe_allow_html=True) st.sidebar.markdown("---") st.sidebar.markdown("

šŸ‘¤ User Profile

", 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("""
šŸ†“ FREE -- NO CREDIT CARD
""", unsafe_allow_html=True) st.sidebar.markdown(""" **NVIDIA NIM** *(Llama 3.1 405B / Nemotron)* Get free key -> build.nvidia.com """, 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)* Get free key -> console.groq.com """, 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)* Get free key -> aistudio.google.com """, 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)* Get free key -> huggingface.co """, 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("""
šŸ’³ OPTIONAL -- PAID APIS
""", unsafe_allow_html=True) with st.sidebar.expander("Claude (Anthropic) -- paid", expanded=False): st.markdown("Get key -> console.anthropic.com", 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("Get key -> platform.openai.com", 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("

šŸ“ˆ Financial Statement Analyzer

", unsafe_allow_html=True) st.markdown("

Multi-model AI insights scraper and report compilation portal

", 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("Download Formats:", 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("AI Engines:", unsafe_allow_html=True) _hint = "(add key in sidebar ->)" 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("
", unsafe_allow_html=True) start_btn = st.button("Begin Financial Scrape & Analysis") st.markdown("
", 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"""
Data Quality
{dq}%
{dq_label}
""", unsafe_allow_html=True ) # Source attribution src = fin_data.get("source", "") if src: st.caption(f"Data sources: {src}") st.markdown("
", 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("

šŸ“œ Query Audit Trail & History

", unsafe_allow_html=True) st.markdown("

Past analysis runs requested by team members.

", 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.")