CoolFace
Apppublic

warisali128/FinOpsBuddy

sourceHugging Facemitupdated 7mo agoView on Hugging Face
0likes
app.py796 linesDownload Raw Back to root
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()