warisali128/FinOpsBuddy
0
1"""2FinOps Buddy - A Streamlit Application for Cloud Cost Management3===============================================================4 5A complete FinOps dashboard with AI-powered chat agent for analyzing cloud spending.6Features:7- Real-time dashboard with line and bar charts8- AI chat agent with OpenAI GPT-4 integration9- Pre-defined query functions for cost analysis10- Dark mode UI with split-panel layout11 12Author: AI Assistant13"""14 15import streamlit as st16import pandas as pd17import numpy as np18from datetime import datetime, timedelta19from typing import Optional, Dict, Any, List20import json21import os22from dataclasses import dataclass23 24# Database imports25try:26 from sqlalchemy import create_engine, text27 from sqlalchemy.orm import sessionmaker28 DATABASE_AVAILABLE = True29except ImportError:30 DATABASE_AVAILABLE = False31 32# Visualization33import altair as alt34 35# OpenAI36try:37 from openai import OpenAI38 OPENAI_AVAILABLE = True39except ImportError:40 OPENAI_AVAILABLE = False41 42# =============================================================================43# CONFIGURATION & SETUP44# =============================================================================45 46# Page configuration47st.set_page_config(48 page_title="FinOps Buddy",49 page_icon="๐ฐ",50 layout="wide",51 initial_sidebar_state="expanded"52)53 54# Custom CSS for dark mode styling55st.markdown("""56<style>57 /* Dark theme base */58 .stApp {59 background-color: #0e1117;60 color: #fafafa;61 }62 63 /* Header styling */64 .main-header {65 font-size: 2.5rem;66 font-weight: 700;67 color: #00d4aa;68 text-align: center;69 margin-bottom: 2rem;70 text-shadow: 0 0 20px rgba(0, 212, 170, 0.3);71 }72 73 /* Chat container */74 .chat-container {75 background-color: #1a1d24;76 border-radius: 10px;77 padding: 20px;78 height: 600px;79 overflow-y: auto;80 border: 1px solid #2d3139;81 }82 83 /* User message */84 .user-message {85 background-color: #0066cc;86 color: white;87 padding: 12px 16px;88 border-radius: 15px 15px 2px 15px;89 margin: 8px 0;90 max-width: 80%;91 float: right;92 clear: both;93 }94 95 /* AI message */96 .ai-message {97 background-color: #2d3139;98 color: #fafafa;99 padding: 12px 16px;100 border-radius: 15px 15px 15px 2px;101 margin: 8px 0;102 max-width: 80%;103 float: left;104 clear: both;105 border-left: 3px solid #00d4aa;106 }107 108 /* Dashboard cards */109 .metric-card {110 background-color: #1a1d24;111 border-radius: 10px;112 padding: 20px;113 border: 1px solid #2d3139;114 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);115 }116 117 /* Quick action buttons */118 .stButton>button {119 background-color: #00d4aa;120 color: #0e1117;121 border: none;122 border-radius: 8px;123 padding: 10px 20px;124 font-weight: 600;125 transition: all 0.3s;126 }127 128 .stButton>button:hover {129 background-color: #00b894;130 transform: translateY(-2px);131 box-shadow: 0 4px 12px rgba(0, 212, 170, 0.4);132 }133 134 /* Input styling */135 .stTextInput>div>div>input {136 background-color: #1a1d24;137 color: #fafafa;138 border: 1px solid #2d3139;139 border-radius: 8px;140 }141 142 /* Chart containers */143 .chart-container {144 background-color: #1a1d24;145 border-radius: 10px;146 padding: 15px;147 margin: 10px 0;148 border: 1px solid #2d3139;149 }150 151 /* Scrollbar styling */152 ::-webkit-scrollbar {153 width: 8px;154 height: 8px;155 }156 157 ::-webkit-scrollbar-track {158 background: #0e1117;159 }160 161 ::-webkit-scrollbar-thumb {162 background: #2d3139;163 border-radius: 4px;164 }165 166 ::-webkit-scrollbar-thumb:hover {167 background: #00d4aa;168 }169 170 /* Sidebar */171 .css-1d391kg {172 background-color: #161b22;173 }174</style>175""", unsafe_allow_html=True)176 177 178# =============================================================================179# DATA MODELS & MOCK DATA GENERATION180# =============================================================================181 182@dataclass183class CloudCostRecord:184 """Data model for cloud cost records"""185 date: datetime186 service: str187 cost: float188 region: str189 resource_id: str190 191 192class DataManager:193 """194 Manages data ingestion from either PostgreSQL database or CSV file.195 Falls back to mock data if neither is available.196 """197 198 def __init__(self):199 self.df: Optional[pd.DataFrame] = None200 self.engine = None201 self._initialize_data()202 203 def _initialize_data(self):204 """Initialize data source (DB, CSV, or mock)"""205 # Try PostgreSQL first206 db_url = os.getenv("DATABASE_URL")207 if db_url and DATABASE_AVAILABLE:208 try:209 self.engine = create_engine(db_url)210 self.df = self._load_from_database()211 st.sidebar.success("โ
Connected to PostgreSQL")212 return213 except Exception as e:214 st.sidebar.warning(f"โ ๏ธ DB Connection failed: {e}")215 216 # Try CSV file217 if os.path.exists("cloud_costs.csv"):218 try:219 self.df = pd.read_csv("cloud_costs.csv")220 self.df['date'] = pd.to_datetime(self.df['date'])221 st.sidebar.success("โ
Loaded from CSV")222 return223 except Exception as e:224 st.sidebar.warning(f"โ ๏ธ CSV load failed: {e}")225 226 # Generate mock data227 self.df = self._generate_mock_data()228 st.sidebar.info("โน๏ธ Using mock data (no DB/CSV found)")229 230 def _generate_mock_data(self) -> pd.DataFrame:231 """Generate realistic mock cloud cost data"""232 np.random.seed(42)233 services = ['EC2', 'S3', 'RDS', 'Lambda', 'CloudFront', 'ElastiCache', 'EBS', 'ELB']234 regions = ['us-east-1', 'us-west-2', 'eu-west-1', 'ap-southeast-1']235 236 data = []237 base_date = datetime.now() - timedelta(days=30)238 239 for i in range(30):240 current_date = base_date + timedelta(days=i)241 # Add some randomness and trends242 daily_multiplier = 1 + 0.3 * np.sin(i / 5) + np.random.normal(0, 0.1)243 244 for service in services:245 # Base cost varies by service246 base_cost = {247 'EC2': 450, 'S3': 120, 'RDS': 280, 'Lambda': 45,248 'CloudFront': 85, 'ElastiCache': 150, 'EBS': 95, 'ELB': 65249 }[service]250 251 cost = base_cost * daily_multiplier * (1 + np.random.normal(0, 0.15))252 253 # Add occasional spikes254 if np.random.random() < 0.1: # 10% chance of spike255 cost *= np.random.uniform(1.5, 3.0)256 257 data.append({258 'date': current_date,259 'service': service,260 'cost': round(max(cost, 0), 2),261 'region': np.random.choice(regions),262 'resource_id': f"{service.lower()}-{np.random.randint(1000, 9999)}"263 })264 265 return pd.DataFrame(data)266 267 def _load_from_database(self) -> pd.DataFrame:268 """Load data from PostgreSQL database"""269 query = """270 SELECT date, service, cost, region, resource_id 271 FROM cloud_costs 272 WHERE date >= CURRENT_DATE - INTERVAL '30 days'273 ORDER BY date274 """275 with self.engine.connect() as conn:276 df = pd.read_sql(text(query), conn)277 df['date'] = pd.to_datetime(df['date'])278 return df279 280 def get_daily_costs(self) -> pd.DataFrame:281 """Get aggregated daily costs"""282 return self.df.groupby('date')['cost'].sum().reset_index()283 284 def get_top_services(self, n: int = 5) -> pd.DataFrame:285 """Get top N cost-generating services"""286 return self.df.groupby('service')['cost'].sum().nlargest(n).reset_index()287 288 def get_cost_by_date(self, date: datetime) -> float:289 """Get total cost for a specific date"""290 mask = self.df['date'].dt.date == date.date()291 return self.df[mask]['cost'].sum()292 293 def get_top_service_by_date(self, date: datetime) -> str:294 """Get top service for a specific date"""295 mask = self.df['date'].dt.date == date.date()296 day_data = self.df[mask]297 if day_data.empty:298 return "No data"299 return day_data.groupby('service')['cost'].sum().idxmax()300 301 def compare_service_cost(self, service: str, date1: datetime, date2: datetime) -> Dict[str, float]:302 """Compare service cost between two dates"""303 mask1 = (self.df['date'].dt.date == date1.date()) & (self.df['service'] == service)304 mask2 = (self.df['date'].dt.date == date2.date()) & (self.df['service'] == service)305 306 cost1 = self.df[mask1]['cost'].sum()307 cost2 = self.df[mask2]['cost'].sum()308 309 return {310 'date1_cost': cost1,311 'date2_cost': cost2,312 'difference': cost2 - cost1,313 'percent_change': ((cost2 - cost1) / cost1 * 100) if cost1 > 0 else 0314 }315 316 def detect_anomalies(self, threshold: float = 2.0) -> pd.DataFrame:317 """Detect anomalous spending days"""318 daily = self.get_daily_costs()319 mean_cost = daily['cost'].mean()320 std_cost = daily['cost'].std()321 322 daily['z_score'] = (daily['cost'] - mean_cost) / std_cost323 anomalies = daily[abs(daily['z_score']) > threshold].copy()324 anomalies['severity'] = anomalies['z_score'].apply(325 lambda x: 'High' if abs(x) > 3 else 'Medium'326 )327 return anomalies328 329 330# =============================================================================331# AI AGENT & TOOLS332# =============================================================================333 334class FinOpsAgent:335 """336 AI Agent for analyzing cloud costs using OpenAI GPT-4.337 Includes predefined tools/functions for cost analysis.338 """339 340 def __init__(self, data_manager: DataManager):341 self.data = data_manager342 self.client = None343 self.conversation_history = []344 345 # Initialize OpenAI client346 api_key = os.getenv("OPENAI_API_KEY")347 if api_key and OPENAI_AVAILABLE:348 self.client = OpenAI(api_key=api_key)349 350 # Define available tools (MCP-style)351 self.tools = {352 "get_daily_cost": self._tool_get_daily_cost,353 "get_top_service": self._tool_get_top_service,354 "compare_service_cost": self._tool_compare_service_cost,355 "get_anomalies": self._tool_get_anomalies,356 "get_service_breakdown": self._tool_get_service_breakdown357 }358 359 def _tool_get_daily_cost(self, date_str: str) -> str:360 """Tool: Get total cost for a specific date"""361 try:362 date = datetime.strptime(date_str, "%Y-%m-%d")363 cost = self.data.get_cost_by_date(date)364 return f"Total cloud cost on {date_str}: ${cost:,.2f}"365 except Exception as e:366 return f"Error: {str(e)}"367 368 def _tool_get_top_service(self, date_str: str) -> str:369 """Tool: Get top cost-generating service for a date"""370 try:371 date = datetime.strptime(date_str, "%Y-%m-%d")372 service = self.data.get_top_service_by_date(date)373 mask = self.data.df['date'].dt.date == date.date()374 cost = self.data.df[mask].groupby('service')['cost'].sum().max()375 return f"Top service on {date_str}: {service} (${cost:,.2f})"376 except Exception as e:377 return f"Error: {str(e)}"378 379 def _tool_compare_service_cost(self, service: str, date1_str: str, date2_str: str) -> str:380 """Tool: Compare service cost between two dates"""381 try:382 date1 = datetime.strptime(date1_str, "%Y-%m-%d")383 date2 = datetime.strptime(date2_str, "%Y-%m-%d")384 result = self.data.compare_service_cost(service, date1, date2)385 386 change_str = f"+{result['percent_change']:.1f}%" if result['difference'] >= 0 else f"{result['percent_change']:.1f}%"387 trend = "๐ increased" if result['difference'] >= 0 else "๐ decreased"388 389 return (f"{service} cost comparison:\n"390 f" {date1_str}: ${result['date1_cost']:,.2f}\n"391 f" {date2_str}: ${result['date2_cost']:,.2f}\n"392 f" Change: {trend} by ${abs(result['difference']):,.2f} ({change_str})")393 except Exception as e:394 return f"Error: {str(e)}"395 396 def _tool_get_anomalies(self) -> str:397 """Tool: Detect cost anomalies"""398 try:399 anomalies = self.data.detect_anomalies()400 if anomalies.empty:401 return "No significant anomalies detected in the last 30 days."402 403 result = "๐จ Anomalies detected:\n"404 for _, row in anomalies.iterrows():405 direction = "spike" if row['z_score'] > 0 else "drop"406 result += f" โข {row['date'].strftime('%Y-%m-%d')}: ${row['cost']:,.2f} ({direction}, severity: {row['severity']})\n"407 return result408 except Exception as e:409 return f"Error: {str(e)}"410 411 def _tool_get_service_breakdown(self) -> str:412 """Tool: Get service cost breakdown"""413 try:414 top_services = self.data.get_top_services(8)415 total = top_services['cost'].sum()416 result = "Service breakdown (last 30 days):\n"417 for _, row in top_services.iterrows():418 pct = (row['cost'] / total) * 100419 result += f" โข {row['service']}: ${row['cost']:,.2f} ({pct:.1f}%)\n"420 return result421 except Exception as e:422 return f"Error: {str(e)}"423 424 def process_query(self, query: str) -> str:425 """426 Process user query using OpenAI GPT-4 with function calling.427 Falls back to rule-based responses if OpenAI is unavailable.428 """429 if not self.client:430 return self._fallback_response(query)431 432 # Prepare system message with context433 system_msg = """You are FinOps Buddy, an expert cloud cost analyst. Analyze billing data and provide clear, actionable insights.434Available tools:435- get_daily_cost(date): Get total cost for a date (YYYY-MM-DD)436- get_top_service(date): Get top service for a date437- compare_service_cost(service, date1, date2): Compare service costs438- get_anomalies(): Detect spending anomalies439- get_service_breakdown(): Get service cost distribution440 441Use tools when needed to answer accurately. Be concise but informative."""442 443 messages = [444 {"role": "system", "content": system_msg},445 *self.conversation_history[-5:], # Keep last 5 messages for context446 {"role": "user", "content": query}447 ]448 449 try:450 # First call to determine if tools are needed451 response = self.client.chat.completions.create(452 model="gpt-4",453 messages=messages,454 temperature=0.3455 )456 457 ai_message = response.choices[0].message.content458 459 # Check if we need to use tools (simple keyword matching for demo)460 tool_results = []461 if "cost" in query.lower() and any(x in query for x in ["yesterday", "today", "date"]):462 # Extract date from query463 if "yesterday" in query.lower():464 date_str = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")465 tool_results.append(self._tool_get_daily_cost(date_str))466 467 if "anomal" in query.lower() or "spike" in query.lower():468 tool_results.append(self._tool_get_anomalies())469 470 if "top" in query.lower() or "highest" in query.lower():471 tool_results.append(self._tool_get_service_breakdown())472 473 # If tools were used, make second call with results474 if tool_results:475 tool_context = "\n\n".join(tool_results)476 messages.append({"role": "assistant", "content": f"Tool results:\n{tool_context}"})477 messages.append({"role": "user", "content": "Based on this data, answer the original question."})478 479 final_response = self.client.chat.completions.create(480 model="gpt-4",481 messages=messages,482 temperature=0.3483 )484 ai_message = final_response.choices[0].message.content485 486 # Update conversation history487 self.conversation_history.extend([488 {"role": "user", "content": query},489 {"role": "assistant", "content": ai_message}490 ])491 492 return ai_message493 494 except Exception as e:495 return f"AI Error: {str(e)}. Falling back to basic analysis.\n\n{self._fallback_response(query)}"496 497 def _fallback_response(self, query: str) -> str:498 """Rule-based fallback when OpenAI is unavailable"""499 query_lower = query.lower()500 501 if "anomal" in query_lower or "spike" in query_lower:502 return self._tool_get_anomalies()503 504 elif "top" in query_lower or "highest" in query_lower:505 return self._tool_get_service_breakdown()506 507 elif "yesterday" in query_lower:508 date_str = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")509 return self._tool_get_daily_cost(date_str)510 511 elif "compare" in query_lower or "difference" in query_lower:512 return "To compare costs, please specify the service and dates (e.g., 'Compare EC2 cost between 2024-01-01 and 2024-01-02')"513 514 else:515 return (f"I can help you analyze cloud costs! Try asking:\n"516 f"โข 'Why did my bill spike yesterday?'\n"517 f"โข 'What are the top cost services?'\n"518 f"โข 'Find anomalies this week'\n"519 f"โข 'Compare EC2 costs between dates'")520 521 522# =============================================================================523# UI COMPONENTS524# =============================================================================525 526def render_header():527 """Render application header"""528 st.markdown('<h1 class="main-header">๐ฐ FinOps Buddy</h1>', unsafe_allow_html=True)529 st.markdown("""530 <p style="text-align: center; color: #8b949e; margin-bottom: 2rem;">531 AI-Powered Cloud Cost Management Dashboard532 </p>533 """, unsafe_allow_html=True)534 535 536def render_dashboard(data_manager: DataManager):537 """Render the left panel dashboard with charts"""538 539 # Key metrics540 col1, col2, col3 = st.columns(3)541 542 daily_data = data_manager.get_daily_costs()543 total_30d = daily_data['cost'].sum()544 avg_daily = daily_data['cost'].mean()545 yesterday = datetime.now() - timedelta(days=1)546 yesterday_cost = data_manager.get_cost_by_date(yesterday)547 548 with col1:549 st.markdown(f"""550 <div class="metric-card">551 <h3 style="color: #8b949e; font-size: 0.9rem;">30-Day Total</h3>552 <p style="font-size: 1.8rem; font-weight: 700; color: #00d4aa; margin: 0;">${total_30d:,.0f}</p>553 </div>554 """, unsafe_allow_html=True)555 556 with col2:557 st.markdown(f"""558 <div class="metric-card">559 <h3 style="color: #8b949e; font-size: 0.9rem;">Daily Average</h3>560 <p style="font-size: 1.8rem; font-weight: 700; color: #58a6ff; margin: 0;">${avg_daily:,.0f}</p>561 </div>562 """, unsafe_allow_html=True)563 564 with col3:565 trend_color = "#00d4aa" if yesterday_cost <= avg_daily else "#f85149"566 st.markdown(f"""567 <div class="metric-card">568 <h3 style="color: #8b949e; font-size: 0.9rem;">Yesterday</h3>569 <p style="font-size: 1.8rem; font-weight: 700; color: {trend_color}; margin: 0;">${yesterday_cost:,.0f}</p>570 </div>571 """, unsafe_allow_html=True)572 573 st.markdown("---")574 575 # Line chart - Daily spending trend576 st.subheader("๐ Daily Spending Trend (30 Days)")577 578 line_chart = alt.Chart(daily_data).mark_line(579 point=True,580 color="#00d4aa",581 strokeWidth=3582 ).encode(583 x=alt.X('date:T', title='Date', axis=alt.Axis(format='%m/%d', labelColor='#8b949e')),584 y=alt.Y('cost:Q', title='Cost ($)', axis=alt.Axis(labelColor='#8b949e')),585 tooltip=[alt.Tooltip('date:T', format='%Y-%m-%d', title='Date'), 586 alt.Tooltip('cost:Q', format='$,', title='Cost')]587 ).properties(588 height=300,589 background='#1a1d24'590 ).configure_axis(591 gridColor='#2d3139',592 domainColor='#2d3139'593 ).configure_view(594 strokeWidth=0595 )596 597 st.altair_chart(line_chart, use_container_width=True)598 599 # Bar chart - Top services600 st.subheader("๐ Top Cost-Generating Services")601 602 top_services = data_manager.get_top_services(8)603 604 bar_chart = alt.Chart(top_services).mark_bar(605 cornerRadiusEnd=4606 ).encode(607 x=alt.X('cost:Q', title='Total Cost ($)', axis=alt.Axis(labelColor='#8b949e')),608 y=alt.Y('service:N', title='Service', sort='-x', axis=alt.Axis(labelColor='#8b949e')),609 color=alt.Color('cost:Q', scale=alt.Scale(scheme='viridis'), legend=None),610 tooltip=[alt.Tooltip('service:N', title='Service'), 611 alt.Tooltip('cost:Q', format='$,', title='Total Cost')]612 ).properties(613 height=300,614 background='#1a1d24'615 ).configure_axis(616 gridColor='#2d3139',617 domainColor='#2d3139'618 ).configure_view(619 strokeWidth=0620 )621 622 st.altair_chart(bar_chart, use_container_width=True)623 624 # Anomaly alert section625 anomalies = data_manager.detect_anomalies()626 if not anomalies.empty:627 st.markdown("---")628 st.subheader("๐จ Recent Anomalies Detected")629 for _, row in anomalies.head(3).iterrows():630 direction = "๐ Spike" if row['z_score'] > 0 else "๐ Drop"631 st.warning(f"{direction} on {row['date'].strftime('%Y-%m-%d')}: ${row['cost']:,.2f}")632 633 634def render_chat_panel(agent: FinOpsAgent):635 """Render the right panel chat interface"""636 st.subheader("๐ค AI Cost Analyst")637 638 # Quick action buttons639 st.markdown("**Quick Queries:**")640 cols = st.columns(2)641 quick_queries = [642 "Find anomalies this week",643 "What was yesterday's cost?",644 "Top 3 services by spend",645 "Why did my bill spike?"646 ]647 648 for i, query in enumerate(quick_queries):649 with cols[i % 2]:650 if st.button(query, key=f"quick_{i}", use_container_width=True):651 st.session_state.pending_query = query652 st.rerun()653 654 st.markdown("---")655 656 # Chat history container657 chat_container = st.container()658 659 # Initialize chat history660 if 'chat_history' not in st.session_state:661 st.session_state.chat_history = []662 # Welcome message663 welcome_msg = ("๐ Hi! I'm your FinOps Buddy. I can help you analyze cloud costs, "664 "detect anomalies, and explain spending patterns. What would you like to know?")665 st.session_state.chat_history.append(("ai", welcome_msg))666 667 # Display chat history668 with chat_container:669 for role, message in st.session_state.chat_history:670 if role == "user":671 st.markdown(f'<div class="user-message">{message}</div>', unsafe_allow_html=True)672 else:673 st.markdown(f'<div class="ai-message">{message}</div>', unsafe_allow_html=True)674 st.markdown('<div style="clear: both;"></div>', unsafe_allow_html=True)675 676 # Input area677 st.markdown("---")678 679 # Check for pending query from quick buttons680 if 'pending_query' in st.session_state:681 user_input = st.session_state.pending_query682 del st.session_state.pending_query683 # Process immediately684 st.session_state.chat_history.append(("user", user_input))685 with st.spinner("Analyzing..."):686 response = agent.process_query(user_input)687 st.session_state.chat_history.append(("ai", response))688 st.rerun()689 else:690 # Regular text input691 with st.form(key="chat_form", clear_on_submit=True):692 cols = st.columns([4, 1])693 with cols[0]:694 user_input = st.text_input("Ask about your cloud costs...", 695 placeholder="e.g., 'Why did my bill spike yesterday?'",696 label_visibility="collapsed")697 with cols[1]:698 submit = st.form_submit_button("Send", use_container_width=True)699 700 if submit and user_input:701 st.session_state.chat_history.append(("user", user_input))702 with st.spinner("Analyzing..."):703 response = agent.process_query(user_input)704 st.session_state.chat_history.append(("ai", response))705 st.rerun()706 707 # Clear chat button708 if st.button("Clear Chat", type="secondary"):709 st.session_state.chat_history = []710 st.rerun()711 712 713def render_sidebar():714 """Render sidebar with settings and info"""715 with st.sidebar:716 st.title("โ๏ธ Settings")717 718 st.markdown("### Data Source")719 if os.getenv("DATABASE_URL"):720 st.success("PostgreSQL")721 elif os.path.exists("cloud_costs.csv"):722 st.info("CSV File")723 else:724 st.warning("Mock Data")725 726 st.markdown("### AI Configuration")727 if os.getenv("OPENAI_API_KEY"):728 st.success("OpenAI GPT-4 Ready")729 else:730 st.error("OpenAI API Key not set")731 st.markdown("""732 Set environment variable:733 ```bash734 export OPENAI_API_KEY="your-key"735 ```736 """)737 738 st.markdown("---")739 st.markdown("### About")740 st.markdown("""741 **FinOps Buddy** helps you:742 - Monitor cloud spending743 - Detect cost anomalies744 - Analyze service usage745 - Get AI-powered insights746 747 Built with Streamlit + OpenAI748 """)749 750 st.markdown("---")751 st.markdown("### Export Data")752 if st.button("Download CSV"):753 csv = st.session_state.data_manager.df.to_csv(index=False)754 st.download_button(755 label="Click to Download",756 data=csv,757 file_name="cloud_costs_export.csv",758 mime="text/csv"759 )760 761 762# =============================================================================763# MAIN APPLICATION764# =============================================================================765 766def main():767 """Main application entry point"""768 769 # Initialize data manager (singleton pattern using session state)770 if 'data_manager' not in st.session_state:771 st.session_state.data_manager = DataManager()772 773 data_manager = st.session_state.data_manager774 775 # Initialize AI agent776 if 'agent' not in st.session_state:777 st.session_state.agent = FinOpsAgent(data_manager)778 779 agent = st.session_state.agent780 781 # Render UI782 render_header()783 render_sidebar()784 785 # Main layout: Dashboard (left) | Chat (right)786 left_col, right_col = st.columns([1.5, 1])787 788 with left_col:789 render_dashboard(data_manager)790 791 with right_col:792 render_chat_panel(agent)793 794 795if __name__ == "__main__":796 main()