Faham commited on
Commit
31adc25
·
1 Parent(s): e501d8f

CREATE: streamlit_app.py for UI

Browse files
.streamlit/config.toml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [server]
2
+ maxUploadSize = 200
3
+ enableXsrfProtection = false
4
+ enableCORS = false
5
+
6
+ [browser]
7
+ gatherUsageStats = false
8
+
9
+ [theme]
10
+ primaryColor = "#1f77b4"
11
+ backgroundColor = "#ffffff"
12
+ secondaryBackgroundColor = "#f0f2f6"
13
+ textColor = "#262730"
README.md CHANGED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Financial Agent
2
+
3
+ A comprehensive financial analysis tool that provides real-time stock data and news analysis through an AI-powered chat interface.
4
+
5
+ ## Features
6
+
7
+ - **Real-time Stock Data**: Fetch historical stock prices and performance metrics
8
+ - **Latest News Analysis**: Get recent news headlines and sentiment analysis
9
+ - **AI-Powered Insights**: Receive comprehensive analysis and investment recommendations
10
+ - **Interactive Chat Interface**: Modern Streamlit-based web interface
11
+ - **Multiple Stock Support**: Analyze AAPL, TSLA, MSFT, GOOG, and more
12
+
13
+ ## Setup
14
+
15
+ 1. **Install dependencies**:
16
+
17
+ ```bash
18
+ uv sync
19
+ ```
20
+
21
+ 2. **Create a `.env` file** with your API keys:
22
+
23
+ ```
24
+ OPENROUTER_API_KEY=your_openrouter_api_key_here
25
+ MODEL=openai/gpt-4o-mini
26
+ ```
27
+
28
+ 3. **Run the Streamlit app**:
29
+ ```bash
30
+ streamlit run streamlit_app.py
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ 1. Open the web interface in your browser
36
+ 2. Select a stock ticker from the dropdown in the sidebar
37
+ 3. Start chatting with the financial agent about the selected stock
38
+ 4. Ask questions like:
39
+ - "How is this stock performing?"
40
+ - "What's the latest news about this company?"
41
+ - "Should I invest in this stock?"
42
+ - "What are the recent trends?"
43
+
44
+ ## Architecture
45
+
46
+ - **Frontend**: Streamlit web interface
47
+ - **Backend**: Python with OpenAI/OpenRouter integration
48
+ - **Data Sources**:
49
+ - Stock data via `yfinance`
50
+ - News data via `gnews`
51
+ - **AI Model**: GPT-4o-mini via OpenRouter
52
+
53
+ ## Files
54
+
55
+ - `streamlit_app.py`: Main Streamlit web application
56
+ - `agent_client.py`: Original terminal-based client
57
+ - `stock_data_server.py`: MCP server for stock data
58
+ - `news_server.py`: MCP server for news data
main.py → agent_client.py RENAMED
@@ -1,4 +1,3 @@
1
- # agent_client.py
2
  import os
3
  import asyncio
4
  import json
 
 
1
  import os
2
  import asyncio
3
  import json
pyproject.toml CHANGED
@@ -11,8 +11,10 @@ dependencies = [
11
  "mcp[cli]>=1.12.2",
12
  "openai>=1.97.1",
13
  "pandas>=2.3.1",
 
14
  "python-dotenv>=1.1.1",
15
  "sentence-transformers>=5.0.0",
 
16
  "transformers[torch]>=4.54.0",
17
  "yfinance>=0.2.65",
18
  ]
 
11
  "mcp[cli]>=1.12.2",
12
  "openai>=1.97.1",
13
  "pandas>=2.3.1",
14
+ "plotly>=5.17.0",
15
  "python-dotenv>=1.1.1",
16
  "sentence-transformers>=5.0.0",
17
+ "streamlit>=1.28.0",
18
  "transformers[torch]>=4.54.0",
19
  "yfinance>=0.2.65",
20
  ]
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ beautifulsoup4>=4.13.4
2
+ fastmcp>=2.10.6
3
+ gnews>=0.4.1
4
+ mcp[cli]>=1.12.2
5
+ openai>=1.97.1
6
+ pandas>=2.3.1
7
+ plotly>=5.17.0
8
+ python-dotenv>=1.1.1
9
+ sentence-transformers>=5.0.0
10
+ streamlit>=1.28.0
11
+ transformers[torch]>=4.54.0
12
+ yfinance>=0.2.65
simple_mcp_test.py DELETED
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Simple MCP test based on official documentation
4
- """
5
-
6
- import asyncio
7
- from mcp.client.session import ClientSession
8
- from mcp.client.stdio import stdio_client
9
- from mcp import StdioServerParameters
10
-
11
-
12
- async def test_simple_mcp():
13
- """Test MCP connection using the official approach"""
14
- print("Testing MCP connection...")
15
-
16
- try:
17
- # Use the official StdioServerParameters approach
18
- server_params = StdioServerParameters(command="python", args=["news_server.py"])
19
-
20
- # Connect using the official method
21
- async with stdio_client(server_params) as (read, write):
22
- async with ClientSession(read, write) as session:
23
- await session.initialize()
24
- print("✅ Session initialized")
25
-
26
- # List available tools
27
- tools_response = await session.list_tools()
28
- print(
29
- f"✅ Available tools: {[tool.name for tool in tools_response.tools]}"
30
- )
31
-
32
- # Call a tool
33
- result = await session.call_tool("get_latest_news", {"ticker": "TSLA"})
34
- print(f"✅ Tool result: {result.content[0].text[:100]}...")
35
-
36
- except Exception as e:
37
- print(f"❌ Error: {e}")
38
- import traceback
39
-
40
- traceback.print_exc()
41
-
42
-
43
- if __name__ == "__main__":
44
- asyncio.run(test_simple_mcp())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
streamlit_app.py ADDED
@@ -0,0 +1,714 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import asyncio
3
+ import json
4
+ import re
5
+ import os
6
+ import pandas as pd
7
+ import plotly.graph_objects as go
8
+ from plotly.subplots import make_subplots
9
+ import yfinance as yf
10
+ from datetime import datetime, timedelta
11
+ from dotenv import load_dotenv
12
+ from openai import OpenAI
13
+ from mcp.client.session import ClientSession
14
+ from mcp.client.stdio import stdio_client
15
+ from mcp import StdioServerParameters, types
16
+
17
+ # Load environment variables
18
+ load_dotenv()
19
+
20
+ # Check if API key exists - support both .env and Streamlit secrets
21
+ api_key = os.getenv("OPENROUTER_API_KEY") or st.secrets.get("OPENROUTER_API_KEY")
22
+ model = os.getenv("MODEL") or st.secrets.get("MODEL")
23
+
24
+ if not api_key:
25
+ st.error(
26
+ "❌ Error: OPENROUTER_API_KEY not found. Please set it in your environment variables or Streamlit secrets."
27
+ )
28
+ st.stop()
29
+
30
+ if not model:
31
+ st.error(
32
+ "❌ Error: MODEL not found. Please set it in your environment variables or Streamlit secrets."
33
+ )
34
+ st.stop()
35
+
36
+ # Configure the client to connect to OpenRouter
37
+ client = OpenAI(
38
+ base_url="https://openrouter.ai/api/v1",
39
+ api_key=api_key,
40
+ )
41
+
42
+ # Global variable to store discovered tools
43
+ discovered_tools = []
44
+
45
+
46
+ def extract_ticker_from_query(query: str) -> str:
47
+ """Extract ticker symbol from user query."""
48
+ query_upper = query.upper()
49
+
50
+ # First try to find ticker in parentheses
51
+ paren_match = re.search(r"\(([A-Z]{1,5})\)", query_upper)
52
+ if paren_match:
53
+ return paren_match.group(1)
54
+
55
+ # Look for our predefined tickers in the query
56
+ predefined_tickers = ["AAPL", "TSLA", "MSFT", "GOOG"]
57
+ for ticker in predefined_tickers:
58
+ if ticker in query_upper:
59
+ return ticker
60
+
61
+ # Try to find any 2-5 letter uppercase sequence that might be a ticker
62
+ ticker_match = re.search(r"\b([A-Z]{2,5})\b", query_upper)
63
+ if ticker_match:
64
+ potential_ticker = ticker_match.group(1)
65
+ # Avoid common words that might be mistaken for tickers
66
+ if potential_ticker not in [
67
+ "THE",
68
+ "AND",
69
+ "FOR",
70
+ "HOW",
71
+ "WHAT",
72
+ "WHEN",
73
+ "WHERE",
74
+ "WHY",
75
+ ]:
76
+ return potential_ticker
77
+
78
+ return None
79
+
80
+
81
+ def validate_ticker(ticker: str) -> bool:
82
+ """Validate if ticker symbol is in correct format."""
83
+ if not ticker:
84
+ return False
85
+ # Basic validation: 1-5 uppercase letters
86
+ return bool(re.match(r"^[A-Z]{1,5}$", ticker))
87
+
88
+
89
+ async def get_news_data(ticker: str) -> str:
90
+ """Get news data by calling the news server via MCP."""
91
+ try:
92
+ # Validate ticker
93
+ if not validate_ticker(ticker):
94
+ return f"Invalid ticker symbol: {ticker}. Please use a valid stock symbol (e.g., AAPL, TSLA)."
95
+
96
+ # Set up MCP server parameters
97
+ import os
98
+ import sys
99
+
100
+ current_dir = os.path.dirname(os.path.abspath(__file__))
101
+ news_server_path = os.path.join(current_dir, "news_server.py")
102
+
103
+ if not os.path.exists(news_server_path):
104
+ return f"Error: news_server.py not found at {news_server_path}"
105
+
106
+ # Use the same Python executable as the current process
107
+ python_executable = sys.executable
108
+ server_params = StdioServerParameters(
109
+ command=python_executable, args=[news_server_path]
110
+ )
111
+
112
+ # Connect to the MCP server
113
+ try:
114
+ async with stdio_client(server_params) as (read, write):
115
+ async with ClientSession(read, write) as session:
116
+ # Initialize the session
117
+ await session.initialize()
118
+
119
+ # Call the get_latest_news tool
120
+ with st.status(
121
+ f"🔍 Fetching news data for {ticker}...", expanded=False
122
+ ) as status:
123
+ try:
124
+ result = await asyncio.wait_for(
125
+ session.call_tool(
126
+ "get_latest_news", {"ticker": ticker}
127
+ ),
128
+ timeout=30.0, # 30 second timeout
129
+ )
130
+ status.update(
131
+ label=f"✅ News data fetched for {ticker}",
132
+ state="complete",
133
+ )
134
+ except asyncio.TimeoutError:
135
+ status.update(
136
+ label="❌ News data fetch timed out", state="error"
137
+ )
138
+ return f"Timeout getting news for {ticker}"
139
+ except Exception as e:
140
+ status.update(
141
+ label=f"❌ Error fetching news: {e}", state="error"
142
+ )
143
+ return f"Error getting news for {ticker}: {e}"
144
+
145
+ # Parse the result properly
146
+ if result.content:
147
+ for content in result.content:
148
+ if isinstance(content, types.TextContent):
149
+ return content.text
150
+
151
+ return f"No news data returned for {ticker}"
152
+ except Exception as e:
153
+ st.error(f"❌ Failed to connect to news server: {e}")
154
+ return f"Failed to connect to news server: {e}"
155
+
156
+ except Exception as e:
157
+ return f"Error getting news for {ticker}: {e}"
158
+
159
+
160
+ async def get_stock_data(ticker: str) -> str:
161
+ """Get stock data by calling the stock server via MCP."""
162
+ try:
163
+ # Validate ticker
164
+ if not validate_ticker(ticker):
165
+ return f"Invalid ticker symbol: {ticker}. Please use a valid stock symbol (e.g., AAPL, TSLA)."
166
+
167
+ # Set up MCP server parameters
168
+ import os
169
+ import sys
170
+
171
+ current_dir = os.path.dirname(os.path.abspath(__file__))
172
+ stock_server_path = os.path.join(current_dir, "stock_data_server.py")
173
+
174
+ if not os.path.exists(stock_server_path):
175
+ return f"Error: stock_data_server.py not found at {stock_server_path}"
176
+
177
+ # Use the same Python executable as the current process
178
+ python_executable = sys.executable
179
+ server_params = StdioServerParameters(
180
+ command=python_executable, args=[stock_server_path]
181
+ )
182
+
183
+ # Connect to the MCP server
184
+ try:
185
+ async with stdio_client(server_params) as (read, write):
186
+ async with ClientSession(read, write) as session:
187
+ # Initialize the session
188
+ await session.initialize()
189
+
190
+ # Call the get_historical_stock_data tool
191
+ with st.status(
192
+ f"📊 Fetching stock data for {ticker}...", expanded=False
193
+ ) as status:
194
+ try:
195
+ result = await asyncio.wait_for(
196
+ session.call_tool(
197
+ "get_historical_stock_data", {"ticker": ticker}
198
+ ),
199
+ timeout=30.0, # 30 second timeout
200
+ )
201
+ status.update(
202
+ label=f"✅ Stock data fetched for {ticker}",
203
+ state="complete",
204
+ )
205
+ except asyncio.TimeoutError:
206
+ status.update(
207
+ label="❌ Stock data fetch timed out", state="error"
208
+ )
209
+ return f"Timeout getting stock data for {ticker}"
210
+ except Exception as e:
211
+ status.update(
212
+ label=f"❌ Error fetching stock data: {e}",
213
+ state="error",
214
+ )
215
+ return f"Error getting stock data for {ticker}: {e}"
216
+
217
+ # Parse the result properly
218
+ if result.content:
219
+ for content in result.content:
220
+ if isinstance(content, types.TextContent):
221
+ return content.text
222
+
223
+ return f"No stock data returned for {ticker}"
224
+ except Exception as e:
225
+ st.error(f"❌ Failed to connect to stock data server: {e}")
226
+ return f"Failed to connect to stock data server: {e}"
227
+
228
+ except Exception as e:
229
+ return f"Error getting stock data for {ticker}: {e}"
230
+
231
+
232
+ def create_stock_chart(ticker: str):
233
+ """Create an interactive stock price chart for the given ticker."""
234
+ try:
235
+ # Get stock data
236
+ stock = yf.Ticker(ticker)
237
+ hist_data = stock.history(period="30d")
238
+
239
+ if hist_data.empty:
240
+ st.warning(f"No data available for {ticker}")
241
+ return
242
+
243
+ # Create simple line chart
244
+ fig = go.Figure()
245
+
246
+ # Add price line chart
247
+ fig.add_trace(
248
+ go.Scatter(
249
+ x=hist_data.index,
250
+ y=hist_data["Close"],
251
+ mode="lines+markers",
252
+ name=f"{ticker} Price",
253
+ line=dict(color="#1f77b4", width=2),
254
+ marker=dict(size=4),
255
+ )
256
+ )
257
+
258
+ # Update layout
259
+ fig.update_layout(
260
+ title=f"{ticker} Stock Price (30 Days)",
261
+ xaxis_title="Date",
262
+ yaxis_title="Price ($)",
263
+ height=500,
264
+ showlegend=False,
265
+ hovermode="x unified",
266
+ )
267
+
268
+ # Update axes
269
+ fig.update_xaxes(
270
+ title_text="Date",
271
+ tickformat="%b %d",
272
+ tickangle=45,
273
+ )
274
+ fig.update_yaxes(title_text="Price ($)")
275
+
276
+ return fig
277
+
278
+ except Exception as e:
279
+ st.error(f"Error creating chart for {ticker}: {e}")
280
+ return None
281
+
282
+
283
+ def initialize_tools():
284
+ """Initialize the available tools."""
285
+ global discovered_tools
286
+
287
+ discovered_tools = [
288
+ {
289
+ "type": "function",
290
+ "function": {
291
+ "name": "get_latest_news",
292
+ "description": "Fetches recent news headlines and descriptions for a specific stock ticker. Use this when user asks about news, updates, or recent events about a company.",
293
+ "parameters": {
294
+ "type": "object",
295
+ "properties": {
296
+ "ticker": {
297
+ "type": "string",
298
+ "description": "The stock ticker symbol (e.g., 'AAPL', 'GOOG', 'TSLA'). Must be a valid stock symbol.",
299
+ }
300
+ },
301
+ "required": ["ticker"],
302
+ },
303
+ },
304
+ },
305
+ {
306
+ "type": "function",
307
+ "function": {
308
+ "name": "get_historical_stock_data",
309
+ "description": "Fetches recent historical stock data (Open, High, Low, Close, Volume) for a given ticker. Use this when user asks about stock performance, price data, or market performance.",
310
+ "parameters": {
311
+ "type": "object",
312
+ "properties": {
313
+ "ticker": {
314
+ "type": "string",
315
+ "description": "The stock ticker symbol (e.g., 'AAPL', 'TSLA', 'MSFT'). Must be a valid stock symbol.",
316
+ }
317
+ },
318
+ "required": ["ticker"],
319
+ },
320
+ },
321
+ },
322
+ ]
323
+
324
+
325
+ async def execute_tool_call(tool_call):
326
+ """Execute a tool call using MCP servers."""
327
+ try:
328
+ tool_name = tool_call.function.name
329
+ arguments = json.loads(tool_call.function.arguments)
330
+ ticker = arguments.get("ticker")
331
+
332
+ with st.status(
333
+ f"🛠️ Executing {tool_name} for {ticker}...", expanded=False
334
+ ) as status:
335
+ if tool_name == "get_latest_news":
336
+ result = await get_news_data(ticker)
337
+ if "Error" in result or "Failed" in result:
338
+ status.update(label=f"❌ {result}", state="error")
339
+ else:
340
+ status.update(
341
+ label=f"✅ {tool_name} completed for {ticker}", state="complete"
342
+ )
343
+ return result
344
+ elif tool_name == "get_historical_stock_data":
345
+ result = await get_stock_data(ticker)
346
+ if "Error" in result or "Failed" in result:
347
+ status.update(label=f"❌ {result}", state="error")
348
+ else:
349
+ status.update(
350
+ label=f"✅ {tool_name} completed for {ticker}", state="complete"
351
+ )
352
+ return result
353
+ else:
354
+ status.update(label=f"❌ Unknown tool: {tool_name}", state="error")
355
+ return f"Unknown tool: {tool_name}"
356
+ except json.JSONDecodeError as e:
357
+ st.error(f"❌ Invalid tool arguments format: {e}")
358
+ return f"Error: Invalid tool arguments format"
359
+ except Exception as e:
360
+ st.error(f"❌ Error executing tool {tool_call.function.name}: {e}")
361
+ return f"Error executing tool {tool_call.function.name}: {e}"
362
+
363
+
364
+ # The master prompt that defines the agent's behavior
365
+ system_prompt = """
366
+ You are a financial assistant that provides comprehensive analysis based on real-time data. You MUST use tools to get data and then curate the information to answer the user's specific question.
367
+
368
+ AVAILABLE TOOLS:
369
+ - get_latest_news: Get recent news for a ticker
370
+ - get_historical_stock_data: Get stock performance data for a ticker
371
+
372
+ CRITICAL INSTRUCTIONS:
373
+ 1. You MUST call BOTH tools (get_latest_news AND get_historical_stock_data) for every query
374
+ 2. After getting both news and stock data, analyze and synthesize the information
375
+ 3. Answer the user's specific question based on the data you gathered
376
+ 4. Provide insights, trends, and recommendations based on the combined data
377
+ 5. Format your response clearly with sections for news, performance, and analysis
378
+
379
+ EXAMPLE WORKFLOW:
380
+ 1. User asks: "Should I invest in AAPL?"
381
+ 2. You call: get_latest_news with {"ticker": "AAPL"}
382
+ 3. You call: get_historical_stock_data with {"ticker": "AAPL"}
383
+ 4. You analyze both datasets and provide investment advice based on news sentiment and stock performance
384
+
385
+ You are FORBIDDEN from responding without calling both tools. Always call both tools first, then provide a curated analysis based on the user's question.
386
+ """
387
+
388
+
389
+ async def run_agent(user_query, selected_ticker):
390
+ """Run the financial agent with the given query and ticker."""
391
+
392
+ # Construct the query to always fetch both data types
393
+ full_query = f"Based on the latest news and stock performance data for {selected_ticker}, {user_query}"
394
+
395
+ messages = [
396
+ {"role": "system", "content": system_prompt},
397
+ {"role": "user", "content": full_query},
398
+ ]
399
+
400
+ try:
401
+ # Get initial response from the model
402
+ with st.spinner("🤖 Analyzing your request..."):
403
+ response = client.chat.completions.create(
404
+ model=model,
405
+ messages=messages,
406
+ tools=discovered_tools,
407
+ tool_choice="required",
408
+ )
409
+
410
+ if not response.choices or len(response.choices) == 0:
411
+ st.error("❌ Error: No response from model")
412
+ return
413
+
414
+ response_message = response.choices[0].message
415
+
416
+ # Truncate tool call IDs if they're too long (max 40 chars)
417
+ if hasattr(response_message, "tool_calls") and response_message.tool_calls:
418
+ for tool_call in response_message.tool_calls:
419
+ if len(tool_call.id) > 40:
420
+ tool_call.id = tool_call.id[:40]
421
+
422
+ messages.append(response_message)
423
+
424
+ # Execute tool calls if any
425
+ if response_message.tool_calls:
426
+ st.info("🛠️ Executing data collection...")
427
+ for tool_call in response_message.tool_calls:
428
+ # Execute the tool call
429
+ tool_result = await execute_tool_call(tool_call)
430
+
431
+ # Add tool result to messages
432
+ messages.append(
433
+ {
434
+ "role": "tool",
435
+ "tool_call_id": tool_call.id[:40], # Truncate to max 40 chars
436
+ "content": tool_result if tool_result else "No data available",
437
+ }
438
+ )
439
+
440
+ # Get final response from the model
441
+ with st.spinner("🤖 Generating analysis..."):
442
+ final_response = client.chat.completions.create(
443
+ model="openai/gpt-4o-mini", # Try a different model
444
+ messages=messages,
445
+ )
446
+
447
+ if final_response.choices and len(final_response.choices) > 0:
448
+ final_content = final_response.choices[0].message.content
449
+ return final_content if final_content else "Empty response"
450
+ else:
451
+ return "No response generated"
452
+ else:
453
+ return (
454
+ response_message.content if response_message.content else "No response"
455
+ )
456
+
457
+ except Exception as e:
458
+ st.error(f"❌ Error: {e}")
459
+ return "Please try again with a different question."
460
+
461
+
462
+ def display_top_news(ticker: str):
463
+ """Display top news headlines for the given ticker with clickable links."""
464
+ try:
465
+ import gnews
466
+ from bs4 import BeautifulSoup
467
+ import re
468
+
469
+ def preprocess_text(text):
470
+ """A simple function to clean text by removing HTML and extra whitespace."""
471
+ if not text:
472
+ return ""
473
+ soup = BeautifulSoup(text, "html.parser")
474
+ clean_text = soup.get_text()
475
+ clean_text = re.sub(r"\s+", " ", clean_text).strip()
476
+ return clean_text
477
+
478
+ # Get news data
479
+ google_news = gnews.GNews(language="en", country="US", period="7d")
480
+ search_query = f'"{ticker}" stock market news'
481
+ articles = google_news.get_news(search_query)
482
+
483
+ if not articles:
484
+ st.info(f"No recent news found for {ticker}")
485
+ return
486
+
487
+ # Display top 5 articles
488
+ for i, article in enumerate(articles[:5], 1):
489
+ title = preprocess_text(article.get("title", ""))
490
+ url = article.get("url", "")
491
+ publisher = article.get("publisher", {}).get("title", "Unknown Source")
492
+
493
+ # Create a clickable link
494
+ if url:
495
+ st.markdown(f"[{title}]({url})")
496
+ st.caption(f"Source: {publisher}")
497
+ else:
498
+ st.markdown(f"{title}")
499
+ st.caption(f"Source: {publisher}")
500
+
501
+ # Add some spacing between articles
502
+ if i < 5:
503
+ st.markdown("---")
504
+
505
+ except Exception as e:
506
+ st.error(f"Error fetching news for {ticker}: {e}")
507
+
508
+
509
+ def test_server_availability():
510
+ """Test if the MCP servers are available and can be executed."""
511
+ import os
512
+ import subprocess
513
+ import time
514
+
515
+ current_dir = os.path.dirname(os.path.abspath(__file__))
516
+
517
+ # Test news server
518
+ news_server_path = os.path.join(current_dir, "news_server.py")
519
+ if not os.path.exists(news_server_path):
520
+ st.error(f"❌ news_server.py not found at {news_server_path}")
521
+ return False
522
+
523
+ # Test stock data server
524
+ stock_server_path = os.path.join(current_dir, "stock_data_server.py")
525
+ if not os.path.exists(stock_server_path):
526
+ st.error(f"❌ stock_data_server.py not found at {stock_server_path}")
527
+ return False
528
+
529
+ # Test if servers can be executed by checking if they can be imported
530
+ import sys
531
+ import importlib.util
532
+
533
+ # Initialize session state for notifications
534
+ if "notifications" not in st.session_state:
535
+ st.session_state.notifications = []
536
+ if "notification_times" not in st.session_state:
537
+ st.session_state.notification_times = {}
538
+ if "servers_importable_shown" not in st.session_state:
539
+ st.session_state.servers_importable_shown = False
540
+
541
+ current_time = time.time()
542
+
543
+ # Clean up old notifications (older than 10 seconds)
544
+ st.session_state.notifications = [
545
+ msg
546
+ for msg, timestamp in zip(
547
+ st.session_state.notifications, st.session_state.notification_times.values()
548
+ )
549
+ if current_time - timestamp < 10
550
+ ]
551
+ st.session_state.notification_times = {
552
+ k: v
553
+ for k, v in st.session_state.notification_times.items()
554
+ if current_time - v < 10
555
+ }
556
+
557
+ try:
558
+ # Test if news_server can be imported
559
+ spec = importlib.util.spec_from_file_location("news_server", news_server_path)
560
+ if spec is None or spec.loader is None:
561
+ st.warning("⚠️ Could not load news_server.py")
562
+ else:
563
+ # Add temporary success notification only once
564
+ if not st.session_state.servers_importable_shown:
565
+ st.success("✅ news_server.py is importable")
566
+ st.session_state.servers_importable_shown = True
567
+ except Exception as e:
568
+ st.warning(f"⚠️ Could not import news_server.py: {e}")
569
+
570
+ try:
571
+ # Test if stock_data_server can be imported
572
+ spec = importlib.util.spec_from_file_location(
573
+ "stock_data_server", stock_server_path
574
+ )
575
+ if spec is None or spec.loader is None:
576
+ st.warning("⚠️ Could not load stock_data_server.py")
577
+ else:
578
+ # Add temporary success notification only once
579
+ if not st.session_state.servers_importable_shown:
580
+ st.success("✅ stock_data_server.py is importable")
581
+ st.session_state.servers_importable_shown = True
582
+ except Exception as e:
583
+ st.warning(f"⚠️ Could not import stock_data_server.py: {e}")
584
+
585
+ return True
586
+
587
+
588
+ def main():
589
+ st.set_page_config(page_title="Financial Agent", page_icon="📈", layout="wide")
590
+
591
+ st.title("📈 Financial Agent")
592
+ st.markdown(
593
+ "Get comprehensive financial analysis and insights for your selected stocks."
594
+ )
595
+
596
+ # Initialize tools
597
+ initialize_tools()
598
+
599
+ # Test server availability only once on startup
600
+ if "servers_tested" not in st.session_state:
601
+ st.session_state.servers_tested = False
602
+
603
+ if not st.session_state.servers_tested:
604
+ test_server_availability()
605
+ st.session_state.servers_tested = True
606
+
607
+ # Available tickers
608
+ available_tickers = {
609
+ "AAPL": "Apple Inc.",
610
+ "TSLA": "Tesla Inc.",
611
+ "MSFT": "Microsoft Corporation",
612
+ "GOOG": "Alphabet Inc. (Google)",
613
+ }
614
+
615
+ # Sidebar for ticker selection
616
+ st.sidebar.header("📊 Stock Selection")
617
+ selected_ticker = st.sidebar.selectbox(
618
+ "Choose a stock ticker:",
619
+ options=list(available_tickers.keys()),
620
+ format_func=lambda x: f"{x} - {available_tickers[x]}",
621
+ index=None,
622
+ placeholder="Select a ticker...",
623
+ )
624
+
625
+ # Main content area
626
+ if not selected_ticker:
627
+ st.info(
628
+ "👈 Please select a stock ticker from the sidebar to view the chart and start chatting."
629
+ )
630
+ st.markdown(
631
+ """
632
+ **How to use:**
633
+ 1. Select a stock ticker from the sidebar
634
+ 2. View the interactive stock price chart
635
+ 3. Ask questions about the stock's performance, news, or investment advice
636
+ 4. The agent will fetch real-time data and provide comprehensive analysis
637
+
638
+ **Example questions:**
639
+ - "How is this stock performing?"
640
+ - "What's the latest news about this company?"
641
+ - "Should I invest in this stock?"
642
+ - "What are the recent trends?"
643
+ """
644
+ )
645
+ else:
646
+ st.success(
647
+ f"✅ Selected: {selected_ticker} - {available_tickers[selected_ticker]}"
648
+ )
649
+
650
+ # Stock Chart and News Section
651
+ st.header("📈 Stock Analysis")
652
+
653
+ # Create two columns for chart and news
654
+ col1, col2 = st.columns([2, 1])
655
+
656
+ with col1:
657
+ st.subheader("📈 Stock Price Chart")
658
+ # Create and display the stock chart
659
+ with st.spinner(f"Loading chart for {selected_ticker}..."):
660
+ chart_fig = create_stock_chart(selected_ticker)
661
+ if chart_fig:
662
+ st.plotly_chart(chart_fig, use_container_width=True)
663
+ else:
664
+ st.warning(f"Could not load chart for {selected_ticker}")
665
+
666
+ with col2:
667
+ st.subheader("📰 Top News")
668
+ # Display top news for the selected ticker
669
+ display_top_news(selected_ticker)
670
+
671
+ # Chat Section in a container
672
+ st.header("💬 Chat with Financial Agent")
673
+
674
+ # Create a container for the chat interface
675
+ with st.container():
676
+ # Initialize chat history
677
+ if "messages" not in st.session_state:
678
+ st.session_state.messages = []
679
+
680
+ # Display chat history
681
+ for message in st.session_state.messages:
682
+ with st.chat_message(message["role"]):
683
+ st.markdown(message["content"])
684
+
685
+ # Chat input
686
+ if prompt := st.chat_input(f"Ask about {selected_ticker}..."):
687
+ # Add user message to chat history
688
+ st.session_state.messages.append({"role": "user", "content": prompt})
689
+
690
+ # Display user message
691
+ with st.chat_message("user"):
692
+ st.markdown(prompt)
693
+
694
+ # Display assistant response
695
+ with st.chat_message("assistant"):
696
+ with st.spinner("🤖 Analyzing..."):
697
+ response = asyncio.run(run_agent(prompt, selected_ticker))
698
+ st.markdown(response)
699
+ st.session_state.messages.append(
700
+ {"role": "assistant", "content": response}
701
+ )
702
+
703
+ # Clear chat button
704
+ col1, col2 = st.columns([1, 4])
705
+ with col1:
706
+ if st.button("🗑️ Clear Chat History"):
707
+ st.session_state.messages = []
708
+ st.rerun()
709
+ with col2:
710
+ st.markdown("*Chat history will be maintained during your session*")
711
+
712
+
713
+ if __name__ == "__main__":
714
+ main()
uv.lock CHANGED
@@ -26,6 +26,22 @@ wheels = [
26
  { url = "https://files.pythonhosted.org/packages/9f/1c/a17fb513aeb684fb83bef5f395910f53103ab30308bbdd77fd66d6698c46/accelerate-1.9.0-py3-none-any.whl", hash = "sha256:c24739a97ade1d54af4549a65f8b6b046adc87e2b3e4d6c66516e32c53d5a8f1", size = 367073, upload_time = "2025-07-16T16:24:52.957Z" },
27
  ]
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  [[package]]
30
  name = "annotated-types"
31
  version = "0.7.0"
@@ -84,6 +100,24 @@ wheels = [
84
  { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload_time = "2025-04-15T17:05:12.221Z" },
85
  ]
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  [[package]]
88
  name = "certifi"
89
  version = "2025.7.14"
@@ -429,8 +463,10 @@ dependencies = [
429
  { name = "mcp", extra = ["cli"] },
430
  { name = "openai" },
431
  { name = "pandas" },
 
432
  { name = "python-dotenv" },
433
  { name = "sentence-transformers" },
 
434
  { name = "transformers", extra = ["torch"] },
435
  { name = "yfinance" },
436
  ]
@@ -443,8 +479,10 @@ requires-dist = [
443
  { name = "mcp", extras = ["cli"], specifier = ">=1.12.2" },
444
  { name = "openai", specifier = ">=1.97.1" },
445
  { name = "pandas", specifier = ">=2.3.1" },
 
446
  { name = "python-dotenv", specifier = ">=1.1.1" },
447
  { name = "sentence-transformers", specifier = ">=5.0.0" },
 
448
  { name = "transformers", extras = ["torch"], specifier = ">=4.54.0" },
449
  { name = "yfinance", specifier = ">=0.2.65" },
450
  ]
@@ -477,6 +515,30 @@ wheels = [
477
  { url = "https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21", size = 199597, upload_time = "2025-07-15T16:05:19.529Z" },
478
  ]
479
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  [[package]]
481
  name = "gnews"
482
  version = "0.4.1"
@@ -823,6 +885,15 @@ version = "0.0.12"
823
  source = { registry = "https://pypi.org/simple" }
824
  sdist = { url = "https://files.pythonhosted.org/packages/17/0d/74f0293dfd7dcc3837746d0138cbedd60b31701ecc75caec7d3f281feba0/multitasking-0.0.12.tar.gz", hash = "sha256:2fba2fa8ed8c4b85e227c5dd7dc41c7d658de3b6f247927316175a57349b84d1", size = 19984, upload_time = "2025-07-20T21:27:51.636Z" }
825
 
 
 
 
 
 
 
 
 
 
826
  [[package]]
827
  name = "networkx"
828
  version = "3.4.2"
@@ -1337,6 +1408,19 @@ wheels = [
1337
  { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload_time = "2025-05-07T22:47:40.376Z" },
1338
  ]
1339
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1340
  [[package]]
1341
  name = "protobuf"
1342
  version = "6.31.1"
@@ -1366,6 +1450,49 @@ wheels = [
1366
  { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload_time = "2025-02-13T21:54:37.486Z" },
1367
  ]
1368
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1369
  [[package]]
1370
  name = "pycparser"
1371
  version = "2.22"
@@ -1496,6 +1623,20 @@ wheels = [
1496
  { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload_time = "2025-06-24T13:26:45.485Z" },
1497
  ]
1498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1499
  [[package]]
1500
  name = "pygments"
1501
  version = "2.19.2"
@@ -2092,6 +2233,15 @@ wheels = [
2092
  { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload_time = "2024-12-04T17:35:26.475Z" },
2093
  ]
2094
 
 
 
 
 
 
 
 
 
 
2095
  [[package]]
2096
  name = "sniffio"
2097
  version = "1.3.1"
@@ -2135,6 +2285,36 @@ wheels = [
2135
  { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload_time = "2025-07-20T17:31:56.738Z" },
2136
  ]
2137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2138
  [[package]]
2139
  name = "sympy"
2140
  version = "1.14.0"
@@ -2147,6 +2327,15 @@ wheels = [
2147
  { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload_time = "2025-04-27T18:04:59.103Z" },
2148
  ]
2149
 
 
 
 
 
 
 
 
 
 
2150
  [[package]]
2151
  name = "threadpoolctl"
2152
  version = "3.6.0"
@@ -2181,6 +2370,15 @@ wheels = [
2181
  { url = "https://files.pythonhosted.org/packages/13/c3/cc2755ee10be859c4338c962a35b9a663788c0c0b50c0bdd8078fb6870cf/tokenizers-0.21.2-cp39-abi3-win_amd64.whl", hash = "sha256:58747bb898acdb1007f37a7bbe614346e98dc28708ffb66a3fd50ce169ac6c98", size = 2509918, upload_time = "2025-06-24T10:24:53.71Z" },
2182
  ]
2183
 
 
 
 
 
 
 
 
 
 
2184
  [[package]]
2185
  name = "torch"
2186
  version = "2.7.1"
@@ -2233,6 +2431,25 @@ wheels = [
2233
  { url = "https://files.pythonhosted.org/packages/b1/29/beb45cdf5c4fc3ebe282bf5eafc8dfd925ead7299b3c97491900fe5ed844/torch-2.7.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:988b0cbc4333618a1056d2ebad9eb10089637b659eb645434d0809d8d937b946", size = 68645708, upload_time = "2025-06-04T17:34:39.852Z" },
2234
  ]
2235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2236
  [[package]]
2237
  name = "tqdm"
2238
  version = "4.67.1"
@@ -2356,6 +2573,24 @@ wheels = [
2356
  { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload_time = "2025-06-28T16:15:44.816Z" },
2357
  ]
2358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2359
  [[package]]
2360
  name = "websockets"
2361
  version = "15.0.1"
 
26
  { url = "https://files.pythonhosted.org/packages/9f/1c/a17fb513aeb684fb83bef5f395910f53103ab30308bbdd77fd66d6698c46/accelerate-1.9.0-py3-none-any.whl", hash = "sha256:c24739a97ade1d54af4549a65f8b6b046adc87e2b3e4d6c66516e32c53d5a8f1", size = 367073, upload_time = "2025-07-16T16:24:52.957Z" },
27
  ]
28
 
29
+ [[package]]
30
+ name = "altair"
31
+ version = "5.5.0"
32
+ source = { registry = "https://pypi.org/simple" }
33
+ dependencies = [
34
+ { name = "jinja2" },
35
+ { name = "jsonschema" },
36
+ { name = "narwhals" },
37
+ { name = "packaging" },
38
+ { name = "typing-extensions", marker = "python_full_version < '3.14'" },
39
+ ]
40
+ sdist = { url = "https://files.pythonhosted.org/packages/16/b1/f2969c7bdb8ad8bbdda031687defdce2c19afba2aa2c8e1d2a17f78376d8/altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d", size = 705305, upload_time = "2024-11-23T23:39:58.542Z" }
41
+ wheels = [
42
+ { url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200, upload_time = "2024-11-23T23:39:56.4Z" },
43
+ ]
44
+
45
  [[package]]
46
  name = "annotated-types"
47
  version = "0.7.0"
 
100
  { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload_time = "2025-04-15T17:05:12.221Z" },
101
  ]
102
 
103
+ [[package]]
104
+ name = "blinker"
105
+ version = "1.9.0"
106
+ source = { registry = "https://pypi.org/simple" }
107
+ sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload_time = "2024-11-08T17:25:47.436Z" }
108
+ wheels = [
109
+ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload_time = "2024-11-08T17:25:46.184Z" },
110
+ ]
111
+
112
+ [[package]]
113
+ name = "cachetools"
114
+ version = "6.1.0"
115
+ source = { registry = "https://pypi.org/simple" }
116
+ sdist = { url = "https://files.pythonhosted.org/packages/8a/89/817ad5d0411f136c484d535952aef74af9b25e0d99e90cdffbe121e6d628/cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587", size = 30714, upload_time = "2025-06-16T18:51:03.07Z" }
117
+ wheels = [
118
+ { url = "https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e", size = 11189, upload_time = "2025-06-16T18:51:01.514Z" },
119
+ ]
120
+
121
  [[package]]
122
  name = "certifi"
123
  version = "2025.7.14"
 
463
  { name = "mcp", extra = ["cli"] },
464
  { name = "openai" },
465
  { name = "pandas" },
466
+ { name = "plotly" },
467
  { name = "python-dotenv" },
468
  { name = "sentence-transformers" },
469
+ { name = "streamlit" },
470
  { name = "transformers", extra = ["torch"] },
471
  { name = "yfinance" },
472
  ]
 
479
  { name = "mcp", extras = ["cli"], specifier = ">=1.12.2" },
480
  { name = "openai", specifier = ">=1.97.1" },
481
  { name = "pandas", specifier = ">=2.3.1" },
482
+ { name = "plotly", specifier = ">=5.17.0" },
483
  { name = "python-dotenv", specifier = ">=1.1.1" },
484
  { name = "sentence-transformers", specifier = ">=5.0.0" },
485
+ { name = "streamlit", specifier = ">=1.28.0" },
486
  { name = "transformers", extras = ["torch"], specifier = ">=4.54.0" },
487
  { name = "yfinance", specifier = ">=0.2.65" },
488
  ]
 
515
  { url = "https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21", size = 199597, upload_time = "2025-07-15T16:05:19.529Z" },
516
  ]
517
 
518
+ [[package]]
519
+ name = "gitdb"
520
+ version = "4.0.12"
521
+ source = { registry = "https://pypi.org/simple" }
522
+ dependencies = [
523
+ { name = "smmap" },
524
+ ]
525
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload_time = "2025-01-02T07:20:46.413Z" }
526
+ wheels = [
527
+ { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload_time = "2025-01-02T07:20:43.624Z" },
528
+ ]
529
+
530
+ [[package]]
531
+ name = "gitpython"
532
+ version = "3.1.45"
533
+ source = { registry = "https://pypi.org/simple" }
534
+ dependencies = [
535
+ { name = "gitdb" },
536
+ ]
537
+ sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload_time = "2025-07-24T03:45:54.871Z" }
538
+ wheels = [
539
+ { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload_time = "2025-07-24T03:45:52.517Z" },
540
+ ]
541
+
542
  [[package]]
543
  name = "gnews"
544
  version = "0.4.1"
 
885
  source = { registry = "https://pypi.org/simple" }
886
  sdist = { url = "https://files.pythonhosted.org/packages/17/0d/74f0293dfd7dcc3837746d0138cbedd60b31701ecc75caec7d3f281feba0/multitasking-0.0.12.tar.gz", hash = "sha256:2fba2fa8ed8c4b85e227c5dd7dc41c7d658de3b6f247927316175a57349b84d1", size = 19984, upload_time = "2025-07-20T21:27:51.636Z" }
887
 
888
+ [[package]]
889
+ name = "narwhals"
890
+ version = "1.48.1"
891
+ source = { registry = "https://pypi.org/simple" }
892
+ sdist = { url = "https://files.pythonhosted.org/packages/9b/da/fe15ccd311ebb8fbbdacc447ba5888306c0b4a6253f628d60df351c36c7d/narwhals-1.48.1.tar.gz", hash = "sha256:b375cfdfc20b84b5ac0926f34c5c1373eb23ebea48d47bf75e282161cda63e34", size = 515882, upload_time = "2025-07-24T19:02:19.14Z" }
893
+ wheels = [
894
+ { url = "https://files.pythonhosted.org/packages/cd/cf/411b2083991c6906634910ea0c5e5ea0a01f7f14da4194b39d7ad054c187/narwhals-1.48.1-py3-none-any.whl", hash = "sha256:76e3b069cf20a2746d8e227686b959530e98e8018c594a04e5f4f6f77e0872d9", size = 377332, upload_time = "2025-07-24T19:02:17.548Z" },
895
+ ]
896
+
897
  [[package]]
898
  name = "networkx"
899
  version = "3.4.2"
 
1408
  { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload_time = "2025-05-07T22:47:40.376Z" },
1409
  ]
1410
 
1411
+ [[package]]
1412
+ name = "plotly"
1413
+ version = "6.2.0"
1414
+ source = { registry = "https://pypi.org/simple" }
1415
+ dependencies = [
1416
+ { name = "narwhals" },
1417
+ { name = "packaging" },
1418
+ ]
1419
+ sdist = { url = "https://files.pythonhosted.org/packages/6e/5c/0efc297df362b88b74957a230af61cd6929f531f72f48063e8408702ffba/plotly-6.2.0.tar.gz", hash = "sha256:9dfa23c328000f16c928beb68927444c1ab9eae837d1fe648dbcda5360c7953d", size = 6801941, upload_time = "2025-06-26T16:20:45.765Z" }
1420
+ wheels = [
1421
+ { url = "https://files.pythonhosted.org/packages/ed/20/f2b7ac96a91cc5f70d81320adad24cc41bf52013508d649b1481db225780/plotly-6.2.0-py3-none-any.whl", hash = "sha256:32c444d4c940887219cb80738317040363deefdfee4f354498cc0b6dab8978bd", size = 9635469, upload_time = "2025-06-26T16:20:40.76Z" },
1422
+ ]
1423
+
1424
  [[package]]
1425
  name = "protobuf"
1426
  version = "6.31.1"
 
1450
  { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload_time = "2025-02-13T21:54:37.486Z" },
1451
  ]
1452
 
1453
+ [[package]]
1454
+ name = "pyarrow"
1455
+ version = "21.0.0"
1456
+ source = { registry = "https://pypi.org/simple" }
1457
+ sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload_time = "2025-07-18T00:57:31.761Z" }
1458
+ wheels = [
1459
+ { url = "https://files.pythonhosted.org/packages/17/d9/110de31880016e2afc52d8580b397dbe47615defbf09ca8cf55f56c62165/pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26", size = 31196837, upload_time = "2025-07-18T00:54:34.755Z" },
1460
+ { url = "https://files.pythonhosted.org/packages/df/5f/c1c1997613abf24fceb087e79432d24c19bc6f7259cab57c2c8e5e545fab/pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79", size = 32659470, upload_time = "2025-07-18T00:54:38.329Z" },
1461
+ { url = "https://files.pythonhosted.org/packages/3e/ed/b1589a777816ee33ba123ba1e4f8f02243a844fed0deec97bde9fb21a5cf/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb", size = 41055619, upload_time = "2025-07-18T00:54:42.172Z" },
1462
+ { url = "https://files.pythonhosted.org/packages/44/28/b6672962639e85dc0ac36f71ab3a8f5f38e01b51343d7aa372a6b56fa3f3/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51", size = 42733488, upload_time = "2025-07-18T00:54:47.132Z" },
1463
+ { url = "https://files.pythonhosted.org/packages/f8/cc/de02c3614874b9089c94eac093f90ca5dfa6d5afe45de3ba847fd950fdf1/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a", size = 43329159, upload_time = "2025-07-18T00:54:51.686Z" },
1464
+ { url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594", size = 45050567, upload_time = "2025-07-18T00:54:56.679Z" },
1465
+ { url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634", size = 26217959, upload_time = "2025-07-18T00:55:00.482Z" },
1466
+ { url = "https://files.pythonhosted.org/packages/94/dc/80564a3071a57c20b7c32575e4a0120e8a330ef487c319b122942d665960/pyarrow-21.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b", size = 31243234, upload_time = "2025-07-18T00:55:03.812Z" },
1467
+ { url = "https://files.pythonhosted.org/packages/ea/cc/3b51cb2db26fe535d14f74cab4c79b191ed9a8cd4cbba45e2379b5ca2746/pyarrow-21.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10", size = 32714370, upload_time = "2025-07-18T00:55:07.495Z" },
1468
+ { url = "https://files.pythonhosted.org/packages/24/11/a4431f36d5ad7d83b87146f515c063e4d07ef0b7240876ddb885e6b44f2e/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e", size = 41135424, upload_time = "2025-07-18T00:55:11.461Z" },
1469
+ { url = "https://files.pythonhosted.org/packages/74/dc/035d54638fc5d2971cbf1e987ccd45f1091c83bcf747281cf6cc25e72c88/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569", size = 42823810, upload_time = "2025-07-18T00:55:16.301Z" },
1470
+ { url = "https://files.pythonhosted.org/packages/2e/3b/89fced102448a9e3e0d4dded1f37fa3ce4700f02cdb8665457fcc8015f5b/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e", size = 43391538, upload_time = "2025-07-18T00:55:23.82Z" },
1471
+ { url = "https://files.pythonhosted.org/packages/fb/bb/ea7f1bd08978d39debd3b23611c293f64a642557e8141c80635d501e6d53/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c", size = 45120056, upload_time = "2025-07-18T00:55:28.231Z" },
1472
+ { url = "https://files.pythonhosted.org/packages/6e/0b/77ea0600009842b30ceebc3337639a7380cd946061b620ac1a2f3cb541e2/pyarrow-21.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6", size = 26220568, upload_time = "2025-07-18T00:55:32.122Z" },
1473
+ { url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305, upload_time = "2025-07-18T00:55:35.373Z" },
1474
+ { url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264, upload_time = "2025-07-18T00:55:39.303Z" },
1475
+ { url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099, upload_time = "2025-07-18T00:55:42.889Z" },
1476
+ { url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529, upload_time = "2025-07-18T00:55:47.069Z" },
1477
+ { url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883, upload_time = "2025-07-18T00:55:53.069Z" },
1478
+ { url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802, upload_time = "2025-07-18T00:55:57.714Z" },
1479
+ { url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175, upload_time = "2025-07-18T00:56:01.364Z" },
1480
+ { url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306, upload_time = "2025-07-18T00:56:04.42Z" },
1481
+ { url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622, upload_time = "2025-07-18T00:56:07.505Z" },
1482
+ { url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094, upload_time = "2025-07-18T00:56:10.994Z" },
1483
+ { url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576, upload_time = "2025-07-18T00:56:15.569Z" },
1484
+ { url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342, upload_time = "2025-07-18T00:56:19.531Z" },
1485
+ { url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218, upload_time = "2025-07-18T00:56:23.347Z" },
1486
+ { url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551, upload_time = "2025-07-18T00:56:26.758Z" },
1487
+ { url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064, upload_time = "2025-07-18T00:56:30.214Z" },
1488
+ { url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837, upload_time = "2025-07-18T00:56:33.935Z" },
1489
+ { url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158, upload_time = "2025-07-18T00:56:37.528Z" },
1490
+ { url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885, upload_time = "2025-07-18T00:56:41.483Z" },
1491
+ { url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625, upload_time = "2025-07-18T00:56:48.002Z" },
1492
+ { url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890, upload_time = "2025-07-18T00:56:52.568Z" },
1493
+ { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006, upload_time = "2025-07-18T00:56:56.379Z" },
1494
+ ]
1495
+
1496
  [[package]]
1497
  name = "pycparser"
1498
  version = "2.22"
 
1623
  { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload_time = "2025-06-24T13:26:45.485Z" },
1624
  ]
1625
 
1626
+ [[package]]
1627
+ name = "pydeck"
1628
+ version = "0.9.1"
1629
+ source = { registry = "https://pypi.org/simple" }
1630
+ dependencies = [
1631
+ { name = "jinja2" },
1632
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
1633
+ { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
1634
+ ]
1635
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240, upload_time = "2024-05-10T15:36:21.153Z" }
1636
+ wheels = [
1637
+ { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403, upload_time = "2024-05-10T15:36:17.36Z" },
1638
+ ]
1639
+
1640
  [[package]]
1641
  name = "pygments"
1642
  version = "2.19.2"
 
2233
  { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload_time = "2024-12-04T17:35:26.475Z" },
2234
  ]
2235
 
2236
+ [[package]]
2237
+ name = "smmap"
2238
+ version = "5.0.2"
2239
+ source = { registry = "https://pypi.org/simple" }
2240
+ sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload_time = "2025-01-02T07:14:40.909Z" }
2241
+ wheels = [
2242
+ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload_time = "2025-01-02T07:14:38.724Z" },
2243
+ ]
2244
+
2245
  [[package]]
2246
  name = "sniffio"
2247
  version = "1.3.1"
 
2285
  { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload_time = "2025-07-20T17:31:56.738Z" },
2286
  ]
2287
 
2288
+ [[package]]
2289
+ name = "streamlit"
2290
+ version = "1.47.1"
2291
+ source = { registry = "https://pypi.org/simple" }
2292
+ dependencies = [
2293
+ { name = "altair" },
2294
+ { name = "blinker" },
2295
+ { name = "cachetools" },
2296
+ { name = "click" },
2297
+ { name = "gitpython" },
2298
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
2299
+ { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
2300
+ { name = "packaging" },
2301
+ { name = "pandas" },
2302
+ { name = "pillow" },
2303
+ { name = "protobuf" },
2304
+ { name = "pyarrow" },
2305
+ { name = "pydeck" },
2306
+ { name = "requests" },
2307
+ { name = "tenacity" },
2308
+ { name = "toml" },
2309
+ { name = "tornado" },
2310
+ { name = "typing-extensions" },
2311
+ { name = "watchdog", marker = "sys_platform != 'darwin'" },
2312
+ ]
2313
+ sdist = { url = "https://files.pythonhosted.org/packages/19/da/cef67ed4614f04932a00068fe6291455deb884a04fd94f7ad78492b0e91a/streamlit-1.47.1.tar.gz", hash = "sha256:daed79763d1cafeb03cdd800b91aa9c7adc3688c6b2cbf4ecc2ca899aab82a2a", size = 9544057, upload_time = "2025-07-25T15:37:08.482Z" }
2314
+ wheels = [
2315
+ { url = "https://files.pythonhosted.org/packages/c0/4d/701f5fcf9c0d388dad9d94ba272d333c7efa6231ddee1babc59d26dc14d2/streamlit-1.47.1-py3-none-any.whl", hash = "sha256:c7881549e3ba1daecfb5541f32ee6ff70e549f1c3400c92d045897cb7a29772a", size = 9944872, upload_time = "2025-07-25T15:37:05.758Z" },
2316
+ ]
2317
+
2318
  [[package]]
2319
  name = "sympy"
2320
  version = "1.14.0"
 
2327
  { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload_time = "2025-04-27T18:04:59.103Z" },
2328
  ]
2329
 
2330
+ [[package]]
2331
+ name = "tenacity"
2332
+ version = "9.1.2"
2333
+ source = { registry = "https://pypi.org/simple" }
2334
+ sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload_time = "2025-04-02T08:25:09.966Z" }
2335
+ wheels = [
2336
+ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload_time = "2025-04-02T08:25:07.678Z" },
2337
+ ]
2338
+
2339
  [[package]]
2340
  name = "threadpoolctl"
2341
  version = "3.6.0"
 
2370
  { url = "https://files.pythonhosted.org/packages/13/c3/cc2755ee10be859c4338c962a35b9a663788c0c0b50c0bdd8078fb6870cf/tokenizers-0.21.2-cp39-abi3-win_amd64.whl", hash = "sha256:58747bb898acdb1007f37a7bbe614346e98dc28708ffb66a3fd50ce169ac6c98", size = 2509918, upload_time = "2025-06-24T10:24:53.71Z" },
2371
  ]
2372
 
2373
+ [[package]]
2374
+ name = "toml"
2375
+ version = "0.10.2"
2376
+ source = { registry = "https://pypi.org/simple" }
2377
+ sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload_time = "2020-11-01T01:40:22.204Z" }
2378
+ wheels = [
2379
+ { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload_time = "2020-11-01T01:40:20.672Z" },
2380
+ ]
2381
+
2382
  [[package]]
2383
  name = "torch"
2384
  version = "2.7.1"
 
2431
  { url = "https://files.pythonhosted.org/packages/b1/29/beb45cdf5c4fc3ebe282bf5eafc8dfd925ead7299b3c97491900fe5ed844/torch-2.7.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:988b0cbc4333618a1056d2ebad9eb10089637b659eb645434d0809d8d937b946", size = 68645708, upload_time = "2025-06-04T17:34:39.852Z" },
2432
  ]
2433
 
2434
+ [[package]]
2435
+ name = "tornado"
2436
+ version = "6.5.1"
2437
+ source = { registry = "https://pypi.org/simple" }
2438
+ sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934, upload_time = "2025-05-22T18:15:38.788Z" }
2439
+ wheels = [
2440
+ { url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948, upload_time = "2025-05-22T18:15:20.862Z" },
2441
+ { url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112, upload_time = "2025-05-22T18:15:22.591Z" },
2442
+ { url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672, upload_time = "2025-05-22T18:15:24.027Z" },
2443
+ { url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019, upload_time = "2025-05-22T18:15:25.735Z" },
2444
+ { url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252, upload_time = "2025-05-22T18:15:27.499Z" },
2445
+ { url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930, upload_time = "2025-05-22T18:15:29.299Z" },
2446
+ { url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351, upload_time = "2025-05-22T18:15:31.038Z" },
2447
+ { url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328, upload_time = "2025-05-22T18:15:32.426Z" },
2448
+ { url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396, upload_time = "2025-05-22T18:15:34.205Z" },
2449
+ { url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840, upload_time = "2025-05-22T18:15:36.1Z" },
2450
+ { url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload_time = "2025-05-22T18:15:37.433Z" },
2451
+ ]
2452
+
2453
  [[package]]
2454
  name = "tqdm"
2455
  version = "4.67.1"
 
2573
  { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload_time = "2025-06-28T16:15:44.816Z" },
2574
  ]
2575
 
2576
+ [[package]]
2577
+ name = "watchdog"
2578
+ version = "6.0.0"
2579
+ source = { registry = "https://pypi.org/simple" }
2580
+ sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload_time = "2024-11-01T14:07:13.037Z" }
2581
+ wheels = [
2582
+ { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload_time = "2024-11-01T14:06:59.472Z" },
2583
+ { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload_time = "2024-11-01T14:07:01.431Z" },
2584
+ { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload_time = "2024-11-01T14:07:02.568Z" },
2585
+ { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload_time = "2024-11-01T14:07:03.893Z" },
2586
+ { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload_time = "2024-11-01T14:07:05.189Z" },
2587
+ { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload_time = "2024-11-01T14:07:06.376Z" },
2588
+ { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload_time = "2024-11-01T14:07:07.547Z" },
2589
+ { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload_time = "2024-11-01T14:07:09.525Z" },
2590
+ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload_time = "2024-11-01T14:07:10.686Z" },
2591
+ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload_time = "2024-11-01T14:07:11.845Z" },
2592
+ ]
2593
+
2594
  [[package]]
2595
  name = "websockets"
2596
  version = "15.0.1"