import streamlit as st
import sys
import os
import httpx
import pandas as pd
import json
import time
from datetime import datetime
import base64
import subprocess
# --- Path Setup ---
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '.')))
# --- Configuration ---
WATCHLIST_FILE = "watchlist.json"
ALERTS_FILE = "alerts.json"
# --- Page Configuration ---
st.set_page_config(
page_title="Sentinel - AI Financial Intelligence",
page_icon="🛡️",
layout="wide",
initial_sidebar_state="expanded"
)
# --- Custom CSS ---
def load_css(file_name):
with open(file_name) as f:
st.markdown(f'', unsafe_allow_html=True)
load_css("style.css")
# --- Auto-Start Backend Services ---
# --- Auto-Start Backend Services ---
# --- Auto-Start Backend Services ---
@st.cache_resource
def start_background_services():
"""Checks if backend services are running and starts them if needed."""
# Check if Gateway is already running
try:
with httpx.Client(timeout=1.0) as client:
response = client.get("http://127.0.0.1:8000/")
if response.status_code == 200:
print("✅ Gateway is already running.")
return
except:
print("⚠️ Gateway not found. Initializing backend services...")
services = [
["mcp_gateway.py", "8000"],
["tavily_mcp.py", "8001"],
["alphavantage_mcp.py", "8002"],
["private_mcp.py", "8003"]
]
env = os.environ.copy()
# Inject secrets
try:
def flatten_secrets(secrets, prefix=""):
for key, value in secrets.items():
if isinstance(value, dict):
flatten_secrets(value, f"{prefix}{key}_")
else:
env[f"{prefix}{key}"] = str(value)
if hasattr(st, "secrets"):
flatten_secrets(st.secrets)
print("✅ Secrets injected into subprocess environment.")
except Exception as e:
print(f"⚠️ Secrets injection warning: {e}")
# Start services - NON-BLOCKING, LOG TO STDOUT
cwd = os.path.dirname(os.path.abspath(__file__))
for script, port in services:
print(f"🚀 Launching {script} on port {port}...")
# Use subprocess.Popen without waiting
subprocess.Popen(
[sys.executable, script],
cwd=cwd,
env=env,
# Inherit stdout/stderr so logs appear in Streamlit Cloud console
# stdout=subprocess.DEVNULL,
# stderr=subprocess.DEVNULL
)
print("🚀 Launching Monitor...")
subprocess.Popen(
[sys.executable, "monitor.py"],
cwd=cwd,
env=env
)
# Do NOT wait. Return immediately to let UI render.
print("✅ Background services launch triggered.")
# Trigger startup (cached, runs once per container)
start_background_services()
# --- Helper Functions ---
@st.cache_data(ttl=60)
def check_server_status():
urls = {"Gateway": "http://127.0.0.1:8000/", "Tavily": "http://127.0.0.1:8001/", "Alpha Vantage": "http://127.0.0.1:8002/", "Private DB": "http://127.0.0.1:8003/"}
statuses = {}
with httpx.Client(timeout=2.0) as client:
for name, url in urls.items():
try:
response = client.get(url)
statuses[name] = "✅ Online" if response.status_code == 200 else "⚠️ Error"
except: statuses[name] = "❌ Offline"
return statuses
def load_watchlist():
if not os.path.exists(WATCHLIST_FILE): return []
try:
with open(WATCHLIST_FILE, 'r') as f:
return json.load(f)
except:
return []
def save_watchlist(watchlist):
with open(WATCHLIST_FILE, 'w') as f: json.dump(watchlist, f)
def load_alerts():
if not os.path.exists(ALERTS_FILE): return []
try:
with open(ALERTS_FILE, 'r') as f:
return json.load(f)
except:
return []
def get_base64_image(image_path):
try:
with open(image_path, "rb") as img_file:
return base64.b64encode(img_file.read()).decode()
except Exception:
return ""
# --- Session State ---
if 'page' not in st.session_state:
st.session_state.page = 'home'
if 'analysis_complete' not in st.session_state:
st.session_state.analysis_complete = False
if 'final_state' not in st.session_state:
st.session_state.final_state = None
if 'error_message' not in st.session_state:
st.session_state.error_message = None
# --- UI Components ---
def render_sidebar():
with st.sidebar:
# Logo Area
logo_base64 = get_base64_image("assets/logo.png")
if logo_base64:
st.markdown(f"""
SENTINEL
AI Financial Intelligence
""", unsafe_allow_html=True)
# Navigation
if st.button("🏠 Home", use_container_width=True):
st.session_state.page = 'home'
st.rerun()
if st.button("⚡ Analysis Console", use_container_width=True):
st.session_state.page = 'analysis'
st.rerun()
st.markdown("---")
# Settings - Completely Redesigned
st.markdown("### 🎯 Intelligence Configuration")
# Analysis Depth
st.select_slider(
"Analysis Depth",
options=["Quick Scan", "Standard", "Deep Dive", "Comprehensive"],
value="Standard"
)
# Risk Profile
st.selectbox(
"Risk Tolerance",
["Conservative", "Moderate", "Aggressive", "Custom"],
help="Adjusts recommendation thresholds"
)
# Time Horizon
st.radio(
"Investment Horizon",
["Short-term (< 1 year)", "Medium-term (1-5 years)", "Long-term (5+ years)"],
index=1
)
# Market Sentiment Tracking
st.toggle("Track Market Sentiment", value=True, help="Include social media and news sentiment analysis")
st.markdown("---")
# System Status
with st.expander("📡 System Status", expanded=False):
server_statuses = check_server_status()
for name, status in server_statuses.items():
dot_class = "status-ok" if status == "✅ Online" else "status-err"
st.markdown(f"""
{name}
{status.split(' ')[1]}
""", unsafe_allow_html=True)
# Watchlist
with st.expander("🛡️ Watchlist", expanded=False):
watchlist = load_watchlist()
new_symbol = st.text_input("Add Symbol:", placeholder="e.g. MSFT").upper()
if st.button("Add"):
if new_symbol and new_symbol not in watchlist:
watchlist.append(new_symbol)
save_watchlist(watchlist)
st.rerun()
if watchlist:
st.markdown("---")
for symbol in watchlist:
col1, col2 = st.columns([3, 1])
col1.markdown(f"**{symbol}**")
if col2.button("❌", key=f"del_{symbol}"):
watchlist.remove(symbol)
save_watchlist(watchlist)
st.rerun()
def render_home():
# Auto-refresh logic (Every 10s)
if 'last_refresh_home' not in st.session_state:
st.session_state.last_refresh_home = time.time()
if time.time() - st.session_state.last_refresh_home > 10:
st.session_state.last_refresh_home = time.time()
st.rerun()
# Hero Section with Logo
logo_base64 = get_base64_image("assets/logo.png")
if logo_base64:
st.markdown(f"""
Sentinel AI
Financial Intelligence
Transform raw market data into actionable business insights with the power of AI.
Analyze stocks, news, and portfolios automatically using intelligent agents.
""", unsafe_allow_html=True)
else:
# Fallback without logo
st.markdown("""
Sentinel AI
Financial Intelligence
Transform raw market data into actionable business insights with the power of AI.
Analyze stocks, news, and portfolios automatically using intelligent agents.
""", unsafe_allow_html=True)
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
if st.button("🚀 Start Analysis", use_container_width=True):
st.session_state.page = 'analysis'
st.rerun()
# Feature Cards
st.markdown("""
🧠
Intelligent Analysis
Our AI automatically understands market structures, identifies patterns, and generates meaningful insights without manual configuration.
📊
Smart Visualizations
Intelligently creates the most appropriate charts and graphs for your data, with interactive visualizations.
🎯
Actionable Recommendations
Get specific, measurable recommendations for improving your portfolio based on data-driven insights.
""", unsafe_allow_html=True)
# --- Live Wire on Home Page ---
st.markdown("---")
st.markdown("### 🚨 Live Wire Trending")
alerts_container = st.container()
alerts = load_alerts()
if not alerts:
alerts_container.caption("No active alerts in feed.")
else:
for alert in reversed(alerts[-10:]): # Show last 10 on home
alert_type = alert.get("type", "INFO")
css_class = "alert-market" if alert_type == "MARKET" else "alert-news" if alert_type == "NEWS" else ""
icon = "📉" if alert_type == "MARKET" else "📰"
timestamp = datetime.fromisoformat(alert.get("timestamp", datetime.now().isoformat())).strftime("%H:%M:%S")
html = f"""
"""
alerts_container.markdown(html, unsafe_allow_html=True)
# Footer
st.markdown("
", unsafe_allow_html=True)
st.markdown("""
Powered by Google Gemini • Built with LangGraph • Designed with Streamlit
""", unsafe_allow_html=True)
def render_analysis():
st.markdown("## ⚡ Intelligence Directive")
# Error Display
if st.session_state.error_message:
st.error(st.session_state.error_message)
if st.button("Dismiss Error"):
st.session_state.error_message = None
st.rerun()
col_main, col_alerts = st.columns([3, 1.2])
with col_main:
with st.form("research_form", clear_on_submit=False):
task_input = st.text_area("Enter directive:", placeholder="e.g., Analyze the recent volatility for Tesla ($TSLA) and summarize news.", height=100)
submitted = st.form_submit_button("EXECUTE ANALYSIS", use_container_width=True)
if submitted and task_input:
st.session_state.error_message = None
server_statuses = check_server_status()
all_online = all(s == "✅ Online" for s in server_statuses.values())
if not all_online:
st.error("SYSTEM HALTED: Core services offline. Check sidebar status.")
else:
with st.status("🚀 SENTINEL ORCHESTRATOR ENGAGED...", expanded=True) as status:
try:
from agents.orchestrator_v3 import get_orchestrator
# Use default provider or env var
orchestrator = get_orchestrator(llm_provider="gemini")
final_state_result = {}
for event in orchestrator.stream({"task": task_input}):
agent_name = list(event.keys())[0]
state_update = list(event.values())[0]
final_state_result.update(state_update)
status.write(f"🛡️ Agent Active: {agent_name}...")
status.update(label="✅ Analysis Complete!", state="complete", expanded=False)
st.session_state.final_state = final_state_result
st.session_state.analysis_complete = True
st.rerun()
except Exception as e:
status.update(label="❌ System Failure", state="error")
st.session_state.error_message = f"RUNTIME ERROR: {e}"
st.rerun()
if st.session_state.analysis_complete:
final_state = st.session_state.final_state
symbol = final_state.get('symbol', 'N/A') if final_state else 'N/A'
st.markdown(f"### 📝 Report: {symbol}")
# Executive Summary
st.info(final_state.get("final_report", "No report generated."))
# Deep-Dive Insights
with st.expander("🔍 Deep-Dive Insights", expanded=True):
insights = final_state.get("analysis_results", {}).get("insights")
if insights: st.markdown(insights)
else: st.warning("No deep-dive insights available.")
# Charts
with st.expander("📊 Market Telemetry"):
charts = final_state.get("analysis_results", {}).get("charts", [])
if charts:
for chart in charts:
st.plotly_chart(chart, use_container_width=True)
else:
st.caption("No telemetry data available.")
# Raw Data
with st.expander("💾 Raw Intelligence Logs"):
tab1, tab2, tab3 = st.tabs(["Web Intelligence", "Market Data", "Internal Portfolio"])
with tab1: st.json(final_state.get('web_research_results', '{}'))
with tab2: st.json(final_state.get('market_data_results', '{}'))
with tab3: st.json(final_state.get('portfolio_data_results', '{}'))
if st.button("🛡️ New Analysis"):
st.session_state.analysis_complete = False
st.session_state.final_state = None
st.rerun()
# Live Alerts Feed
with col_alerts:
st.markdown("### 🚨 Live Wire")
alerts_container = st.container()
# Auto-refresh logic
if 'last_refresh' not in st.session_state:
st.session_state.last_refresh = time.time()
if time.time() - st.session_state.last_refresh > 10:
st.session_state.last_refresh = time.time()
st.rerun()
alerts = load_alerts()
if not alerts:
alerts_container.caption("No active alerts in feed.")
else:
for alert in reversed(alerts[-20:]):
alert_type = alert.get("type", "INFO")
css_class = "alert-market" if alert_type == "MARKET" else "alert-news" if alert_type == "NEWS" else ""
icon = "📉" if alert_type == "MARKET" else "📰"
timestamp = datetime.fromisoformat(alert.get("timestamp", datetime.now().isoformat())).strftime("%H:%M:%S")
html = f"""
"""
alerts_container.markdown(html, unsafe_allow_html=True)
# --- Main App Routing ---
render_sidebar()
if st.session_state.page == 'home':
render_home()
elif st.session_state.page == 'analysis':
render_analysis()