Shivam4096's picture
Upload folder using huggingface_hub
598ea92 verified
Raw History Blame Contribute Delete
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.")