| | 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 |
| |
|
| | |
| | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '.'))) |
| |
|
| | |
| | WATCHLIST_FILE = "watchlist.json" |
| | ALERTS_FILE = "alerts.json" |
| |
|
| | |
| | st.set_page_config( |
| | page_title="Sentinel - AI Financial Intelligence", |
| | page_icon="π‘οΈ", |
| | layout="wide", |
| | initial_sidebar_state="expanded" |
| | ) |
| |
|
| | |
| | def load_css(file_name): |
| | with open(file_name) as f: |
| | st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True) |
| |
|
| | load_css("style.css") |
| |
|
| | |
| | |
| | |
| | @st.cache_resource |
| | def start_background_services(): |
| | """Checks if backend services are running and starts them if needed.""" |
| | |
| | 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() |
| | |
| | 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}") |
| |
|
| | |
| | cwd = os.path.dirname(os.path.abspath(__file__)) |
| | for script, port in services: |
| | print(f"π Launching {script} on port {port}...") |
| | |
| | subprocess.Popen( |
| | [sys.executable, script], |
| | cwd=cwd, |
| | env=env, |
| | |
| | |
| | |
| | ) |
| | |
| | print("π Launching Monitor...") |
| | subprocess.Popen( |
| | [sys.executable, "monitor.py"], |
| | cwd=cwd, |
| | env=env |
| | ) |
| | |
| | |
| | print("β
Background services launch triggered.") |
| |
|
| | |
| | start_background_services() |
| |
|
| | |
| | @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 "" |
| |
|
| | |
| | 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 |
| |
|
| | |
| |
|
| | def render_sidebar(): |
| | with st.sidebar: |
| | |
| | logo_base64 = get_base64_image("assets/logo.png") |
| | if logo_base64: |
| | st.markdown(f""" |
| | <div style="text-align: center; margin-bottom: 2rem;"> |
| | <img src="data:image/png;base64,{logo_base64}" style="width: 80px; height: 80px; margin-bottom: 10px;"> |
| | <h2 style="margin:0; font-size: 1.5rem;">SENTINEL</h2> |
| | <p style="color: var(--text-secondary); font-size: 0.8rem;">AI Financial Intelligence</p> |
| | </div> |
| | """, unsafe_allow_html=True) |
| | |
| | |
| | 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("---") |
| | |
| | |
| | st.markdown("### π― Intelligence Configuration") |
| | |
| | |
| | st.select_slider( |
| | "Analysis Depth", |
| | options=["Quick Scan", "Standard", "Deep Dive", "Comprehensive"], |
| | value="Standard" |
| | ) |
| | |
| | |
| | st.selectbox( |
| | "Risk Tolerance", |
| | ["Conservative", "Moderate", "Aggressive", "Custom"], |
| | help="Adjusts recommendation thresholds" |
| | ) |
| | |
| | |
| | st.radio( |
| | "Investment Horizon", |
| | ["Short-term (< 1 year)", "Medium-term (1-5 years)", "Long-term (5+ years)"], |
| | index=1 |
| | ) |
| | |
| | |
| | st.toggle("Track Market Sentiment", value=True, help="Include social media and news sentiment analysis") |
| | |
| | st.markdown("---") |
| | |
| | |
| | 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""" |
| | <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;"> |
| | <span style="font-size: 0.9rem;">{name}</span> |
| | <div><span class="status-dot {dot_class}"></span><span style="font-size: 0.8rem; color: var(--text-secondary);">{status.split(' ')[1]}</span></div> |
| | </div> |
| | """, unsafe_allow_html=True) |
| |
|
| | |
| | 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(): |
| | |
| | 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() |
| |
|
| | |
| | logo_base64 = get_base64_image("assets/logo.png") |
| | |
| | if logo_base64: |
| | st.markdown(f""" |
| | <div class="hero-container"> |
| | <div style="display: flex; align-items: center; justify-content: center; gap: 20px; margin-bottom: 1.5rem;"> |
| | <img src="data:image/png;base64,{logo_base64}" style="width: 80px; height: 80px;"> |
| | <h1 class="hero-title" style="margin: 0;">Sentinel AI<br>Financial Intelligence</h1> |
| | </div> |
| | <p class="hero-subtitle"> |
| | Transform raw market data into actionable business insights with the power of AI. |
| | Analyze stocks, news, and portfolios automatically using intelligent agents. |
| | </p> |
| | </div> |
| | """, unsafe_allow_html=True) |
| | else: |
| | |
| | st.markdown(""" |
| | <div class="hero-container"> |
| | <h1 class="hero-title">Sentinel AI<br>Financial Intelligence</h1> |
| | <p class="hero-subtitle"> |
| | Transform raw market data into actionable business insights with the power of AI. |
| | Analyze stocks, news, and portfolios automatically using intelligent agents. |
| | </p> |
| | </div> |
| | """, 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() |
| |
|
| | |
| | st.markdown(""" |
| | <div class="feature-grid"> |
| | <div class="feature-card"> |
| | <div class="feature-icon">π§ </div> |
| | <div class="feature-title">Intelligent Analysis</div> |
| | <div class="feature-desc"> |
| | Our AI automatically understands market structures, identifies patterns, and generates meaningful insights without manual configuration. |
| | </div> |
| | </div> |
| | <div class="feature-card"> |
| | <div class="feature-icon">π</div> |
| | <div class="feature-title">Smart Visualizations</div> |
| | <div class="feature-desc"> |
| | Intelligently creates the most appropriate charts and graphs for your data, with interactive visualizations. |
| | </div> |
| | </div> |
| | <div class="feature-card"> |
| | <div class="feature-icon">π―</div> |
| | <div class="feature-title">Actionable Recommendations</div> |
| | <div class="feature-desc"> |
| | Get specific, measurable recommendations for improving your portfolio based on data-driven insights. |
| | </div> |
| | </div> |
| | </div> |
| | """, unsafe_allow_html=True) |
| |
|
| | |
| | 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:]): |
| | 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""" |
| | <div class="alert-card {css_class}"> |
| | <div class="alert-header"> |
| | <span>{icon} {alert.get("symbol")}</span> |
| | <span>{timestamp}</span> |
| | </div> |
| | <div class="alert-body"> |
| | {alert.get("message")} |
| | </div> |
| | </div> |
| | """ |
| | alerts_container.markdown(html, unsafe_allow_html=True) |
| |
|
| | |
| | st.markdown("<br><br><br>", unsafe_allow_html=True) |
| | st.markdown(""" |
| | <div style="text-align: center; color: var(--text-secondary); font-size: 0.9rem;"> |
| | Powered by <b>Google Gemini</b> β’ Built with <b>LangGraph</b> β’ Designed with <b>Streamlit</b> |
| | </div> |
| | """, unsafe_allow_html=True) |
| |
|
| | def render_analysis(): |
| | st.markdown("## β‘ Intelligence Directive") |
| | |
| | |
| | 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 |
| | |
| | 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}") |
| | |
| | |
| | st.info(final_state.get("final_report", "No report generated.")) |
| | |
| | |
| | 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.") |
| | |
| | |
| | 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.") |
| | |
| | |
| | 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() |
| |
|
| | |
| | with col_alerts: |
| | st.markdown("### π¨ Live Wire") |
| | alerts_container = st.container() |
| | |
| | |
| | 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""" |
| | <div class="alert-card {css_class}"> |
| | <div class="alert-header"> |
| | <span>{icon} {alert.get("symbol")}</span> |
| | <span>{timestamp}</span> |
| | </div> |
| | <div class="alert-body"> |
| | {alert.get("message")} |
| | </div> |
| | </div> |
| | """ |
| | alerts_container.markdown(html, unsafe_allow_html=True) |
| |
|
| | |
| | render_sidebar() |
| |
|
| | if st.session_state.page == 'home': |
| | render_home() |
| | elif st.session_state.page == 'analysis': |
| | render_analysis() |