CosmickVisions commited on
Commit
f412351
·
verified ·
1 Parent(s): 22dcc8b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +403 -313
app.py CHANGED
@@ -1,69 +1,171 @@
1
- import gradio as gr
2
- import groq
3
  import os
4
  import tempfile
5
  import uuid
6
- import yfinance as yf
7
- import pandas as pd
8
- import plotly.graph_objects as go
9
- from dotenv import load_dotenv
10
- from langchain.text_splitter import RecursiveCharacterTextSplitter
11
- from langchain.vectorstores import FAISS
12
- from langchain.embeddings import HuggingFaceEmbeddings
13
- import fitz # PyMuPDF
14
  import base64
15
- from PIL import Image
16
  import io
17
- import requests
18
  import json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  # Load environment variables
21
  load_dotenv()
22
  client = groq.Client(api_key=os.getenv("GROQ_LEGAL_API_KEY"))
23
  embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
 
 
24
 
25
  # Directory to store FAISS indexes
26
  FAISS_INDEX_DIR = "faiss_indexes_finance"
27
  if not os.path.exists(FAISS_INDEX_DIR):
28
  os.makedirs(FAISS_INDEX_DIR)
29
 
30
- # Dictionary to store user-specific vectorstores
31
  user_vectorstores = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- # Custom CSS for Finance theme
 
 
 
 
 
 
 
 
 
 
 
 
34
  custom_css = """
35
  :root {
36
- --primary-color: #FFD700; /* Gold */
37
- --secondary-color: #008000; /* Dark Green */
38
- --light-background: #F0FFF0; /* Honeydew */
39
- --dark-text: #333333;
40
- --white: #FFFFFF;
 
41
  --border-color: #E5E7EB;
42
  }
43
- body { background-color: var(--light-background); font-family: 'Inter', sans-serif; }
 
 
 
 
 
 
 
 
 
 
 
44
  .container { max-width: 1200px !important; margin: 0 auto !important; padding: 10px; }
45
- .header { background-color: var(--white); border-bottom: 2px solid var(--border-color); padding: 15px 0; margin-bottom: 20px; border-radius: 12px 12px 0 0; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
46
- .header-title { color: var(--secondary-color); font-size: 1.8rem; font-weight: 700; text-align: center; }
47
- .header-subtitle { color: var(--dark-text); font-size: 1rem; text-align: center; margin-top: 5px; }
48
- .chat-container { border-radius: 12px !important; box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important; background-color: var(--white) !important; border: 1px solid var(--border-color) !important; min-height: 500px; }
49
- .message-user { background-color: var(--primary-color) !important; color: var(--dark-text) !important; border-radius: 18px 18px 4px 18px !important; padding: 12px 16px !important; margin-left: auto !important; max-width: 80% !important; }
50
- .message-bot { background-color: #F0F0F0 !important; color: var(--dark-text) !important; border-radius: 18px 18px 18px 4px !important; padding: 12px 16px !important; margin-right: auto !important; max-width: 80% !important; }
51
- .input-area { background-color: var(--white) !important; border-top: 1px solid var(--border-color) !important; padding: 12px !important; border-radius: 0 0 12px 12px !important; }
52
  .input-box { border: 1px solid var(--border-color) !important; border-radius: 24px !important; padding: 12px 16px !important; box-shadow: 0 2px 4px rgba(0,0,0,0.05) !important; }
53
- .send-btn { background-color: var(--secondary-color) !important; border-radius: 24px !important; color: var(--white) !important; padding: 10px 20px !important; font-weight: 500 !important; }
54
- .clear-btn { background-color: #F0F0F0 !important; border: 1px solid var(--border-color) !important; border-radius: 24px !important; color: var(--dark-text) !important; padding: 8px 16px !important; font-weight: 500 !important; }
55
- .pdf-viewer-container { border-radius: 12px !important; box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important; background-color: var(--white) !important; border: 1px solid var(--border-color) !important; padding: 20px; }
56
  .pdf-viewer-image { max-width: 100%; height: auto; border: 1px solid var(--border-color); border-radius: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
57
- .stats-box { background-color: #E6F2E6; padding: 10px; border-radius: 8px; margin-top: 10px; }
58
- .tool-container { background-color: var(--white); border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); padding: 15px; margin-bottom: 20px; }
59
- .tool-title { color: var(--secondary-color); font-size: 1.2rem; font-weight: 600; margin-bottom: 10px; }
60
  .chart-container { height: 400px; width: 100%; border-radius: 8px; overflow: hidden; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  """
62
 
63
- # Function to process PDF files (unchanged)
64
  def process_pdf(pdf_file):
65
  if pdf_file is None:
66
- return None, "No file uploaded", {"page_images": [], "total_pages": 0, "total_words": 0}
 
67
  try:
68
  session_id = str(uuid.uuid4())
69
  with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as temp_file:
@@ -72,12 +174,7 @@ def process_pdf(pdf_file):
72
 
73
  doc = fitz.open(pdf_path)
74
  texts = [page.get_text() for page in doc]
75
- page_images = []
76
- for page in doc:
77
- pix = page.get_pixmap()
78
- img_bytes = pix.tobytes("png")
79
- img_base64 = base64.b64encode(img_bytes).decode("utf-8")
80
- page_images.append(img_base64)
81
  total_pages = len(doc)
82
  total_words = sum(len(text.split()) for text in texts)
83
  doc.close()
@@ -90,89 +187,85 @@ def process_pdf(pdf_file):
90
  user_vectorstores[session_id] = vectorstore
91
 
92
  os.unlink(pdf_path)
