| |
| import streamlit as st |
| import sys |
| import os |
| import httpx |
| import time |
| import json |
| from datetime import datetime |
|
|
| |
| sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '.'))) |
| from agents.orchestrator_v3 import SentinelOrchestratorV3 |
|
|
| |
| WATCHLIST_FILE = "watchlist.json" |
| ALERTS_FILE = "alerts.json" |
|
|
| |
| st.set_page_config( |
| page_title="Aegis Digital Briefing", |
| page_icon="🛡️", |
| layout="wide" |
| ) |
|
|
| |
| st.markdown(""" |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@600;700&family=Open+Sans:wght@400;600&display=swap'); |
| |
| html, body, [class*="st-"] { |
| font-family: 'Open Sans', sans-serif; |
| } |
| |
| /* Main Headers */ |
| h1, h2, h3 { |
| font-family: 'Source Serif 4', serif; |
| } |
| .main-header { |
| font-size: 2.8rem; |
| font-weight: 700; |
| color: #1A202C; |
| text-align: center; |
| margin-bottom: 0.5rem; |
| } |
| .subtitle { |
| text-align: center; |
| color: #718096; |
| font-size: 1.1rem; |
| margin-bottom: 2.5rem; |
| } |
| |
| /* Card/Widget styling */ |
| .card { |
| background-color: #FFFFFF; |
| border-radius: 8px; |
| padding: 25px; |
| border: 1px solid #E2E8F0; |
| } |
| .metric-card { |
| border-radius: 8px; |
| padding: 1.5rem; |
| text-align: center; |
| border: 1px solid #E2E8F0; |
| } |
| .metric-value { |
| font-size: 2rem; |
| font-weight: 700; |
| color: #2D3748; |
| } |
| .metric-label { |
| font-size: 0.9rem; |
| color: #A0AEC0; |
| font-weight: 600; |
| } |
| |
| /* Sidebar "Analyst Notes" */ |
| .sidebar .st-emotion-cache-16txtl3 { |
| font-size: 1.2rem; |
| font-weight: 600; |
| color: #2D3748; |
| } |
| .note-entry { |
| background-color: #F7FAFC; |
| border-left: 4px solid #4299E1; |
| padding: 1rem; |
| border-radius: 4px; |
| margin-bottom: 0.75rem; |
| } |
| .note-title { font-weight: 600; color: #2C5282; margin-bottom: 0.25rem; } |
| .note-content { font-size: 0.85rem; color: #4A5568; } |
| |
| /* Alerts Styling */ |
| .alert-card { |
| padding: 1rem; |
| border-radius: 6px; |
| margin-bottom: 0.8rem; |
| border-left: 5px solid #CBD5E0; |
| background-color: #F7FAFC; |
| } |
| .alert-market { border-left-color: #E53E3E; background-color: #FFF5F5; } /* Red for Market */ |
| .alert-news { border-left-color: #3182CE; background-color: #EBF8FF; } /* Blue for News */ |
| .alert-header { display: flex; justify-content: space-between; font-size: 0.85rem; color: #718096; margin-bottom: 0.5rem; } |
| .alert-body { font-weight: 600; color: #2D3748; } |
| |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| |
| @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 [] |
|
|
| if 'final_state' not in st.session_state: |
| st.session_state.final_state = None |
|
|
| |
|
|
| |
| st.markdown('<h1 class="main-header">Aegis Digital Briefing Room</h1>', unsafe_allow_html=True) |
| st.markdown('<p class="subtitle">Automated Intelligence Reports for Modern Finance</p>', unsafe_allow_html=True) |
|
|
| |
| sidebar = st.sidebar |
| sidebar.title("🛡️ Command Center") |
|
|
| |
| sidebar.subheader("Active Watchlist") |
| watchlist = load_watchlist() |
| new_symbol = sidebar.text_input("Add Symbol:", placeholder="e.g. MSFT").upper() |
| if sidebar.button("Add to Watchlist"): |
| if new_symbol and new_symbol not in watchlist: |
| watchlist.append(new_symbol) |
| save_watchlist(watchlist) |
| st.rerun() |
|
|
| symbol_to_remove = sidebar.selectbox("Remove Symbol:", ["Select..."] + watchlist) |
| if symbol_to_remove != "Select..." and sidebar.button("Remove"): |
| watchlist.remove(symbol_to_remove) |
| save_watchlist(watchlist) |
| st.rerun() |
|
|
| sidebar.markdown("---") |
|
|
| |
| sidebar.title("👨💼 Analyst's Live Notes") |
| notes_placeholder = sidebar.empty() |
| notes_placeholder.info("Awaiting new directive...") |
|
|
| |
| main_col, alerts_col = st.columns([3, 1]) |
|
|
| with main_col: |
| |
| main_container = st.container(border=True) |
| |
| |
| with main_container: |
| st.subheader("🚀 Launch On-Demand Analysis") |
| with st.form("research_form"): |
| task_input = st.text_input("", placeholder="Enter your directive, e.g., 'Analyze market reaction to the latest Apple ($AAPL) product launch'", label_visibility="collapsed") |
| submitted = st.form_submit_button("Generate Briefing", use_container_width=True) |
|
|
| |
| if submitted and task_input: |
| server_statuses = check_server_status() |
| if not all(s == "✅ Online" for s in server_statuses.values()): |
| main_container.error("Analysis cannot proceed. One or more backend services are offline. Please check the status.") |
| else: |
| |
| |
| final_state_result = {} |
| analyst_notes = [] |
| |
| try: |
| with st.spinner("Your AI Analyst is compiling the briefing... This may take a moment."): |
| for event in SentinelOrchestratorV3.stream({"task": task_input}): |
| node_name = list(event.keys())[0] |
| final_state_result.update(event[node_name]) |
| |
| |
| note = "" |
| if node_name == "extract_symbol": |
| note = f"Identified target entity: **{event[node_name].get('symbol', 'N/A')}**" |
| elif node_name == "web_researcher": |
| note = "Sourced initial open-source intelligence from the web." |
| elif node_name == "market_data_analyst": |
| note = "Retrieved latest intraday market performance data." |
| elif node_name == "data_analyzer": |
| note = "Commenced deep-dive quantitative analysis of time-series data." |
| elif node_name == "report_synthesizer": |
| note = "Synthesizing all findings into the final executive briefing." |
| |
| if note: |
| analyst_notes.append(f'<div class="note-entry"><div class="note-title">{node_name.replace("_", " ").title()}</div><div class="note-content">{note}</div></div>') |
| notes_placeholder.markdown("".join(analyst_notes), unsafe_allow_html=True) |
| time.sleep(0.5) |
|
|
| |
| st.session_state.final_state = final_state_result |
| final_state = st.session_state.final_state |
| symbol = final_state.get("symbol", "N/A") |
|
|
| |
| st.markdown(f"## Briefing: {symbol} - {datetime.now().strftime('%B %d, %Y')}") |
| st.markdown("---") |
| |
| |
| st.subheader("Key Performance Indicators") |
| df = final_state.get("analysis_results", {}).get("dataframe") |
| if df is not None and not df.empty: |
| m_col1, m_col2, m_col3, m_col4 = st.columns(4) |
| with m_col1: |
| st.markdown(f'<div class="metric-card"><div class="metric-value">${df["close"].iloc[-1]:.2f}</div><div class="metric-label">Latest Close Price</div></div>', unsafe_allow_html=True) |
| with m_col2: |
| st.markdown(f'<div class="metric-card"><div class="metric-value">{df["volume"].sum()/1e6:.2f}M</div><div class="metric-label">Total Volume</div></div>', unsafe_allow_html=True) |
| with m_col3: |
| st.markdown(f'<div class="metric-card"><div class="metric-value">${df["high"].max():.2f}</div><div class="metric-label">Intraday High</div></div>', unsafe_allow_html=True) |
| with m_col4: |
| st.markdown(f'<div class="metric-card"><div class="metric-value">${df["low"].min():.2f}</div><div class="metric-label">Intraday Low</div></div>', unsafe_allow_html=True) |
| else: |
| st.info("Quantitative market data was not applicable for this briefing.") |
| |
| st.markdown("<br>", unsafe_allow_html=True) |
| |
| |
| brief_col1, brief_col2 = st.columns([7, 5]) |
| with brief_col1: |
| st.subheader("Executive Summary & Analysis") |
| report_html = final_state.get("final_report", "No report generated.").replace("\n", "<br>") |
| st.markdown(f'<div class="card" style="height: 100%;">{report_html}</div>', unsafe_allow_html=True) |
| |
| with brief_col2: |
| st.subheader("Visual Data Debrief") |
| charts = final_state.get("analysis_results", {}).get("charts", []) |
| if charts: |
| for chart in charts: |
| st.plotly_chart(chart, use_container_width=True) |
| else: |
| st.markdown('<div class="card" style="height: 100%;"><p>No visualizations were generated for this briefing.</p></div>', unsafe_allow_html=True) |
| |
| |
| with st.expander("Show Evidence Log & Methodology"): |
| st.markdown("#### Open Source Intelligence (Web Research)") |
| st.json(final_state.get('web_research_results', '{}')) |
| st.markdown("#### Deep-Dive Analysis Insights") |
| st.text(final_state.get("analysis_results", {}).get("insights", "No insights.")) |
|
|
| if st.button("Start New Briefing"): |
| st.session_state.final_state = None |
| st.rerun() |
|
|
| except Exception as e: |
| st.error(f"An error occurred: {e}") |
|
|
| |
| with alerts_col: |
| st.subheader("🚨 Live Alerts") |
| st.caption("Real-time monitoring feed") |
| |
| alerts_container = st.container(height=600) |
| |
| |
| 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.info("No active alerts.") |
| else: |
| for alert in alerts: |
| 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") |
| |
| 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) |