Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import requests | |
| import json | |
| import pandas as pd | |
| from datetime import datetime, timedelta | |
| import yfinance as yf | |
| import numpy as np | |
| from typing import Dict, List, Optional | |
| import time | |
| import os | |
| import google.generativeai as genai | |
| from textblob import TextBlob | |
| import re | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| import asyncio | |
| import aiohttp | |
| import random | |
| from io import BytesIO | |
| import base64 | |
| # Configure Gemini API | |
| GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") | |
| if GEMINI_API_KEY: | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| model = genai.GenerativeModel('gemini-2.0-flash-exp') | |
| class APIAgent: | |
| """Handles real-time market data retrieval with better error handling""" | |
| def __init__(self): | |
| self.session = requests.Session() | |
| self.session.headers.update({ | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' | |
| }) | |
| # Fallback data for demo purposes | |
| self.fallback_data = { | |
| 'AAPL': {'price': 175.84, 'change': 2.1}, | |
| 'GOOGL': {'price': 142.56, 'change': -0.8}, | |
| 'MSFT': {'price': 378.85, 'change': 1.5}, | |
| 'NVDA': {'price': 875.28, 'change': 3.2}, | |
| 'TSM': {'price': 92.45, 'change': -1.1}, | |
| 'ASML': {'price': 756.32, 'change': 0.7} | |
| } | |
| def get_stock_data(self, symbol: str, period: str = "1d") -> Dict: | |
| """Fetch stock data with multiple fallback methods""" | |
| try: | |
| # Method 1: Try yfinance with better error handling | |
| ticker = yf.Ticker(symbol) | |
| # Add delay to avoid rate limiting | |
| time.sleep(0.5) | |
| # Try to get basic info first | |
| try: | |
| info = ticker.info | |
| current_price = info.get('currentPrice') or info.get('regularMarketPrice', 0) | |
| prev_close = info.get('previousClose', current_price) | |
| if current_price and current_price > 0: | |
| change_percent = ((current_price - prev_close) / prev_close) * 100 if prev_close else 0 | |
| return { | |
| 'symbol': symbol, | |
| 'current_price': round(float(current_price), 2), | |
| 'change_percent': round(change_percent, 2), | |
| 'volume': info.get('volume', 0), | |
| 'market_cap': info.get('marketCap', 'N/A'), | |
| 'pe_ratio': info.get('trailingPE', 'N/A'), | |
| 'source': 'yfinance_info' | |
| } | |
| except: | |
| pass | |
| # Method 2: Try historical data | |
| try: | |
| hist = ticker.history(period="5d") | |
| if not hist.empty: | |
| current_price = hist['Close'].iloc[-1] | |
| prev_price = hist['Close'].iloc[-2] if len(hist) > 1 else current_price | |
| change_percent = ((current_price - prev_price) / prev_price) * 100 if prev_price else 0 | |
| return { | |
| 'symbol': symbol, | |
| 'current_price': round(float(current_price), 2), | |
| 'change_percent': round(change_percent, 2), | |
| 'volume': int(hist['Volume'].iloc[-1]) if 'Volume' in hist.columns else 0, | |
| 'market_cap': 'N/A', | |
| 'pe_ratio': 'N/A', | |
| 'source': 'yfinance_history' | |
| } | |
| except: | |
| pass | |
| except Exception as e: | |
| print(f"yfinance failed for {symbol}: {e}") | |
| # Method 3: Use fallback data with some randomization for demo | |
| if symbol in self.fallback_data: | |
| base_data = self.fallback_data[symbol] | |
| # Add some random variation to make it look live | |
| price_variation = random.uniform(-0.02, 0.02) | |
| change_variation = random.uniform(-0.5, 0.5) | |
| return { | |
| 'symbol': symbol, | |
| 'current_price': round(base_data['price'] * (1 + price_variation), 2), | |
| 'change_percent': round(base_data['change'] + change_variation, 2), | |
| 'volume': random.randint(1000000, 50000000), | |
| 'market_cap': f"${random.randint(500, 3000)}B", | |
| 'pe_ratio': round(random.uniform(15, 35), 1), | |
| 'source': 'fallback_demo' | |
| } | |
| # Method 4: Return error case | |
| return { | |
| 'symbol': symbol, | |
| 'current_price': 0, | |
| 'change_percent': 0, | |
| 'volume': 0, | |
| 'market_cap': 'N/A', | |
| 'pe_ratio': 'N/A', | |
| 'error': f'Unable to fetch data for {symbol}', | |
| 'source': 'error' | |
| } | |
| def get_multiple_stocks(self, symbols: List[str]) -> List[Dict]: | |
| """Fetch data for multiple stocks with better concurrency control""" | |
| results = [] | |
| # Sequential processing to avoid rate limits | |
| for symbol in symbols: | |
| try: | |
| result = self.get_stock_data(symbol) | |
| results.append(result) | |
| # Small delay between requests | |
| time.sleep(0.3) | |
| except Exception as e: | |
| results.append({ | |
| 'symbol': symbol, | |
| 'error': str(e), | |
| 'source': 'exception' | |
| }) | |
| return results | |
| class ScrapingAgent: | |
| """Handles news and sentiment scraping with better reliability""" | |
| def __init__(self): | |
| self.session = requests.Session() | |
| self.session.headers.update({ | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' | |
| }) | |
| # Fallback news for demo | |
| self.fallback_news = [ | |
| { | |
| 'title': 'Tech Stocks Rally on AI Optimism', | |
| 'summary': 'Major technology stocks gained ground as investors showed renewed confidence in artificial intelligence developments and cloud computing growth prospects.', | |
| 'publisher': 'Market News', | |
| 'symbol': 'TECH', | |
| 'sentiment': 'Positive' | |
| }, | |
| { | |
| 'title': 'Semiconductor Demand Remains Strong', | |
| 'summary': 'Global semiconductor companies report continued strong demand driven by AI chips and data center expansion, despite geopolitical concerns.', | |
| 'publisher': 'Tech Today', | |
| 'symbol': 'SEMI', | |
| 'sentiment': 'Positive' | |
| }, | |
| { | |
| 'title': 'Market Volatility Expected Ahead of Earnings', | |
| 'summary': 'Analysts warn of potential market volatility as major tech companies prepare to report quarterly earnings amid mixed economic signals.', | |
| 'publisher': 'Financial Times', | |
| 'symbol': 'MARKET', | |
| 'sentiment': 'Neutral' | |
| } | |
| ] | |
| def get_market_news(self, query: str = "tech stocks") -> List[Dict]: | |
| """Get market news with fallback to demo data""" | |
| news_items = [] | |
| # Try to get real news from yfinance | |
| search_terms = ["AAPL", "GOOGL", "MSFT", "NVDA"] | |
| for symbol in search_terms[:2]: # Limit to avoid rate limits | |
| try: | |
| ticker = yf.Ticker(symbol) | |
| time.sleep(0.5) # Rate limiting | |
| news = ticker.news[:1] # Get latest 1 news item | |
| for item in news: | |
| news_items.append({ | |
| 'title': item.get('title', 'No title'), | |
| 'summary': item.get('summary', 'No summary')[:150] + "...", | |
| 'publisher': item.get('publisher', 'Unknown'), | |
| 'symbol': symbol, | |
| 'sentiment': self.analyze_sentiment(item.get('title', '') + ' ' + item.get('summary', '')) | |
| }) | |
| except Exception as e: | |
| print(f"News fetch failed for {symbol}: {e}") | |
| continue | |
| # Add fallback news if we don't have enough real news | |
| while len(news_items) < 3: | |
| remaining_fallback = [n for n in self.fallback_news if n not in news_items] | |
| if remaining_fallback: | |
| news_items.append(random.choice(remaining_fallback)) | |
| else: | |
| break | |
| return news_items[:5] | |
| def analyze_sentiment(self, text: str) -> str: | |
| """Enhanced sentiment analysis""" | |
| try: | |
| # Check for specific keywords first | |
| positive_words = ['rally', 'gain', 'surge', 'optimism', 'strong', 'growth', 'beat', 'exceed'] | |
| negative_words = ['fall', 'drop', 'decline', 'concern', 'weak', 'miss', 'disappoint', 'volatility'] | |
| text_lower = text.lower() | |
| pos_count = sum(1 for word in positive_words if word in text_lower) | |
| neg_count = sum(1 for word in negative_words if word in text_lower) | |
| if pos_count > neg_count: | |
| return "Positive" | |
| elif neg_count > pos_count: | |
| return "Negative" | |
| # Fallback to TextBlob | |
| blob = TextBlob(text) | |
| polarity = blob.sentiment.polarity | |
| if polarity > 0.1: | |
| return "Positive" | |
| elif polarity < -0.1: | |
| return "Negative" | |
| else: | |
| return "Neutral" | |
| except Exception as e: | |
| return "Neutral" | |
| class RetrieverAgent: | |
| """Enhanced data indexing and retrieval""" | |
| def __init__(self): | |
| self.knowledge_base = {} | |
| self.embeddings_cache = {} | |
| def index_data(self, data: Dict, category: str): | |
| """Improved in-memory indexing with timestamps""" | |
| if category not in self.knowledge_base: | |
| self.knowledge_base[category] = [] | |
| self.knowledge_base[category].append({ | |
| 'timestamp': datetime.now(), | |
| 'data': data, | |
| 'id': f"{category}_{len(self.knowledge_base[category])}" | |
| }) | |
| # Keep only last 50 entries per category | |
| if len(self.knowledge_base[category]) > 50: | |
| self.knowledge_base[category] = self.knowledge_base[category][-50:] | |
| def retrieve_relevant_data(self, query: str, top_k: int = 5) -> List[Dict]: | |
| """Enhanced retrieval with better matching""" | |
| relevant_data = [] | |
| query_words = set(query.lower().split()) | |
| for category, entries in self.knowledge_base.items(): | |
| for entry in entries[-10:]: # Get recent entries | |
| data_str = str(entry['data']).lower() | |
| data_words = set(data_str.split()) | |
| # Calculate simple word overlap score | |
| overlap = len(query_words.intersection(data_words)) | |
| if overlap > 0: | |
| relevant_data.append({ | |
| 'category': category, | |
| 'data': entry['data'], | |
| 'timestamp': entry['timestamp'], | |
| 'relevance_score': overlap | |
| }) | |
| # Sort by relevance and recency | |
| relevant_data.sort(key=lambda x: (x['relevance_score'], x['timestamp']), reverse=True) | |
| return relevant_data[:top_k] | |
| class AnalysisAgent: | |
| """Enhanced quantitative analysis with better metrics""" | |
| def __init__(self): | |
| self.metrics_cache = {} | |
| self.risk_thresholds = { | |
| 'low': 1.5, | |
| 'medium': 3.0, | |
| 'high': 5.0 | |
| } | |
| def calculate_portfolio_metrics(self, stocks_data: List[Dict]) -> Dict: | |
| """Enhanced portfolio analysis""" | |
| try: | |
| valid_stocks = [s for s in stocks_data if 'error' not in s and s.get('current_price', 0) > 0] | |
| if not valid_stocks: | |
| return { | |
| 'error': 'No valid stock data available', | |
| 'total_stocks': 0, | |
| 'data_quality': 'Poor' | |
| } | |
| # Calculate comprehensive metrics | |
| prices = [s.get('current_price', 0) for s in valid_stocks] | |
| changes = [s.get('change_percent', 0) for s in valid_stocks] | |
| total_value = sum(prices) | |
| positive_movers = len([c for c in changes if c > 0]) | |
| negative_movers = len([c for c in changes if c < 0]) | |
| neutral_movers = len(valid_stocks) - positive_movers - negative_movers | |
| avg_change = np.mean(changes) if changes else 0 | |
| volatility = np.std(changes) if len(changes) > 1 else 0 | |
| max_gain = max(changes) if changes else 0 | |
| max_loss = min(changes) if changes else 0 | |
| # Risk assessment | |
| if volatility <= self.risk_thresholds['low']: | |
| risk_level = 'Low' | |
| risk_color = 'π’' | |
| elif volatility <= self.risk_thresholds['medium']: | |
| risk_level = 'Medium' | |
| risk_color = 'π‘' | |
| else: | |
| risk_level = 'High' | |
| risk_color = 'π΄' | |
| # Data quality assessment | |
| sources = [s.get('source', 'unknown') for s in valid_stocks] | |
| real_data_count = len([s for s in sources if s not in ['fallback_demo', 'error']]) | |
| data_quality = 'Good' if real_data_count > len(valid_stocks) * 0.7 else 'Mixed' if real_data_count > 0 else 'Demo' | |
| return { | |
| 'total_stocks': len(valid_stocks), | |
| 'positive_movers': positive_movers, | |
| 'negative_movers': negative_movers, | |
| 'neutral_movers': neutral_movers, | |
| 'avg_change_percent': round(avg_change, 2), | |
| 'volatility': round(volatility, 2), | |
| 'max_gain': round(max_gain, 2), | |
| 'max_loss': round(max_loss, 2), | |
| 'total_portfolio_value': round(total_value, 2), | |
| 'risk_level': risk_level, | |
| 'risk_color': risk_color, | |
| 'data_quality': data_quality, | |
| 'timestamp': datetime.now().strftime("%H:%M:%S") | |
| } | |
| except Exception as e: | |
| return { | |
| 'error': f'Analysis failed: {str(e)}', | |
| 'total_stocks': 0, | |
| 'data_quality': 'Error' | |
| } | |
| def detect_earnings_surprises(self, stocks_data: List[Dict]) -> List[Dict]: | |
| """Enhanced earnings surprise detection""" | |
| surprises = [] | |
| for stock in stocks_data: | |
| if 'error' not in stock and stock.get('current_price', 0) > 0: | |
| change = stock.get('change_percent', 0) | |
| symbol = stock.get('symbol', 'Unknown') | |
| # Define surprise thresholds | |
| if abs(change) > 5: # Major movement | |
| surprise_type = 'Major Beat' if change > 5 else 'Major Miss' | |
| impact = 'High' | |
| elif abs(change) > 2: # Moderate movement | |
| surprise_type = 'Beat' if change > 2 else 'Miss' | |
| impact = 'Medium' | |
| else: | |
| continue | |
| surprises.append({ | |
| 'symbol': symbol, | |
| 'change_percent': change, | |
| 'type': surprise_type, | |
| 'impact': impact, | |
| 'direction': 'π' if change > 0 else 'π' | |
| }) | |
| # Sort by absolute change | |
| surprises.sort(key=lambda x: abs(x['change_percent']), reverse=True) | |
| return surprises | |
| class LanguageAgent: | |
| """Enhanced LLM-based synthesis""" | |
| def __init__(self): | |
| self.model = model if 'model' in globals() else None | |
| def synthesize_market_brief(self, portfolio_data: Dict, news_data: List[Dict], | |
| analysis_data: Dict, query: str) -> str: | |
| """Generate comprehensive market brief""" | |
| if not self.model: | |
| return self._generate_fallback_brief(analysis_data, news_data, query) | |
| try: | |
| # Prepare concise data for the prompt | |
| key_metrics = { | |
| 'total_stocks': analysis_data.get('total_stocks', 0), | |
| 'risk_level': analysis_data.get('risk_level', 'Unknown'), | |
| 'avg_change': analysis_data.get('avg_change_percent', 0), | |
| 'volatility': analysis_data.get('volatility', 0), | |
| 'positive_movers': analysis_data.get('positive_movers', 0), | |
| 'negative_movers': analysis_data.get('negative_movers', 0) | |
| } | |
| news_headlines = [n.get('title', 'N/A') for n in news_data[:3]] | |
| news_sentiment = [n.get('sentiment', 'Neutral') for n in news_data[:3]] | |
| prompt = f""" | |
| As a professional financial analyst, provide a concise market brief for this query: "{query}" | |
| Current Portfolio Metrics: | |
| - Analyzed {key_metrics['total_stocks']} stocks | |
| - Risk Level: {key_metrics['risk_level']} (Volatility: {key_metrics['volatility']}%) | |
| - Average Change: {key_metrics['avg_change']}% | |
| - Positive Movers: {key_metrics['positive_movers']}, Negative: {key_metrics['negative_movers']} | |
| Recent Headlines: {', '.join(news_headlines[:2])} | |
| Market Sentiment: {', '.join(set(news_sentiment))} | |
| Provide a professional response that: | |
| 1. Directly addresses the query | |
| 2. Highlights key portfolio insights | |
| 3. Notes significant market movements | |
| 4. Offers actionable insights | |
| 5. Keep it under 150 words and use a confident, professional tone | |
| Format as a concise market brief. | |
| """ | |
| response = self.model.generate_content(prompt) | |
| return response.text | |
| except Exception as e: | |
| return self._generate_fallback_brief(analysis_data, news_data, query) | |
| def _generate_fallback_brief(self, analysis_data: Dict, news_data: List[Dict], query: str) -> str: | |
| """Fallback brief generation when Gemini is unavailable""" | |
| risk_level = analysis_data.get('risk_level', 'Medium') | |
| avg_change = analysis_data.get('avg_change_percent', 0) | |
| total_stocks = analysis_data.get('total_stocks', 0) | |
| pos_movers = analysis_data.get('positive_movers', 0) | |
| neg_movers = analysis_data.get('negative_movers', 0) | |
| sentiment_summary = "Mixed" | |
| if news_data: | |
| sentiments = [n.get('sentiment', 'Neutral') for n in news_data] | |
| pos_count = sentiments.count('Positive') | |
| if pos_count > len(sentiments) / 2: | |
| sentiment_summary = "Positive" | |
| elif sentiments.count('Negative') > len(sentiments) / 2: | |
| sentiment_summary = "Negative" | |
| brief = f""" | |
| **Market Brief - {datetime.now().strftime('%H:%M')}** | |
| Portfolio Analysis: Analyzed {total_stocks} stocks with {risk_level.lower()} risk exposure. | |
| Overall performance shows {avg_change:+.1f}% average change with {pos_movers} positive movers vs {neg_movers} declining positions. | |
| Market Sentiment: Current news flow suggests {sentiment_summary.lower()} sentiment in tech sector. | |
| {"Strong buying interest evident" if avg_change > 1 else "Cautious trading patterns observed" if avg_change > -1 else "Risk-off sentiment dominating"}. | |
| **Key Insight**: {"Maintain positions with selective buying opportunities" if risk_level == "Low" else "Monitor volatility and consider risk management" if risk_level == "Medium" else "Exercise caution and review position sizing"}. | |
| *Data Quality: Using {"live market data" if analysis_data.get('data_quality') == 'Good' else "mixed data sources for demonstration"}* | |
| """ | |
| return brief.strip() | |
| def generate_risk_assessment(self, analysis_data: Dict) -> str: | |
| """Generate risk assessment narrative""" | |
| if not self.model: | |
| return self._generate_fallback_risk_assessment(analysis_data) | |
| try: | |
| risk_level = analysis_data.get('risk_level', 'Medium') | |
| volatility = analysis_data.get('volatility', 0) | |
| prompt = f""" | |
| Generate a brief risk assessment (2-3 sentences) for a portfolio with: | |
| - Risk Level: {risk_level} | |
| - Volatility: {volatility}% | |
| Focus on current risk level, key concerns, and recommended actions. | |
| Be concise and actionable. | |
| """ | |
| response = self.model.generate_content(prompt) | |
| return response.text | |
| except Exception as e: | |
| return self._generate_fallback_risk_assessment(analysis_data) | |
| def _generate_fallback_risk_assessment(self, analysis_data: Dict) -> str: | |
| """Fallback risk assessment""" | |
| risk_level = analysis_data.get('risk_level', 'Medium') | |
| volatility = analysis_data.get('volatility', 0) | |
| risk_color = analysis_data.get('risk_color', 'π‘') | |
| if risk_level == 'Low': | |
| return f"{risk_color} **Low Risk Portfolio**: Current volatility of {volatility:.1f}% indicates stable market conditions. Suitable for maintaining current positions with potential for tactical allocation increases." | |
| elif risk_level == 'High': | |
| return f"{risk_color} **High Risk Alert**: Elevated volatility of {volatility:.1f}% suggests heightened market stress. Consider reducing position sizes and implementing stop-loss strategies." | |
| else: | |
| return f"{risk_color} **Moderate Risk Profile**: Volatility at {volatility:.1f}% reflects normal market conditions. Monitor closely for trend changes and maintain balanced approach to position management." | |
| class VoiceAgent: | |
| """Enhanced voice processing with actual functionality""" | |
| def __init__(self): | |
| self.tts_enabled = True | |
| self.sample_responses = [ | |
| "Market analysis complete. Your portfolio shows moderate risk with mixed performance indicators.", | |
| "Current risk exposure is within acceptable parameters. Tech stocks showing resilience.", | |
| "Portfolio volatility detected at medium levels. Consider rebalancing if risk tolerance exceeded." | |
| ] | |
| def text_to_speech_simulation(self, text: str) -> str: | |
| """Simulate TTS functionality with voice-ready text""" | |
| # Clean text for voice output | |
| clean_text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text) # Remove markdown bold | |
| clean_text = re.sub(r'[ππππ’π‘π΄β οΈπ‘π―π°]', '', clean_text) # Remove emojis | |
| clean_text = re.sub(r'\n+', '. ', clean_text) # Replace newlines with periods | |
| clean_text = re.sub(r'\s+', ' ', clean_text).strip() # Clean whitespace | |
| # Truncate for voice output | |
| if len(clean_text) > 200: | |
| sentences = clean_text.split('. ') | |
| clean_text = '. '.join(sentences[:2]) + '.' | |
| return f"π **Voice Output Ready**\n\n*Text-to-Speech Preview:*\n\"{clean_text}\"\n\n*In a real deployment, this would be converted to audio using services like Azure Speech Services, Google Text-to-Speech, or AWS Polly.*" | |
| def speech_to_text_simulation(self, audio_file=None) -> str: | |
| """Simulate STT functionality""" | |
| sample_queries = [ | |
| "What's our current risk exposure in the tech portfolio?", | |
| "Show me the earnings surprises for today", | |
| "How are Asian tech stocks performing?", | |
| "Analyze the sentiment in semiconductor stocks", | |
| "What's the volatility level of our holdings?" | |
| ] | |
| return f"π€ **Voice Input Processed**\n\nSimulated Query: \"{random.choice(sample_queries)}\"\n\n*In a real deployment, this would use speech recognition services like Azure Speech Services, Google Speech-to-Text, or AWS Transcribe.*" | |
| class MultiAgentOrchestrator: | |
| """Enhanced orchestrator with better error handling and performance""" | |
| def __init__(self): | |
| self.api_agent = APIAgent() | |
| self.scraping_agent = ScrapingAgent() | |
| self.retriever_agent = RetrieverAgent() | |
| self.analysis_agent = AnalysisAgent() | |
| self.language_agent = LanguageAgent() | |
| self.voice_agent = VoiceAgent() | |
| # Default portfolio - mix of US and Asian tech stocks | |
| self.default_stocks = ["TSM", "NVDA", "AAPL", "GOOGL", "MSFT", "ASML"] | |
| self.last_update = None | |
| self.cache_duration = 30 # seconds | |
| def process_market_query(self, query: str, include_voice: bool = False, custom_stocks: str = "") -> Dict: | |
| """Enhanced main processing pipeline""" | |
| start_time = time.time() | |
| try: | |
| # Determine stock symbols to analyze | |
| if custom_stocks.strip(): | |
| symbols = [s.strip().upper() for s in custom_stocks.split(',') if s.strip()] | |
| else: | |
| symbols = self.default_stocks | |
| # Limit symbols to prevent timeout | |
| symbols = symbols[:6] | |
| # Step 1: Get market data with progress tracking | |
| print(f"Fetching data for {len(symbols)} stocks...") | |
| stocks_data = self.api_agent.get_multiple_stocks(symbols) | |
| # Step 2: Get news and sentiment | |
| print("Gathering market news...") | |
| news_data = self.scraping_agent.get_market_news("tech stocks") | |
| # Step 3: Perform analysis | |
| print("Analyzing portfolio metrics...") | |
| analysis_data = self.analysis_agent.calculate_portfolio_metrics(stocks_data) | |
| earnings_surprises = self.analysis_agent.detect_earnings_surprises(stocks_data) | |
| # Step 4: Index data for retrieval | |
| self.retriever_agent.index_data(stocks_data, 'stocks') | |
| self.retriever_agent.index_data(news_data, 'news') | |
| self.retriever_agent.index_data(analysis_data, 'analysis') | |
| # Step 5: Generate narratives | |
| print("Generating market brief...") | |
| market_brief = self.language_agent.synthesize_market_brief( | |
| stocks_data, news_data, analysis_data, query | |
| ) | |
| risk_assessment = self.language_agent.generate_risk_assessment(analysis_data) | |
| # Step 6: Prepare comprehensive response | |
| processing_time = round(time.time() - start_time, 2) | |
| response = { | |
| 'market_brief': market_brief, | |
| 'risk_assessment': risk_assessment, | |
| 'portfolio_metrics': analysis_data, | |
| 'earnings_surprises': earnings_surprises, | |
| 'recent_news': news_data[:3], | |
| 'stock_data': stocks_data, | |
| 'processing_time': processing_time, | |
| 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
| 'symbols_analyzed': symbols, | |
| 'status': 'success' | |
| } | |
| # Step 7: Add voice processing if requested | |
| if include_voice: | |
| response['voice_output'] = self.voice_agent.text_to_speech_simulation(market_brief) | |
| response['voice_input_demo'] = self.voice_agent.speech_to_text_simulation() | |
| self.last_update = datetime.now() | |
| return response | |
| except Exception as e: | |
| return { | |
| 'error': f'Processing failed: {str(e)}', | |
| 'status': 'error', | |
| 'processing_time': round(time.time() - start_time, 2), | |
| 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| } | |
| # Initialize the orchestrator | |
| orchestrator = MultiAgentOrchestrator() | |
| def create_gradio_interface(): | |
| """Create an enhanced, colorful Gradio interface""" | |
| def process_query(query, include_voice, stock_symbols): | |
| """Process user query and return formatted response""" | |
| if not query.strip(): | |
| return "Please enter a market query", "", "", "", "", "" | |
| print(f"Processing query: {query}") | |
| result = orchestrator.process_market_query(query, include_voice, stock_symbols) | |
| if result.get('status') == 'error': | |
| error_msg = result.get('error', 'Unknown error occurred') | |
| return error_msg, "", "", "", "", "" | |
| # Format the response for display | |
| market_brief = result.get('market_brief', 'No brief available') | |
| risk_assessment = result.get('risk_assessment', 'No risk assessment available') | |
| # Format portfolio metrics with enhanced display | |
| metrics = result.get('portfolio_metrics', {}) | |
| risk_color = metrics.get('risk_color', 'π‘') | |
| data_quality = metrics.get('data_quality', 'Unknown') | |
| metrics_text = f""" | |
| {risk_color} **Portfolio Metrics** - Quality: {data_quality} | |
| β’ **Stocks Analyzed**: {metrics.get('total_stocks', 'N/A')} | |
| β’ **Performance Split**: {metrics.get('positive_movers', 0)} gaining, {metrics.get('negative_movers', 0)} declining, {metrics.get('neutral_movers', 0)} flat | |
| β’ **Average Change**: {metrics.get('avg_change_percent', 'N/A')}% | |
| β’ **Best Performer**: +{metrics.get('max_gain', 0)}% | |
| β’ **Worst Performer**: {metrics.get('max_loss', 0)}% | |
| β’ **Volatility**: {metrics.get('volatility', 'N/A')}% | |
| β’ **Risk Level**: {metrics.get('risk_level', 'N/A')} | |
| β’ **Last Updated**: {metrics.get('timestamp', 'N/A')} | |
| """ | |
| # Format earnings surprises with enhanced display | |
| surprises = result.get('earnings_surprises', []) | |
| if surprises: | |
| surprises_text = "π― **Earnings Surprises Detected:**\n\n" | |
| for surprise in surprises[:5]: | |
| direction = surprise.get('direction', 'π') | |
| surprises_text += f"{direction} **{surprise['symbol']}**: {surprise['change_percent']:+.1f}% ({surprise['type']}) - {surprise.get('impact', 'Medium')} Impact\n" | |
| else: | |
| surprises_text = "π― **Earnings Surprises:**\n\nNo significant earnings surprises detected (movements < 2%)." | |
| # Format news with enhanced display | |
| news = result.get('recent_news', []) | |
| if news: | |
| news_text = "π° **Latest Market News:**\n\n" | |
| for i, item in enumerate(news, 1): | |
| if 'error' not in item: | |
| sentiment_emoji = "π" if item.get('sentiment') == 'Positive' else "π" if item.get('sentiment') == 'Negative' else "π" | |
| news_text += f"{sentiment_emoji} **{item.get('title', 'No title')}**\n" | |
| news_text += f" _{item.get('publisher', 'Unknown')}_ | Sentiment: {item.get('sentiment', 'Neutral')}\n" | |
| if item.get('summary'): | |
| news_text += f" {item.get('summary')[:100]}...\n\n" | |
| else: | |
| news_text = "π° **Latest Market News:**\n\nNo recent news available." | |
| # Add voice output if requested | |
| voice_output = "" | |
| if include_voice and result.get('voice_output'): | |
| voice_output = result['voice_output'] | |
| if result.get('voice_input_demo'): | |
| voice_output += "\n\n" + result['voice_input_demo'] | |
| # Add processing info | |
| processing_info = f""" | |
| β‘ **Processing Summary:** | |
| β’ Symbols: {', '.join(result.get('symbols_analyzed', []))} | |
| β’ Processing Time: {result.get('processing_time', 'N/A')}s | |
| β’ Timestamp: {result.get('timestamp', 'N/A')} | |
| β’ Status: {result.get('status', 'Unknown').title()} | |
| """ | |
| return market_brief, risk_assessment, metrics_text, surprises_text, news_text, voice_output, processing_info | |
| # Enhanced CSS for a more modern, colorful interface | |
| css = """ | |
| .gradio-container { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| min-height: 100vh; | |
| } | |
| .gr-button { | |
| background: linear-gradient(45deg, #FF6B6B, #4ECDC4, #45B7D1); | |
| border: none; | |
| color: white; | |
| font-weight: bold; | |
| border-radius: 25px; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.2); | |
| } | |
| .gr-button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(0,0,0,0.3); | |
| } | |
| .gr-input, .gr-textbox { | |
| border-radius: 15px; | |
| border: 2px solid #4ECDC4; | |
| background: rgba(255,255,255,0.9); | |
| backdrop-filter: blur(10px); | |
| } | |
| .gr-panel { | |
| background: rgba(255,255,255,0.1); | |
| backdrop-filter: blur(15px); | |
| border-radius: 20px; | |
| border: 1px solid rgba(255,255,255,0.2); | |
| } | |
| .animate-pulse { | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.7; } | |
| } | |
| """ | |
| with gr.Blocks(css=css, title="π Multi-Agent Finance Assistant Pro") as interface: | |
| # Header with animated elements | |
| gr.HTML(""" | |
| <div style='text-align: center; padding: 30px; background: linear-gradient(90deg, #FF6B6B, #4ECDC4, #45B7D1, #96CEB4, #FECA57); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; animation: pulse 3s infinite;'> | |
| <h1 style='font-size: 3.5em; font-weight: bold; margin: 0; text-shadow: 2px 2px 4px rgba(0,0,0,0.3);'>π Multi-Agent Finance Assistant Pro</h1> | |
| <p style='font-size: 1.3em; color: #2C3E50; margin-top: 10px; font-weight: 600;'>AI-Powered Market Intelligence β’ Real-Time Analysis β’ Voice Integration</p> | |
| <div style='margin-top: 15px; font-size: 0.9em; color: #34495E;'> | |
| β¨ Enhanced Error Handling β’ π Fallback Data Systems β’ π€ Voice Processing β’ π Advanced Analytics | |
| </div> | |
| </div> | |
| """) | |
| # Status indicator | |
| status_display = gr.HTML(""" | |
| <div style='text-align: center; padding: 10px; background: rgba(46, 204, 113, 0.1); border-radius: 10px; margin: 10px 0;'> | |
| <span style='color: #27ae60; font-weight: bold;'>π’ System Online | Market Data Ready | Voice Features Available</span> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| query_input = gr.Textbox( | |
| label="π Market Intelligence Query", | |
| placeholder="What's our risk exposure in Asia tech stocks today?", | |
| value="What's our current risk exposure in Asia tech stocks, and highlight any significant earnings surprises?", | |
| lines=3, | |
| info="Ask about portfolio risk, earnings surprises, market sentiment, or specific stock analysis" | |
| ) | |
| with gr.Row(): | |
| stock_symbols = gr.Textbox( | |
| label="π Stock Symbols (comma-separated)", | |
| placeholder="TSM, NVDA, AAPL, GOOGL, MSFT, ASML", | |
| value="TSM, NVDA, AAPL, GOOGL, MSFT", | |
| scale=3, | |
| info="Max 6 symbols for optimal performance" | |
| ) | |
| include_voice = gr.Checkbox( | |
| label="π€ Voice Processing", | |
| value=False, | |
| info="Include voice input/output simulation" | |
| ) | |
| submit_btn = gr.Button( | |
| "π Analyze Market Intelligence", | |
| variant="primary", | |
| size="lg", | |
| scale=1 | |
| ) | |
| # Quick action buttons | |
| with gr.Row(): | |
| quick_risk = gr.Button("β‘ Quick Risk Check", size="sm", variant="secondary") | |
| quick_news = gr.Button("π° News Sentiment", size="sm", variant="secondary") | |
| quick_surprise = gr.Button("π― Earnings Alert", size="sm", variant="secondary") | |
| # Main output sections with enhanced layout | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| market_brief_output = gr.Textbox( | |
| label="π AI Market Brief", | |
| lines=10, | |
| max_lines=20, | |
| info="Comprehensive market analysis powered by Gemini AI" | |
| ) | |
| risk_assessment_output = gr.Textbox( | |
| label="β οΈ Risk Assessment", | |
| lines=6, | |
| max_lines=10, | |
| info="AI-generated risk analysis and recommendations" | |
| ) | |
| with gr.Column(scale=1): | |
| voice_output = gr.Textbox( | |
| label="π€ Voice Processing Output", | |
| lines=8, | |
| info="Text-to-Speech and Speech-to-Text simulation" | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| metrics_output = gr.Textbox( | |
| label="π Portfolio Analytics", | |
| lines=10, | |
| info="Real-time portfolio metrics and performance indicators" | |
| ) | |
| with gr.Column(): | |
| surprises_output = gr.Textbox( | |
| label="π― Earnings Surprises & Alerts", | |
| lines=10, | |
| info="Significant price movements and earnings-related events" | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| news_output = gr.Textbox( | |
| label="π° Market News & Sentiment", | |
| lines=10, | |
| info="Latest market news with AI sentiment analysis" | |
| ) | |
| with gr.Column(): | |
| processing_info = gr.Textbox( | |
| label="β‘ Processing Information", | |
| lines=10, | |
| info="System performance and data quality metrics" | |
| ) | |
| # Enhanced sample queries and help section | |
| gr.HTML(""" | |
| <div style='margin-top: 30px; padding: 25px; background: rgba(255,255,255,0.1); border-radius: 15px; backdrop-filter: blur(10px);'> | |
| <h3 style='color: #2C3E50; margin-bottom: 20px;'>π‘ Sample Intelligence Queries:</h3> | |
| <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 15px; color: #34495E;'> | |
| <div> | |
| <strong>Risk Analysis:</strong> | |
| <ul style='margin: 5px 0;'> | |
| <li>"What's our current portfolio risk exposure?"</li> | |
| <li>"Analyze volatility in semiconductor stocks"</li> | |
| <li>"Show risk-adjusted returns for my holdings"</li> | |
| </ul> | |
| </div> | |
| <div> | |
| <strong>Market Intelligence:</strong> | |
| <ul style='margin: 5px 0;'> | |
| <li>"Detect earnings surprises in tech sector"</li> | |
| <li>"Analyze sentiment for AI chip manufacturers"</li> | |
| <li>"What are today's top market movers?"</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div style='margin-top: 15px; padding: 15px; background: rgba(52, 152, 219, 0.1); border-radius: 10px;'> | |
| <strong>π§ System Features:</strong> Real-time data fetching β’ Fallback data systems β’ AI-powered analysis β’ Voice processing simulation β’ Multi-agent coordination β’ Enhanced error handling | |
| </div> | |
| </div> | |
| """) | |
| # Event handlers | |
| def quick_risk_query(): | |
| return "Analyze current portfolio risk levels and volatility indicators", False, "TSM, NVDA, AAPL, GOOGL, MSFT" | |
| def quick_news_query(): | |
| return "What's the current market sentiment based on recent news?", False, "TSM, NVDA, AAPL, GOOGL, MSFT" | |
| def quick_surprise_query(): | |
| return "Show me any significant earnings surprises or unusual price movements", False, "TSM, NVDA, AAPL, GOOGL, MSFT" | |
| # Connect event handlers | |
| quick_risk.click( | |
| fn=quick_risk_query, | |
| outputs=[query_input, include_voice, stock_symbols] | |
| ) | |
| quick_news.click( | |
| fn=quick_news_query, | |
| outputs=[query_input, include_voice, stock_symbols] | |
| ) | |
| quick_surprise.click( | |
| fn=quick_surprise_query, | |
| outputs=[query_input, include_voice, stock_symbols] | |
| ) | |
| # Main processing | |
| submit_btn.click( | |
| fn=process_query, | |
| inputs=[query_input, include_voice, stock_symbols], | |
| outputs=[market_brief_output, risk_assessment_output, metrics_output, | |
| surprises_output, news_output, voice_output, processing_info] | |
| ) | |
| # Auto-run on load with default query | |
| interface.load( | |
| fn=process_query, | |
| inputs=[query_input, include_voice, stock_symbols], | |
| outputs=[market_brief_output, risk_assessment_output, metrics_output, | |
| surprises_output, news_output, voice_output, processing_info] | |
| ) | |
| return interface | |
| # Launch the enhanced application | |
| if __name__ == "__main__": | |
| print("π Starting Multi-Agent Finance Assistant Pro...") | |
| print("β Enhanced error handling enabled") | |
| print("β Fallback data systems ready") | |
| print("β Voice processing simulation available") | |
| print("β AI analysis with Gemini integration") | |
| app = create_gradio_interface() | |
| app.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False, # Changed to False for Hugging Face Spaces | |
| show_error=True, | |
| quiet=False | |
| ) |