93
- pdf_state = {"page_images": page_images, "total_pages": total_pages, "total_words": total_words}
94
  return session_id, f"✅ Successfully processed {len(chunks)} text chunks from your PDF", pdf_state
95
  except Exception as e:
96
  if "pdf_path" in locals() and os.path.exists(pdf_path):
97
  os.unlink(pdf_path)
98
- return None, f"Error processing PDF: {str(e)}", {"page_images": [], "total_pages": 0, "total_words": 0}
99
 
100
- # Function to generate chatbot responses with Finance theme
101
- def generate_response(message, session_id, model_name, history):
102
- if not message:
103
- return history
 
 
104
  try:
105
- context = ""
106
- if session_id and session_id in user_vectorstores:
107
- vectorstore = user_vectorstores[session_id]
108
- docs = vectorstore.similarity_search(message, k=3)
109
- if docs:
110
- context = "\n\nRelevant information from uploaded PDF:\n" + "\n".join(f"- {doc.page_content}" for doc in docs)
111
-
112
- # Check if it's a stock ticker query
113
- if message.startswith("$") and len(message) > 1 and len(message) <= 6:
114
- ticker = message[1:].upper()
115
- try:
116
- stock_data = get_stock_data(ticker)
117
- response = f"**Stock Information for {ticker}**\n\n"
118
- response += f"Current Price: ${stock_data['current_price']}\n"
119
- response += f"52-Week High: ${stock_data['52wk_high']}\n"
120
- response += f"Market Cap: ${stock_data['market_cap']:,}\n"
121
- response += f"P/E Ratio: {stock_data['pe_ratio']}\n"
122
- response += f"More data available in the Stock Analysis tab."
123
- history.append((message, response))
124
- return history
125
- except Exception as e:
126
- history.append((message, f"Error retrieving stock data for {ticker}: {str(e)}"))
127
- return history
128
-
129
- system_prompt = "You are a financial assistant specializing in analyzing financial reports, statements, and market trends."
130
- system_prompt += " You can help with stock market information, financial terminology, ratio analysis, and investment concepts."
131
- if context:
132
- system_prompt += " Use the following context to answer the question if relevant: " + context
133
-
134
- completion = client.chat.completions.create(
135
- model=model_name,
136
- messages=[
137
- {"role": "system", "content": system_prompt},
138
- {"role": "user", "content": message}
139
- ],
140
- temperature=0.7,
141
- max_tokens=1024
142
- )
143
- response = completion.choices[0].message.content
144
- history.append((message, response))
145
- return history
146
  except Exception as e:
147
- history.append((message, f"Error generating response: {str(e)}"))
148
- return history
149
 
150
- # Functions to update PDF viewer (unchanged)
151
- def update_pdf_viewer(pdf_state):
152
- if not pdf_state["total_pages"]:
153
- return 0, None, "No PDF uploaded yet"
 
 
154
  try:
155
- img_data = base64.b64decode(pdf_state["page_images"][0])
156
- img = Image.open(io.BytesIO(img_data))
157
- return pdf_state["total_pages"], img, f"**Total Pages:** {pdf_state['total_pages']}\n**Total Words:** {pdf_state['total_words']}"
158
  except Exception as e:
159
- print(f"Error decoding image: {e}")
160
- return 0, None, "Error displaying PDF"
161
 
162
- def update_image(page_num, pdf_state):
163
- if not pdf_state["total_pages"] or page_num < 1 or page_num > pdf_state["total_pages"]:
164
- return None
165
- try:
166
- img_data = base64.b64decode(pdf_state["page_images"][page_num - 1])
167
- img = Image.open(io.BytesIO(img_data))
168
- return img
169
- except Exception as e:
170
- print(f"Error decoding image: {e}")
171
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
- # New Finance-specific tools
174
- def get_stock_data(ticker):
175
- """Tool to fetch latest stock data for a given ticker"""
176
  try:
177
  stock = yf.Ticker(ticker)
178
  info = stock.info
@@ -180,244 +273,241 @@ def get_stock_data(ticker):
180
  "current_price": info.get("currentPrice", info.get("regularMarketPrice", "N/A")),
181
  "52wk_high": info.get("fiftyTwoWeekHigh", "N/A"),
182
  "market_cap": info.get("marketCap", "N/A"),
183
- "pe_ratio": info.get("trailingPE", "N/A"),
184
- "dividend_yield": info.get("dividendYield", "N/A"),
185
- "beta": info.get("beta", "N/A"),
186
- "average_volume": info.get("averageVolume", "N/A")
187
  }
188
  except Exception as e:
189
- print(f"Error fetching stock data: {e}")
190
  raise e
191
 
192
- def get_stock_history(ticker, period="1y"):
193
- """Get historical data for charting"""
 
194
  try:
195
  stock = yf.Ticker(ticker)
196
- hist = stock.history(period=period)
197
- return hist
198
  except Exception as e:
199
- print(f"Error fetching stock history: {e}")
200
  return pd.DataFrame()
201
 
