Spaces:
Sleeping
Sleeping
Yahya Darman
Resolve merge conflict, update branding to Agentic Stock Advisor (RAG), and push to main branch
9e9eccd
import gradio as gr | |
from datetime import datetime | |
import os | |
from dotenv import load_dotenv | |
import requests # Import requests for HTTP calls to Modal backend | |
import plotly.io as pio # Import plotly.io to deserialize JSON strings to Plotly figures | |
# from gradio.themes.utils import fonts | |
# from backend.stock_analyzer import StockAnalyzer | |
# from backend.config import AppConfig | |
# Load environment variables | |
load_dotenv() | |
# class BuffetBotTheme(gr.themes.Soft): | |
# def __init__(self, **kwargs): | |
# super().__init__( | |
# font=( | |
# fonts.GoogleFont("Quicksand"), | |
# "ui-sans-serif", | |
# "sans-serif", | |
# ), | |
# **kwargs | |
# ) | |
class AgenticStockAdvisorApp: | |
def __init__(self): | |
# self.config = AppConfig() | |
# self.stock_analyzer = StockAnalyzer() | |
self.mcp_server_url = os.getenv("MODAL_MCP_SERVER_URL") # Get Modal MCP server URL from environment variable | |
if not self.mcp_server_url: | |
raise ValueError("MODAL_MCP_SERVER_URL environment variable not set.") | |
def create_ui(self): | |
"""Create the Gradio interface.""" | |
custom_css = """ | |
/* General Body and Font */ | |
body { | |
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; | |
color: #333; | |
background-color: #f5f5f5; | |
} | |
/* Main Container Styling */ | |
.gradio-container { | |
max-width: 1000px; /* Slightly wider container */ | |
margin: auto; | |
padding: 30px; | |
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); | |
border-radius: 12px; | |
background-color: #ffffff; | |
margin-top: 30px; | |
margin-bottom: 30px; | |
} | |
/* Headings */ | |
h1 { | |
font-size: 2.8em; | |
color: #1a237e; /* Darker blue for prominence */ | |
text-align: center; | |
margin-bottom: 25px; | |
font-weight: 700; /* Bold */ | |
letter-spacing: -0.5px; | |
} | |
h2 { | |
font-size: 2.0em; | |
color: #3f51b5; /* Medium blue for subheadings */ | |
border-bottom: 2px solid #e8eaf6; /* Light separator */ | |
padding-bottom: 10px; | |
margin-top: 40px; | |
margin-bottom: 20px; | |
font-weight: 600; | |
} | |
h3 { | |
font-size: 1.5em; | |
color: #424242; | |
margin-top: 25px; | |
margin-bottom: 15px; | |
} | |
/* Textboxes and Inputs */ | |
.gr-textbox textarea, .gr-textbox input { | |
border: 1px solid #bdbdbd; | |
border-radius: 8px; | |
padding: 10px 15px; | |
font-size: 1.1em; | |
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); | |
} | |
.gr-textbox label { | |
font-weight: 600; | |
color: #555; | |
margin-bottom: 8px; | |
} | |
/* Buttons */ | |
.gr-button { | |
border-radius: 8px; | |
padding: 12px 25px; | |
font-size: 1.1em; | |
font-weight: 600; | |
transition: all 0.3s ease; | |
} | |
.gr-button.primary { | |
background-color: #4CAF50; /* Green */ | |
color: white; | |
border: none; | |
} | |
.gr-button.primary:hover { | |
background-color: #43a047; | |
box-shadow: 0 2px 8px rgba(0,0,0,0.2); | |
} | |
.gr-button.secondary { | |
background-color: #e3f2fd; /* Light blue background for secondary button */ | |
color: #424242; | |
border: 1px solid #90caf9; /* Light blue border */ | |
} | |
.gr-button.secondary:hover { | |
background-color: #bbdefb; | |
box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
} | |
.gr-button.download { | |
background-color: #2196f3; /* Blue for download */ | |
color: white; | |
border: none; | |
} | |
.gr-button.download:hover { | |
background-color: #1976d2; | |
box-shadow: 0 2px 8px rgba(0,0,0,0.2); | |
} | |
/* Markdown Output */ | |
.gr-markdown { | |
background-color: #f9f9f9; | |
border: 1px solid #e0e0e0; | |
border-radius: 8px; | |
padding: 20px; | |
line-height: 1.6; | |
color: #424242; | |
white-space: normal !important; /* Ensure text wraps */ | |
word-wrap: break-word !important; /* Ensure long words break */ | |
} | |
.gr-markdown p { | |
margin-bottom: 10px; | |
} | |
.gr-markdown ul { | |
list-style-type: disc; | |
margin-left: 20px; | |
padding-left: 0; | |
} | |
.gr-markdown li { | |
margin-bottom: 5px; | |
} | |
/* Plots */ | |
.gr-plot { | |
border: 1px solid #e0e0e0; | |
border-radius: 8px; | |
padding: 10px; | |
background-color: #ffffff; | |
margin-top: 20px; | |
} | |
/* Specific element adjustments */ | |
#connection-status-textbox { | |
font-weight: 500; | |
color: #3f51b5; | |
} | |
#loading-status-textbox { | |
font-style: italic; | |
color: #757575; | |
} | |
#input-button-column { | |
background-color: #ffffff !important; | |
} | |
.investor-note { | |
color: #2e7d32; /* Green color for investor notes */ | |
font-style: italic; | |
font-weight: 500; | |
margin-top: 8px; | |
margin-bottom: 8px; | |
padding-left: 8px; | |
border-left: 2px solid #66bb6a; /* Small green bar on the left */ | |
} | |
""" | |
with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as app: | |
gr.Markdown("# 📈 Agentic Stock Advisor - AI Stock Advisor") | |
# Connection Status | |
with gr.Column(): | |
connection_btn = gr.Button("🔗 Test MCP Connection", variant="secondary") | |
connection_status = gr.Textbox(label="MCP Connection Status", interactive=False, elem_id="connection-status-textbox") | |
# New vLLM Connection Test | |
vllm_connection_btn = gr.Button("🧠 Test vLLM Connection", variant="secondary") | |
vllm_connection_status = gr.Textbox(label="vLLM Connection Status", interactive=False, elem_id="vllm-connection-status-textbox") | |
with gr.Row(): | |
with gr.Column(scale=2, elem_id="input-button-column"): | |
ticker_input = gr.Textbox( | |
label="Enter Stock Ticker", | |
placeholder="e.g., AAPL", | |
max_lines=1 | |
) | |
generate_btn = gr.Button("Generate Report", variant="primary") | |
loading_status = gr.Textbox(label="Status", interactive=False, visible=False, elem_id="loading-status-textbox") | |
# Results display components | |
output = gr.Markdown(label="Analysis", visible=False) | |
revenue_plot = gr.Plot(label="Revenue Growth", visible=False) | |
fcf_plot = gr.Plot(label="Free Cash Flow per Share", visible=False) | |
shares_plot = gr.Plot(label="Shares Outstanding", visible=False) | |
# Static Chart Insights Section | |
chart_insights_text = """ | |
## Chart Insights | |
### Revenue Growth | |
<p class="investor-note">*What to look for: We are looking for companies that consistently grow their revenue year after year, ideally. Consistent growth indicates market acceptance and business expansion.*</p> | |
### Free Cash Flow per Share | |
<p class="investor-note">*What to look for: Look for companies with consistently high and growing free cash flow. High FCF indicates a company has strong financial health and flexibility for reinvestment, debt reduction, or shareholder returns.*</p> | |
### Shares Outstanding | |
<p class="investor-note">*What to look for: Ideally, look for a declining trend in shares outstanding. This suggests the company is buying back its own shares, which can increase shareholder value by reducing the number of shares in circulation.*</p> | |
""" | |
chart_insights = gr.Markdown(value=chart_insights_text, visible=False, label="Chart Insights") | |
download_button = gr.DownloadButton( | |
label="Download Analysis as TXT", | |
visible=False, | |
variant="secondary" | |
) | |
# Hidden state to store analysis text for download | |
analysis_text_state = gr.State() | |
# Event handlers | |
generate_btn.click( | |
fn=self.generate_report, | |
inputs=[ticker_input], | |
outputs=[ | |
output, | |
revenue_plot, | |
fcf_plot, | |
shares_plot, | |
analysis_text_state, | |
download_button, | |
loading_status, | |
chart_insights | |
] | |
) | |
ticker_input.submit( | |
fn=self.generate_report, | |
inputs=[ticker_input], | |
outputs=[ | |
output, | |
revenue_plot, | |
fcf_plot, | |
shares_plot, | |
analysis_text_state, | |
download_button, | |
loading_status, | |
chart_insights | |
] | |
) | |
download_button.click( | |
fn=self._save_and_return_analysis_file, | |
inputs=[analysis_text_state, ticker_input], | |
outputs=[download_button], | |
show_progress="hidden" | |
) | |
# Event handler for connection button | |
connection_btn.click( | |
fn=self._test_mcp_connection, | |
inputs=[], | |
outputs=[connection_status] | |
) | |
vllm_connection_btn.click( | |
fn=self._test_vllm_connection, | |
inputs=[], | |
outputs=[vllm_connection_status] | |
) | |
return app | |
def _save_and_return_analysis_file(self, analysis_text: str, ticker: str): | |
"""Saves the analysis text to a file and returns the path for download.""" | |
if not analysis_text: | |
return None # No file to download if no analysis text | |
# Ensure reports directory exists | |
reports_dir = "reports" | |
os.makedirs(reports_dir, exist_ok=True) | |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
file_path = os.path.join(reports_dir, f"{ticker}_analysis_{timestamp}.txt") | |
with open(file_path, "w") as f: | |
f.write(analysis_text) | |
return file_path | |
def generate_report(self, ticker): | |
"""Generate stock analysis report.""" | |
if not ticker or not ticker.strip(): | |
return ( | |
gr.update(value="Error: Please enter a valid stock ticker", visible=True), | |
gr.update(value=None, visible=False), | |
gr.update(value=None, visible=False), | |
gr.update(value=None, visible=False), | |
None, | |
gr.update(visible=False), | |
gr.update(value="❌ Error: Please enter a valid stock ticker", visible=True), | |
gr.update(visible=False) | |
) | |
try: | |
# Show loading state | |
yield ( | |
gr.update(value="⏳ Generating report... Please wait.", visible=True), | |
gr.update(value=None, visible=False), | |
gr.update(value=None, visible=False), | |
gr.update(value=None, visible=False), | |
None, | |
gr.update(visible=False), | |
gr.update(value="⏳ Analyzing stock data and generating insights...", visible=True), | |
gr.update(visible=False) | |
) | |
# Call the Modal MCP server for analysis | |
headers = {'Content-Type': 'application/json'} | |
payload = {"ticker": ticker} | |
response = requests.post(f"{self.mcp_server_url}/analyze", headers=headers, json=payload) | |
response.raise_for_status() | |
analysis_data = response.json() | |
# Extract data from the response | |
analysis_results = { | |
"analysis": analysis_data["analysis"], | |
"revenue_chart": pio.from_json(analysis_data["revenue_chart"]) if analysis_data["revenue_chart"] else None, | |
"fcf_chart": pio.from_json(analysis_data["fcf_chart"]) if analysis_data["fcf_chart"] else None, | |
"shares_chart": pio.from_json(analysis_data["shares_chart"]) if analysis_data["shares_chart"] else None | |
} | |
# Format the analysis text with a timestamp | |
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
formatted_analysis = f"## Analysis Report for {ticker.upper()}\n*Generated on {timestamp}*\n\n{analysis_results['analysis']}" | |
yield ( | |
gr.update(value=formatted_analysis, visible=True), | |
gr.update(value=analysis_results["revenue_chart"], visible=True), | |
gr.update(value=analysis_results["fcf_chart"], visible=True), | |
gr.update(value=analysis_results["shares_chart"], visible=True), | |
formatted_analysis, | |
gr.update(label=f"Download {ticker.upper()} Report", visible=True), | |
gr.update(value="✅ Analysis complete!", visible=True), | |
gr.update(visible=True) | |
) | |
except requests.exceptions.RequestException as e: | |
error_msg = f"Error analyzing stock: {str(e)}" | |
if "429" in str(e): | |
error_msg = "Rate limit exceeded. Please try again in a few minutes." | |
elif "500" in str(e): | |
error_msg = "Server error. Please try again later." | |
yield ( | |
gr.update(value=error_msg, visible=True), | |
gr.update(value=None, visible=False), | |
gr.update(value=None, visible=False), | |
gr.update(value=None, visible=False), | |
None, | |
gr.update(visible=False), | |
gr.update(value=f"❌ {error_msg}", visible=True), | |
gr.update(visible=False) | |
) | |
def _test_mcp_connection(self): | |
"""Test connection to Modal MCP server health endpoint.""" | |
try: | |
response = requests.get(f"{self.mcp_server_url}/health", timeout=10) | |
if response.status_code == 200: | |
return f"✅ Connected to Modal MCP Server: {response.json().get('status', 'OK')}" | |
else: | |
return f"❌ Connection failed (HTTP {response.status_code}): {response.text}" | |
except requests.exceptions.RequestException as e: | |
return f"❌ Connection error: {str(e)}" | |
def _test_vllm_connection(self): | |
"""Tests the connection to the Modal vLLM service via the MCP server.""" | |
try: | |
response = requests.get(f"{self.mcp_server_url}/test_llm_connection") | |
response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx) | |
status_data = response.json() | |
return f"Status: {status_data.get('status', 'Unknown')}" | |
except requests.exceptions.RequestException as e: | |
return f"Error calling vLLM test endpoint: {e}" | |
def main(): | |
"""Main entry point for the application.""" | |
app_instance = AgenticStockAdvisorApp() | |
app = app_instance.create_ui() | |
app.launch(server_name="0.0.0.0", server_port=7860, mcp_server=True) | |
if __name__ == "__main__": | |
main() |