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