202
- def get_fred_data(indicator):
203
- """Get economic data from FRED API"""
204
- api_key = os.getenv("FRED_API_KEY", "")
205
- if not api_key:
206
- return "FRED API key not configured"
207
-
208
- base_url = "https://api.stlouisfed.org/fred/series/observations"
209
- params = {
210
- "series_id": indicator,
211
- "api_key": api_key,
212
- "file_type": "json",
213
- "sort_order": "desc",
214
- "limit": 100
215
- }
216
-
217
- try:
218
- response = requests.get(base_url, params=params)
219
- data = response.json()
220
- return data.get("observations", [])
221
- except Exception as e:
222
- print(f"Error fetching FRED data: {e}")
223
- return []
224
-
225
- def create_stock_chart(ticker, period="1y"):
226
- """Create an interactive stock chart using Plotly"""
227
- try:
228
- df = get_stock_history(ticker, period)
229
- if df.empty:
230
- return None
231
-
232
- fig = go.Figure()
233
-
234
- # Add candlestick chart
235
- fig.add_trace(
236
- go.Candlestick(
237
- x=df.index,
238
- open=df['Open'],
239
- high=df['High'],
240
- low=df['Low'],
241
- close=df['Close'],
242
- name=ticker
243
- )
244
- )
245
-
246
- # Add volume as bar chart on secondary y-axis
247
- fig.add_trace(
248
- go.Bar(
249
- x=df.index,
250
- y=df['Volume'],
251
- name='Volume',
252
- marker_color='rgba(0, 128, 0, 0.3)',
253
- yaxis='y2'
254
- )
255
- )
256
-
257
- # Update layout for dual y-axis
258
- fig.update_layout(
259
- title=f'{ticker} Stock Price',
260
- yaxis_title='Price (USD)',
261
- xaxis_title='Date',
262
- template='plotly_white',
263
- yaxis=dict(
264
- domain=[0.3, 1.0]
265
- ),
266
- yaxis2=dict(
267
- domain=[0, 0.2],
268
- title='Volume'
269
- ),
270
- legend=dict(
271
- orientation="h",
272
- yanchor="bottom",
273
- y=1.02,
274
- xanchor="right",
275
- x=1
276
- ),
277
- height=500
278
- )
279
-
280
- return fig
281
- except Exception as e:
282
- print(f"Error creating stock chart: {e}")
283
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
- def analyze_ticker(ticker_input, period):
286
- """Process the ticker input and return analysis"""
287
- if not ticker_input:
288
- return None, "Please enter a valid ticker symbol", None
 
 
 
 
289
 
290
- ticker = ticker_input.strip().upper()
291
- if ticker.startswith("$"):
292
- ticker = ticker[1:]
293
 
294
  try:
295
- stock_data = get_stock_data(ticker)
296
- chart = create_stock_chart(ticker, period)
297
-
298
- # Create a formatted summary
 
299
  summary = f"""
300
  ### {ticker} Analysis
301
  **Current Price:** ${stock_data['current_price']}
302
  **52-Week High:** ${stock_data['52wk_high']}
303
  **Market Cap:** ${stock_data['market_cap']:,}
304
  **P/E Ratio:** {stock_data['pe_ratio']}
305
- **Dividend Yield:** {stock_data['dividend_yield'] * 100 if stock_data['dividend_yield'] != 'N/A' else 'N/A'}%
306
- **Beta:** {stock_data['beta']}
307
- **Avg Volume:** {stock_data['average_volume']:,}
308
- """
309
-
310
  return chart, summary, ticker
311
  except Exception as e:
312
  return None, f"Error analyzing ticker {ticker}: {str(e)}", None
313
 
314
- # Gradio interface
315
- with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
316
- current_session_id = gr.State(None)
317
- pdf_state = gr.State({"page_images": [], "total_pages": 0, "total_words": 0})
318
- current_ticker = gr.State(None)
319
 
320
- gr.HTML("""
321
- <div class="header">
322
- <div class="header-title">Fin-Vision</div>
323
- <div class="header-subtitle">Analyze financial documents with Groq's LLM API.</div>
324
- </div>
325
- """)
326
 
327
- with gr.Row(elem_classes="container"):
328
- with gr.Column(scale=1, min_width=300):
329
- pdf_file = gr.File(label="Upload PDF Document", file_types=[".pdf"], type="binary")
330
- upload_button = gr.Button("Process PDF", variant="primary")
331
- pdf_status = gr.Markdown("No PDF uploaded yet")
332
- model_dropdown = gr.Dropdown(
333
- choices=["llama3-70b-8192", "llama3-8b-8192", "mixtral-8x7b-32768", "gemma-7b-it"],
334
- value="llama3-70b-8192",
335
- label="Select Groq Model"
336
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
- # Finance Tools Section
339
- gr.Markdown("### Financial Tools", elem_classes="tool-title")
340
- with gr.Group(elem_classes="tool-container"):
341
  with gr.Tabs():
342
- with gr.TabItem("Stock Analysis"):
343
- ticker_input = gr.Textbox(label="Enter Ticker Symbol (e.g., AAPL)", placeholder="AAPL")
344
- period_dropdown = gr.Dropdown(
345
- choices=["1mo", "3mo", "6mo", "1y", "2y", "5y", "max"],
346
- value="1y",
347
- label="Time Period"
348
- )
349
- analyze_button = gr.Button("Analyze Stock")
350
-
351
- with gr.Column(scale=2, min_width=600):
352
- with gr.Tabs():
353
- with gr.TabItem("PDF Viewer"):
354
- with gr.Column(elem_classes="pdf-viewer-container"):
355
  page_slider = gr.Slider(minimum=1, maximum=1, step=1, label="Page Number", value=1)
356
  pdf_image = gr.Image(label="PDF Page", type="pil", elem_classes="pdf-viewer-image")
357
  stats_display = gr.Markdown("No PDF uploaded yet", elem_classes="stats-box")
358
-
359
- with gr.TabItem("Stock Analysis"):
360
- with gr.Column(elem_classes="pdf-viewer-container"):
361
  stock_chart = gr.Plot(label="Stock Price Chart", elem_classes="chart-container")
362
  stock_summary = gr.Markdown("Enter a ticker symbol to see analysis")
363
-
364
- with gr.Row(elem_classes="container"):
365
- with gr.Column(scale=2, min_width=600):
366
  chatbot = gr.Chatbot(height=500, bubble_full_width=False, show_copy_button=True, elem_classes="chat-container")
367
  with gr.Row():
368
- msg = gr.Textbox(show_label=False, placeholder="Ask about your financial document or type $TICKER for stock info...", scale=5)
369
  send_btn = gr.Button("Send", scale=1)
370
- clear_btn = gr.Button("Clear Conversation")
371
-
372
- # Event Handlers
373
- upload_button.click(
374
- process_pdf,
375
- inputs=[pdf_file],
376
- outputs=[current_session_id, pdf_status, pdf_state]
377
- ).then(
378
- update_pdf_viewer,
379
- inputs=[pdf_state],
380
- outputs=[page_slider, pdf_image, stats_display]
381
- )
382
-
383
- msg.submit(
384
- generate_response,
385
- inputs=[msg, current_session_id, model_dropdown, chatbot],
386
- outputs=[chatbot]
387
- ).then(lambda: "", None, [msg])
388
-
389
- send_btn.click(
390
- generate_response,
391
- inputs=[msg, current_session_id, model_dropdown, chatbot],
392
- outputs=[chatbot]
393
- ).then(lambda: "", None, [msg])
394
-
395
- clear_btn.click(
396
- lambda: ([], None, "No PDF uploaded yet", {"page_images": [], "total_pages": 0, "total_words": 0}, 0, None, "No PDF uploaded yet", None),
397
- None,
398
- [chatbot, current_session_id, pdf_status, pdf_state, page_slider, pdf_image, stats_display, current_ticker]
399
- )
400
-
401
- page_slider.change(
402
- update_image,
403
- inputs=[page_slider, pdf_state],
404
- outputs=[pdf_image]
405
- )
 
 
 
 
 
 
406
 
407
- # Stock analysis handler
408
- analyze_button.click(
409
- analyze_ticker,
410
- inputs=[ticker_input, period_dropdown],
411
- outputs=[stock_chart, stock_summary, current_ticker]
412
- )
413
-
414
- # Add footer with attribution
415
- gr.HTML("""
416
- <div style="text-align: center; margin-top: 20px; padding: 10px; color: #666; font-size: 0.8rem; border-top: 1px solid #eee;">
417
- Created by Calvin Allen Crawford
418
- </div>
419
- """)
420
 
421
- # Launch the app
422
  if __name__ == "__main__":
 
423
  demo.launch()
 
1
+ # Standard library imports
 
2
  import os
3
  import tempfile
4
  import uuid
 
 
 
 
 
 
 
 
5
  import base64
 
6
  import io
 
7
  import json
8
+ import re
9
+ from datetime import datetime, timedelta
10
+
11
+ # Third-party imports
12
+ import gradio as gr
13
+ import groq
14
+ import numpy as np
15
+ import pandas as pd
16
+ import requests
17
+ import fitz # PyMuPDF
18
+ from PIL import Image
19
+ from dotenv import load_dotenv
20
+ import yfinance as yf
21
+ import plotly.graph_objects as go
22
+ from pydantic import BaseModel, validator
23
+
24
+ # LangChain imports
25
+ from langchain_community.embeddings import HuggingFaceEmbeddings
26
+ from langchain_community.vectorstores import FAISS
27
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
28
 
29
  # Load environment variables
30
  load_dotenv()
31
  client = groq.Client(api_key=os.getenv("GROQ_LEGAL_API_KEY"))
32
  embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
33
+ SERPER_API_KEY = os.getenv("SERPER_API_KEY")
34
+ BRAVE_API_KEY = os.getenv("BRAVE_API_KEY")
35
 
36
  # Directory to store FAISS indexes
37
  FAISS_INDEX_DIR = "faiss_indexes_finance"
38
  if not os.path.exists(FAISS_INDEX_DIR):
39
  os.makedirs(FAISS_INDEX_DIR)
40
 
41
+ # Dictionaries for state management
42
  user_vectorstores = {}
43
+ chart_data_store = {}
44
+
45
+ # Pydantic Models
46
+ class PDFState(BaseModel):
47
+ page_images: list[str]
48
+ total_pages: int
49
+ total_words: int
50
+
51
+ class StockAnalysisInput(BaseModel):
52
+ ticker: str
53
+ period: str
54
+
55
+ @validator('ticker')
56
+ def validate_ticker(cls, v):
57
+ v = v.strip().lstrip("$")
58
+ if not v.isalpha() or len(v) > 5:
59
+ raise ValueError('Invalid ticker symbol')
60
+ return v.upper()
61
+
62
+ @validator('period')
63
+ def validate_period(cls, v):
64
+ valid_periods = ["1mo", "3mo", "6mo", "1y", "2y", "5y", "max"]
65
+ if v not in valid_periods:
66
+ raise ValueError('Invalid period')
67
+ return v
68
 
69
+ class ModelName(str):
70
+ @classmethod
71
+ def __get_validators__(cls):
72
+ yield cls.validate
73
+
74
+ @classmethod
75
+ def validate(cls, v):
76
+ allowed_models = ["llama3-70b-8192", "llama3-8b-8192", "mixtral-8x7b-32768", "gemma-7b-it"]
77
+ if v not in allowed_models:
78
+ raise ValueError(f"Invalid model name: {v}")
79
+ return v
80
+
81
+ # Custom CSS with dark mode support
82
  custom_css = """
83
  :root {
84
+ --bg-color: #FFFFFF;
85
+ --text-color: #333333;
86
+ --primary-color: #0C4160;
87
+ --secondary-color: #0D6980;
88
+ --accent-color: #16A6DB;
89
+ --light-color: #EBF5FA;
90
  --border-color: #E5E7EB;
91
  }
92
+
93
+ [data-theme="dark"] {
94
+ --bg-color: #1E1E1E;
95
+ --text-color: #F5F5F5;
96
+ --primary-color: #16A6DB;
97
+ --secondary-color: #0D6980;
98
+ --accent-color: #0C4160;
99
+ --light-color: #333333;
100
+ --border-color: #444444;
101
+ }
102
+
103
+ body { background-color: var(--bg-color); color: var(--text-color); font-family: 'IBM Plex Sans', sans-serif; }
104
  .container { max-width: 1200px !important; margin: 0 auto !important; padding: 10px; }
105
+ .header { background-color: var(--primary-color); padding: 20px 0; margin-bottom: 20px; border-radius: 12px 12px 0 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
106
+ .header-title { color: var(--light-color); font-size: 1.8rem; font-weight: 700; text-align: center; }
107
+ .header-subtitle { color: var(--light-color); opacity: 0.8; font-size: 1rem; text-align: center; margin-top: 5px; }
108
+ .chat-container { border-radius: 12px !important; box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important; background-color: var(--bg-color) !important; border: 1px solid var(--border-color) !important; min-height: 500px; }
109
+ .message-user { background-color: var(--accent-color) !important; color: var(--light-color) !important; border-radius: 18px 18px 4px 18px !important; padding: 12px 16px !important; margin-left: auto !important; max-width: 80% !important; }
110
+ .message-bot { background-color: var(--light-color) !important; color: var(--text-color) !important; border-radius: 18px 18px 18px 4px !important; padding: 12px 16px !important; margin-right: auto !important; max-width: 80% !important; }
111
+ .input-area { background-color: var(--bg-color) !important; border-top: 1px solid var(--border-color) !important; padding: 12px !important; border-radius: 0 0 12px 12px !important; }
112
  .input-box { border: 1px solid var(--border-color) !important; border-radius: 24px !important; padding: 12px 16px !important; box-shadow: 0 2px 4px rgba(0,0,0,0.05) !important; }
113
+ .send-btn { background-color: var(--accent-color) !important; border-radius: 24px !important; color: var(--light-color) !important; padding: 10px 20px !important; font-weight: 500 !important; }
114
+ .clear-btn { background-color: var(--light-color) !important; border: 1px solid var(--border-color) !important; border-radius: 24px !important; color: var(--text-color) !important; padding: 8px 16px !important; font-weight: 500 !important; }
115
+ .pdf-viewer-container { border-radius: 12px !important; box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important; background-color: var(--bg-color) !important; border: 1px solid var(--border-color) !important; padding: 20px; }
116
  .pdf-viewer-image { max-width: 100%; height: auto; border: 1px solid var(--border-color); border-radius: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
117
+ .stats-box { background-color: var(--light-color); padding: 10px; border-radius: 8px; margin-top: 10px; }
118
+ .tool-container { background-color: var(--bg-color); border-radius: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); padding: 15px; margin-bottom: 20px; }
119
+ .tool-title { font-weight: bold; color: var(--primary-color); margin-bottom: 10px; font-size: 1.1rem; }
120
  .chart-container { height: 400px; width: 100%; border-radius: 8px; overflow: hidden; }
121
+ .toggle-container { display: flex; align-items: center; margin-bottom: 15px; }
122
+ .toggle-label { margin-right: 10px; font-weight: 500; }
123
+ .search-toggle { margin-left: 5px; }
124
+ .spinner { border: 4px solid #f3f3f3; border-top: 4px solid var(--primary-color); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; }
125
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
126
+ """
127
+
128
+ # Custom JavaScript for UI enhancements
129
+ custom_js = """
130
+ function toggleTheme() {
131
+ const currentTheme = document.body.getAttribute('data-theme');
132
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
133
+ document.body.setAttribute('data-theme', newTheme);
134
+ localStorage.setItem('theme', newTheme);
135
+ }
136
+
137
+ function showSpinner() {
138
+ document.getElementById('spinner').style.display = 'block';
139
+ }
140
+
141
+ function hideSpinner() {
142
+ document.getElementById('spinner').style.display = 'none';
143
+ }
144
+
145
+ document.addEventListener('DOMContentLoaded', () => {
146
+ const savedTheme = localStorage.getItem('theme');
147
+ if (savedTheme) {
148
+ document.body.setAttribute('data-theme', savedTheme);
149
+ }
150
+ tippy('#pdf_file', { content: 'Upload a PDF document for analysis', placement: 'top' });
151
+ tippy('#ticker_input', { content: 'Enter a stock ticker symbol (e.g., AAPL)', placement: 'top' });
152
+ });
153
+ """
154
+
155
+ # Spinner HTML and Tippy.js for tooltips
156
+ custom_html = """
157
+ <div id="spinner" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);">
158
+ <div class="spinner"></div>
159
+ </div>
160
+ <script src="https://unpkg.com/@popperjs/core@2"></script>
161
+ <script src="https://unpkg.com/tippy.js@6"></script>
162
  """
163
 
164
+ # Helper Functions
165
  def process_pdf(pdf_file):
166
  if pdf_file is None:
167
+ return None, "No file uploaded", PDFState(page_images=[], total_pages=0, total_words=0)
168
+
169
  try:
170
  session_id = str(uuid.uuid4())
171
  with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as temp_file:
 
174
 
175
  doc = fitz.open(pdf_path)
176
  texts = [page.get_text() for page in doc]
177
+ page_images = [base64.b64encode(page.get_pixmap().tobytes("png")).decode("utf-8") for page in doc]
 
 
 
 
 
178
  total_pages = len(doc)
179
  total_words = sum(len(text.split()) for text in texts)
180
  doc.close()
 
187
  user_vectorstores[session_id] = vectorstore
188
 
189
  os.unlink(pdf_path)
190
+ pdf_state = PDFState(page_images=page_images, total_pages=total_pages, total_words=total_words)
191
  return session_id, f"✅ Successfully processed {len(chunks)} text chunks from your PDF", pdf_state
192
  except Exception as e:
193
  if "pdf_path" in locals() and os.path.exists(pdf_path):
194
  os.unlink(pdf_path)
195
+ return None, f"Error processing PDF: {str(e)}", PDFState(page_images=[], total_pages=0, total_words=0)
196
 
197
+ def serper_search(query):
198
+ if not SERPER_API_KEY:
199
+ return {"error": "Serper API key not configured."}
200
+ url = "https://google.serper.dev/search"
201
+ payload = json.dumps({"q": query, "gl": "us", "hl": "en", "autocorrect": True})
202
+ headers = {'X-API-KEY': SERPER_API_KEY, 'Content-Type': 'application/json'}
203
  try:
204
+ response = requests.post(url, headers=headers, data=payload)
205
+ return response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  except Exception as e:
207
+ return {"error": str(e)}
 
208
 
209
+ def brave_search(query):
210
+ if not BRAVE_API_KEY:
211
+ return {"error": "Brave Search API key not configured."}
212
+ url = "https://api.search.brave.com/res/v1/web/search"
213
+ params = {"q": query, "count": 10, "search_lang": "en", "country": "us"}
214
+ headers = {'Accept': 'application/json', 'X-Subscription-Token': BRAVE_API_KEY}
215
  try:
216
+ response = requests.get(url, params=params, headers=headers)
217
+ return response.json()
 
218
  except Exception as e:
219
+ return {"error": str(e)}
 
220
 
221
+ def get_financial_news(ticker, use_brave_search, enable_search, model_name="llama3-8b-8192"):
222
+ if not enable_search:
223
+ return [{"title": "Real-time search disabled", "snippet": "Enable real-time search to fetch news"}]
224
+ query = f"{ticker} stock news financial analysis latest"
225
+ if use_brave_search and BRAVE_API_KEY:
226
+ results = brave_search(query)
227
+ if "web" in results and "results" in results["web"]:
228
+ return [{"title": item.get("title", ""), "link": item.get("url", ""), "snippet": item.get("description", ""), "source": item.get("source", "")}
229
+ for item in results["web"]["results"][:5]]
230
+ if SERPER_API_KEY:
231
+ results = serper_search(query)
232
+ if "organic" in results:
233
+ return [{"title": item.get("title", ""), "link": item.get("link", ""), "snippet": item.get("snippet", ""), "source": item.get("source", "")}
234
+ for item in results["organic"][:5]]
235
+ return [{"title": "No news available", "snippet": "Search APIs not configured or failed."}]
236
+
237
+ def get_market_sentiment(ticker, use_brave_search, enable_search, model_name="llama3-8b-8192"):
238
+ if not enable_search:
239
+ return "Real-time search is disabled"
240
+ query = f"{ticker} stock market sentiment analysis"
241
+ snippets = []
242
+ if use_brave_search and BRAVE_API_KEY:
243
+ results = brave_search(query)
244
+ if "web" in results and "results" in results["web"]:
245
+ snippets = [item["description"] for item in results["web"]["results"][:3] if "description" in item]
246
+ elif SERPER_API_KEY:
247
+ results = serper_search(query)
248
+ if "organic" in results:
249
+ snippets = [item["snippet"] for item in results["organic"][:3] if "snippet" in item]
250
+ if snippets:
251
+ try:
252
+ completion = client.chat.completions.create(
253
+ model=ModelName.validate(model_name),
254
+ messages=[
255
+ {"role": "system", "content": "Analyze the sentiment based on the provided text."},
256
+ {"role": "user", "content": "\n".join(snippets)}
257
+ ],
258
+ temperature=0.2,
259
+ max_tokens=150
260
+ )
261
+ return completion.choices[0].message.content
262
+ except Exception as e:
263
+ return "Unable to determine sentiment"
264
+ return "No sentiment data available"
265
 
266
+ def get_stock_data(ticker, enable_stock_data):
267
+ if not enable_stock_data:
268
+ return {"message": "Real-time stock data is disabled"}
269
  try:
270
  stock = yf.Ticker(ticker)
271
  info = stock.info
 
273
  "current_price": info.get("currentPrice", info.get("regularMarketPrice", "N/A")),
274
  "52wk_high": info.get("fiftyTwoWeekHigh", "N/A"),
275
  "market_cap": info.get("marketCap", "N/A"),
276
+ "pe_ratio": info.get("trailingPE", "N/A")
 
 
 
277
  }
278
  except Exception as e:
 
279
  raise e
280
 
281
+ def get_stock_history(ticker, period, enable_stock_data):
282
+ if not enable_stock_data:
283
+ return pd.DataFrame()
284
  try:
285
  stock = yf.Ticker(ticker)
286
+ return stock.history(period=period)
 
287
  except Exception as e:
 
288
  return pd.DataFrame()
289
 
290
+ def create_stock_chart(ticker, period, enable_stock_data):
291
+ if not enable_stock_data:
292
+ return None
293
+ df = get_stock_history(ticker, period, enable_stock_data)
294
+ if df.empty:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  return None
296
+ fig = go.Figure()
297
+ fig.add_trace(go.Candlestick(x=df.index, open=df['Open'], high=df['High'], low=df['Low'], close=df['Close'], name=ticker))
298
+ fig.add_trace(go.Bar(x=df.index, y=df['Volume'], name='Volume', marker_color='rgba(0, 128, 0, 0.3)', yaxis='y2'))
299
+ fig.update_layout(
300
+ title=f'{ticker} Stock Price',
301
+ yaxis_title='Price (USD)',
302
+ xaxis_title='Date',
303
+ template='plotly_white',
304
+ yaxis=dict(domain=[0.3, 1.0]),
305
+ yaxis2=dict(domain=[0, 0.2], title='Volume'),
306
+ height=500
307
+ )
308
+ return fig
309
 
310
+ def analyze_ticker(ticker_input, period, use_brave_search, enable_stock_data, enable_search):
311
+ try:
312
+ input_data = StockAnalysisInput(ticker=ticker_input, period=period)
313
+ except ValueError as e:
314
+ return None, str(e), None
315
+
316
+ ticker = input_data.ticker
317
+ period = input_data.period
318
 
319
+ if not enable_stock_data:
320
+ return None, "Real-time stock data is disabled.", None
 
321
 
322
  try:
323
+ stock_data = get_stock_data(ticker, enable_stock_data)
324
+ stock_history = get_stock_history(ticker, period, enable_stock_data)
325
+ chart = create_stock_chart(ticker, period, enable_stock_data)
326
+ chart_data_store[ticker] = {"history": stock_history, "stats": stock_data, "period": period}
327
+ sentiment = get_market_sentiment(ticker, use_brave_search, enable_search, model_name="llama3-8b-8192")
328
  summary = f"""
329
  ### {ticker} Analysis
330
  **Current Price:** ${stock_data['current_price']}
331
  **52-Week High:** ${stock_data['52wk_high']}
332
  **Market Cap:** ${stock_data['market_cap']:,}
333
  **P/E Ratio:** {stock_data['pe_ratio']}
334
+ **Market Sentiment:** {sentiment}
335
+ """
 
 
 
336
  return chart, summary, ticker
337
  except Exception as e:
338
  return None, f"Error analyzing ticker {ticker}: {str(e)}", None
339
 
340
+ def generate_response(message, session_id, model_name, history, current_ticker, use_brave_search, enable_search, enable_stock_data):
341
+ if not message:
342
+ return history
 
 
343
 
344
+ try:
345
+ model_name = ModelName.validate(model_name)
346
+ except ValueError as e:
347
+ return history + [(message, str(e))]
 
 
348
 
349
+ try:
350
+ context = ""
351
+ if session_id in user_vectorstores:
352
+ vectorstore = user_vectorstores[session_id]
353
+ docs = vectorstore.similarity_search(message, k=3)
354
+ if docs:
355
+ context = "\n\nRelevant information from uploaded PDF:\n" + "\n".join(f"- {doc.page_content}" for doc in docs)
356
+
357
+ if message.startswith("$") and len(message) > 1 and len(message) <= 6:
358
+ ticker = message[1:].upper()
359
+ stock_data = get_stock_data(ticker, enable_stock_data)
360
+ news = get_financial_news(ticker, use_brave_search, enable_search, model_name)
361
+ sentiment = get_market_sentiment(ticker, use_brave_search, enable_search, model_name)
362
+ response = f"**Stock Information for {ticker}**\n\n"
363
+ if "message" in stock_data:
364
+ response += stock_data["message"] + "\n"
365
+ else:
366
+ response += f"Current Price: ${stock_data['current_price']}\n52-Week High: ${stock_data['52wk_high']}\nMarket Cap: ${stock_data['market_cap']:,}\nP/E Ratio: {stock_data['pe_ratio']}\n"
367
+ response += f"**Market Sentiment:**\n{sentiment}\n\n**Recent News:**\n"
368
+ for i, item in enumerate(news[:3]):
369
+ response += f"{i+1}. [{item['title']}]({item['link']})\n {item['snippet'][:100]}...\n"
370
+ history.append((message, response))
371
+ return history
372
+
373
+ if message.lower().startswith("/news "):
374
+ topic = message[6:].strip()
375
+ news = get_financial_news(topic, use_brave_search, enable_search, model_name)
376
+ response = f"**Latest Financial News on {topic}:**\n\n"
377
+ for i, item in enumerate(news[:5]):
378
+ response += f"{i+1}. **{item['title']}**\n {item['snippet']}\n [Read more]({item['link']})\n\n"
379
+ history.append((message, response))
380
+ return history
381
+
382
+ system_prompt = "You are a financial assistant specializing in analyzing financial reports, statements, and market trends."
383
+ if context:
384
+ system_prompt += " Use the following context if relevant: " + context
385
+ completion = client.chat.completions.create(
386
+ model=model_name,
387
+ messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": message}],
388
+ temperature=0.7,
389
+ max_tokens=1024
390
+ )
391
+ response = completion.choices[0].message.content
392
+ history.append((message, response))
393
+ return history
394
+ except Exception as e:
395
+ history.append((message, f"Error generating response: {str(e)}"))
396
+ return history
397
+
398
+ def update_pdf_viewer(pdf_state: PDFState):
399
+ if not pdf_state.total_pages:
400
+ return 0, None, "No PDF uploaded yet"
401
+ img_data = base64.b64decode(pdf_state.page_images[0])
402
+ img = Image.open(io.BytesIO(img_data))
403
+ return pdf_state.total_pages, img, f"**Total Pages:** {pdf_state.total_pages}\n**Total Words:** {pdf_state.total_words}"
404
+
405
+ def update_image(page_num, pdf_state: PDFState):
406
+ if not pdf_state.total_pages or page_num < 1 or page_num > pdf_state.total_pages:
407
+ return None
408
+ img_data = base64.b64decode(pdf_state.page_images[page_num - 1])
409
+ return Image.open(io.BytesIO(img_data))
410
+
411
+ # Gradio Interface
412
+ def create_interface():
413
+ with gr.Blocks(css=custom_css, js=custom_js) as demo:
414
+ gr.HTML(custom_html)
415
+ current_session_id = gr.State(None)
416
+ pdf_state = gr.State(PDFState(page_images=[], total_pages=0, total_words=0))
417
+ current_ticker = gr.State(None)
418
+
419
+ gr.HTML("""
420
+ <div class="header">
421
+ <div class="header-title">Fin-Vision</div>
422
+ <div class="header-subtitle">Analyze financial documents with Groq's LLM API.</div>
423
+ </div>
424
+ """)
425
+
426
+ with gr.Row(elem_classes="container"):
427
+ with gr.Column(scale=1, min_width=300):
428
+ pdf_file = gr.File(label="Upload PDF Document", file_types=[".pdf"], type="binary", elem_id="pdf_file")
429
+ upload_button = gr.Button("Process PDF", variant="primary")
430
+ pdf_status = gr.Markdown("No PDF uploaded yet")
431
+
432
+ with gr.Group():
433
+ gr.Markdown("### Real-Time Data Settings")
434
+ enable_stock_data = gr.Checkbox(label="Enable Real-Time Stock Data", value=True)
435
+ enable_search = gr.Checkbox(label="Enable Real-Time Search", value=True)
436
+ use_brave_search = gr.Checkbox(label="Use Brave Search (unchecked = Serper)", value=False)
437
+
438
+ model_dropdown = gr.Dropdown(
439
+ choices=["llama3-70b-8192", "llama3-8b-8192", "mixtral-8x7b-32768", "gemma-7b-it"],
440
+ value="llama3-70b-8192",
441
+ label="Select Groq Model"
442
+ )
443
+ theme_button = gr.Button("Toggle Theme")
444
+
445
+ with gr.Group(elem_classes="tool-container"):
446
+ ticker_input = gr.Textbox(label="Enter Ticker Symbol (e.g., AAPL)", placeholder="AAPL", elem_id="ticker_input")
447
+ period_dropdown = gr.Dropdown(choices=["1mo", "3mo", "6mo", "1y", "2y", "5y", "max"], value="1y", label="Time Period")
448
+ analyze_button = gr.Button("Analyze Stock")
449
 
450
+ with gr.Column(scale=2, min_width=600):
 
 
451
  with gr.Tabs():
452
+ with gr.TabItem("PDF Viewer"):
 
 
 
 
 
 
 
 
 
 
 
 
453
  page_slider = gr.Slider(minimum=1, maximum=1, step=1, label="Page Number", value=1)
454
  pdf_image = gr.Image(label="PDF Page", type="pil", elem_classes="pdf-viewer-image")
455
  stats_display = gr.Markdown("No PDF uploaded yet", elem_classes="stats-box")
456
+
457
+ with gr.TabItem("Stock Analysis"):
 
458
  stock_chart = gr.Plot(label="Stock Price Chart", elem_classes="chart-container")
459
  stock_summary = gr.Markdown("Enter a ticker symbol to see analysis")
460
+
461
+ with gr.Row(elem_classes="container"):
 
462
  chatbot = gr.Chatbot(height=500, bubble_full_width=False, show_copy_button=True, elem_classes="chat-container")
463
  with gr.Row():
464
+ msg = gr.Textbox(show_label=False, placeholder="Ask about your financial document...", scale=5)
465
  send_btn = gr.Button("Send", scale=1)
466
+
467
+ # Event Handlers
468
+ upload_button.click(
469
+ js="showSpinner",
470
+ fn=process_pdf,
471
+ inputs=[pdf_file],
472
+ outputs=[current_session_id, pdf_status, pdf_state]
473
+ ).then(
474
+ update_pdf_viewer,
475
+ inputs=[pdf_state],
476
+ outputs=[page_slider, pdf_image, stats_display]
477
+ ).then(
478
+ js="hideSpinner",
479
+ inputs=[],
480
+ outputs=[]
481
+ )
482
+
483
+ analyze_button.click(
484
+ js="showSpinner",
485
+ fn=analyze_ticker,
486
+ inputs=[ticker_input, period_dropdown, use_brave_search, enable_stock_data, enable_search],
487
+ outputs=[stock_chart, stock_summary, current_ticker]
488
+ ).then(
489
+ js="hideSpinner",
490
+ inputs=[],
491
+ outputs=[]
492
+ )
493
+
494
+ msg.submit(
495
+ generate_response,
496
+ inputs=[msg, current_session_id, model_dropdown, chatbot, current_ticker, use_brave_search, enable_search, enable_stock_data],
497
+ outputs=[chatbot]
498
+ ).then(lambda: "", None, [msg])
499
+
500
+ send_btn.click(
501
+ generate_response,
502
+ inputs=[msg, current_session_id, model_dropdown, chatbot, current_ticker, use_brave_search, enable_search, enable_stock_data],
503
+ outputs=[chatbot]
504
+ ).then(lambda: "", None, [msg])
505
+
506
+ page_slider.change(update_image, inputs=[page_slider, pdf_state], outputs=[pdf_image])
507
+ theme_button.click(js="toggleTheme")
508
 
509
+ return demo
 
 
 
 
 
 
 
 
 
 
 
 
510
 
 
511
  if __name__ == "__main__":
512
+ demo = create_interface()
513
  demo.launch()