CosmickVisions commited on
Commit
e660b38
·
verified ·
1 Parent(s): 51111e7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +670 -333
app.py CHANGED
@@ -6,7 +6,6 @@ import base64
6
  import io
7
  import json
8
  import re
9
- import logging
10
  from datetime import datetime, timedelta
11
 
12
  # Third-party imports
@@ -18,18 +17,12 @@ import requests
18
  import fitz # PyMuPDF
19
  from PIL import Image
20
  from dotenv import load_dotenv
21
- import yfinance as yf
22
- import plotly.graph_objects as go
23
- from pydantic import BaseModel, validator
24
 
25
  # LangChain imports
26
  from langchain_community.embeddings import HuggingFaceEmbeddings
27
  from langchain_community.vectorstores import FAISS
28
  from langchain.text_splitter import RecursiveCharacterTextSplitter
29
 
30
- # Setup logging
31
- logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
32
-
33
  # Load environment variables
34
  load_dotenv()
35
  client = groq.Client(api_key=os.getenv("GROQ_LEGAL_API_KEY"))
@@ -42,131 +35,51 @@ FAISS_INDEX_DIR = "faiss_indexes_finance"
42
  if not os.path.exists(FAISS_INDEX_DIR):
43
  os.makedirs(FAISS_INDEX_DIR)
44
 
45
- # Dictionaries for state management
46
  user_vectorstores = {}
 
 
47
  chart_data_store = {}
48
 
49
- # Pydantic Models
50
- class PDFState(BaseModel):
51
- page_images: list[str]
52
- total_pages: int
53
- total_words: int
54
-
55
- class StockAnalysisInput(BaseModel):
56
- ticker: str
57
- period: str
58
-
59
- @validator('ticker')
60
- def validate_ticker(cls, v):
61
- v = v.strip().lstrip("$")
62
- if not v.isalpha() or len(v) > 5:
63
- raise ValueError('Invalid ticker symbol')
64
- return v.upper()
65
-
66
- @validator('period')
67
- def validate_period(cls, v):
68
- valid_periods = ["1mo", "3mo", "6mo", "1y", "2y", "5y", "max"]
69
- if v not in valid_periods:
70
- raise ValueError('Invalid period')
71
- return v
72
-
73
- class ModelName(str):
74
- @classmethod
75
- def __get_validators__(cls):
76
- yield cls.validate
77
-
78
- @classmethod
79
- def validate(cls, v):
80
- allowed_models = ["llama3-70b-8192", "llama3-8b-8192", "mixtral-8x7b-32768", "gemma-7b-it"]
81
- if v not in allowed_models:
82
- raise ValueError(f"Invalid model name: {v}")
83
- return v
84
-
85
- # Custom CSS
86
  custom_css = """
87
  :root {
88
- --bg-color: #FFFFFF;
89
- --text-color: #333333;
90
  --primary-color: #0C4160;
91
  --secondary-color: #0D6980;
92
  --accent-color: #16A6DB;
93
  --light-color: #EBF5FA;
 
 
94
  --border-color: #E5E7EB;
95
  }
96
-
97
- [data-theme="dark"] {
98
- --bg-color: #1E1E1E;
99
- --text-color: #F5F5F5;
100
- --primary-color: #16A6DB;
101
- --secondary-color: #0D6980;
102
- --accent-color: #0C4160;
103
- --light-color: #333333;
104
- --border-color: #444444;
105
- }
106
-
107
- body { background-color: var(--bg-color); color: var(--text-color); font-family: 'IBM Plex Sans', sans-serif; }
108
  .container { max-width: 1200px !important; margin: 0 auto !important; padding: 10px; }
109
  .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); }
110
- .header-title { color: var(--light-color); font-size: 1.8rem; font-weight: 700; text-align: center; }
111
- .header-subtitle { color: var(--light-color); opacity: 0.8; font-size: 1rem; text-align: center; margin-top: 5px; }
112
- .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; }
113
- .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; }
114
- .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; }
115
- .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; }
116
  .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; }
117
- .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; }
118
- .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; }
119
- .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; }
120
  .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); }
121
  .stats-box { background-color: var(--light-color); padding: 10px; border-radius: 8px; margin-top: 10px; }
122
- .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; }
123
  .tool-title { font-weight: bold; color: var(--primary-color); margin-bottom: 10px; font-size: 1.1rem; }
124
  .chart-container { height: 400px; width: 100%; border-radius: 8px; overflow: hidden; }
125
  .toggle-container { display: flex; align-items: center; margin-bottom: 15px; }
126
  .toggle-label { margin-right: 10px; font-weight: 500; }
127
  .search-toggle { margin-left: 5px; }
128
- .spinner { border: 4px solid #f3f3f3; border-top: 4px solid var(--primary-color); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; }
129
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
130
- """
131
-
132
- # Custom JavaScript (simplified to avoid interference)
133
- custom_js = """
134
- function toggleTheme() {
135
- const currentTheme = document.body.getAttribute('data-theme');
136
- const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
137
- document.body.setAttribute('data-theme', newTheme);
138
- localStorage.setItem('theme', newTheme);
139
- }
140
-
141
- function showSpinner() {
142
- document.getElementById('spinner').style.display = 'block';
143
- }
144
-
145
- function hideSpinner() {
146
- document.getElementById('spinner').style.display = 'none';
147
- }
148
-
149
- document.addEventListener('DOMContentLoaded', () => {
150
- const savedTheme = localStorage.getItem('theme');
151
- if (savedTheme) {
152
- document.body.setAttribute('data-theme', savedTheme);
153
- }
154
- });
155
  """
156
 
157
- # Spinner HTML
158
- custom_html = """
159
- <div id="spinner" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);">
160
- <div class="spinner"></div>
161
- </div>
162
- """
163
-
164
- # Helper Functions
165
  def process_pdf(pdf_file):
166
- logging.debug("Processing PDF file")
167
  if pdf_file is None:
168
- return None, "No file uploaded", PDFState(page_images=[], total_pages=0, total_words=0)
169
-
170
  try:
171
  session_id = str(uuid.uuid4())
172
  with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as temp_file:
@@ -175,7 +88,12 @@ def process_pdf(pdf_file):
175
 
176
  doc = fitz.open(pdf_path)
177
  texts = [page.get_text() for page in doc]
178
- page_images = [base64.b64encode(page.get_pixmap().tobytes("png")).decode("utf-8") for page in doc]
 
 
 
 
 
179
  total_pages = len(doc)
180
  total_words = sum(len(text.split()) for text in texts)
181
  doc.close()
@@ -188,244 +106,628 @@ def process_pdf(pdf_file):
188
  user_vectorstores[session_id] = vectorstore
189
 
190
  os.unlink(pdf_path)
191
- pdf_state = PDFState(page_images=page_images, total_pages=total_pages, total_words=total_words)
192
- logging.debug(f"PDF processed successfully: {session_id}")
193
  return session_id, f"✅ Successfully processed {len(chunks)} text chunks from your PDF", pdf_state
194
  except Exception as e:
195
- logging.error(f"Error processing PDF: {str(e)}")
196
  if "pdf_path" in locals() and os.path.exists(pdf_path):
197
  os.unlink(pdf_path)
198
- return None, f"Error processing PDF: {str(e)}", PDFState(page_images=[], total_pages=0, total_words=0)
199
 
200
- def serper_search(query):
 
 
 
 
201
  if not SERPER_API_KEY:
202
- return {"error": "Serper API key not configured."}
 
203
  url = "https://google.serper.dev/search"
204
- payload = json.dumps({"q": query, "gl": "us", "hl": "en", "autocorrect": True})
205
- headers = {'X-API-KEY': SERPER_API_KEY, 'Content-Type': 'application/json'}
 
 
 
 
 
 
 
 
 
206
  try:
207
- response = requests.post(url, headers=headers, data=payload)
208
  return response.json()
209
  except Exception as e:
 
210
  return {"error": str(e)}
211
 
212
- def brave_search(query):
 
 
 
 
213
  if not BRAVE_API_KEY:
214
- return {"error": "Brave Search API key not configured."}
 
215
  url = "https://api.search.brave.com/res/v1/web/search"
216
- params = {"q": query, "count": 10, "search_lang": "en", "country": "us"}
217
- headers = {'Accept': 'application/json', 'X-Subscription-Token': BRAVE_API_KEY}
 
 
 
 
 
 
 
 
 
 
218
  try:
219
  response = requests.get(url, params=params, headers=headers)
220
  return response.json()
221
  except Exception as e:
 
222
  return {"error": str(e)}
223
 
224
- def get_financial_news(ticker, use_brave_search, enable_search, model_name="llama3-8b-8192"):
225
- if not enable_search:
226
- return [{"title": "Real-time search disabled", "snippet": "Enable real-time search to fetch news"}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  query = f"{ticker} stock news financial analysis latest"
 
 
 
228
  if use_brave_search and BRAVE_API_KEY:
229
  results = brave_search(query)
230
  if "web" in results and "results" in results["web"]:
231
- return [{"title": item.get("title", ""), "link": item.get("url", ""), "snippet": item.get("description", ""), "source": item.get("source", "")}
232
- for item in results["web"]["results"][:5]]
233
- if SERPER_API_KEY:
 
 
 
 
 
 
 
 
234
  results = serper_search(query)
235
  if "organic" in results:
236
- return [{"title": item.get("title", ""), "link": item.get("link", ""), "snippet": item.get("snippet", ""), "source": item.get("source", "")}
237
- for item in results["organic"][:5]]
238
- return [{"title": "No news available", "snippet": "Search APIs not configured or failed."}]
239
-
240
- def get_market_sentiment(ticker, use_brave_search, enable_search, model_name="llama3-8b-8192"):
241
- if not enable_search:
242
- return "Real-time search is disabled"
 
 
 
 
 
 
 
 
 
 
 
243
  query = f"{ticker} stock market sentiment analysis"
244
  snippets = []
 
 
245
  if use_brave_search and BRAVE_API_KEY:
246
  results = brave_search(query)
247
  if "web" in results and "results" in results["web"]:
248
- snippets = [item["description"] for item in results["web"]["results"][:3] if "description" in item]
249
- elif SERPER_API_KEY:
 
 
 
 
250
  results = serper_search(query)
251
  if "organic" in results:
252
- snippets = [item["snippet"] for item in results["organic"][:3] if "snippet" in item]
 
 
 
 
253
  if snippets:
 
 
 
 
 
 
 
254
  try:
255
  completion = client.chat.completions.create(
256
- model=ModelName.validate(model_name),
257
  messages=[
258
- {"role": "system", "content": "Analyze the sentiment based on the provided text."},
259
- {"role": "user", "content": "\n".join(snippets)}
260
  ],
261
  temperature=0.2,
262
  max_tokens=150
263
  )
264
  return completion.choices[0].message.content
265
  except Exception as e:
 
266
  return "Unable to determine sentiment"
267
- return "No sentiment data available"
268
-
269
- def get_stock_data(ticker, enable_stock_data):
270
- if not enable_stock_data:
271
- return {"message": "Real-time stock data is disabled"}
272
- try:
273
- stock = yf.Ticker(ticker)
274
- info = stock.info
275
- return {
276
- "current_price": info.get("currentPrice", info.get("regularMarketPrice", "N/A")),
277
- "52wk_high": info.get("fiftyTwoWeekHigh", "N/A"),
278
- "market_cap": info.get("marketCap", "N/A"),
279
- "pe_ratio": info.get("trailingPE", "N/A")
280
- }
281
- except Exception as e:
282
- raise e
283
-
284
- def get_stock_history(ticker, period, enable_stock_data):
285
- if not enable_stock_data:
286
- return pd.DataFrame()
287
- try:
288
- stock = yf.Ticker(ticker)
289
- return stock.history(period=period)
290
- except Exception as e:
291
- return pd.DataFrame()
292
-
293
- def create_stock_chart(ticker, period, enable_stock_data):
294
- if not enable_stock_data:
295
- return None
296
- df = get_stock_history(ticker, period, enable_stock_data)
297
- if df.empty:
298
- return None
299
- fig = go.Figure()
300
- fig.add_trace(go.Candlestick(x=df.index, open=df['Open'], high=df['High'], low=df['Low'], close=df['Close'], name=ticker))
301
- fig.add_trace(go.Bar(x=df.index, y=df['Volume'], name='Volume', marker_color='rgba(0, 128, 0, 0.3)', yaxis='y2'))
302
- fig.update_layout(
303
- title=f'{ticker} Stock Price',
304
- yaxis_title='Price (USD)',
305
- xaxis_title='Date',
306
- template='plotly_white',
307
- yaxis=dict(domain=[0.3, 1.0]),
308
- yaxis2=dict(domain=[0, 0.2], title='Volume'),
309
- height=500
310
- )
311
- return fig
312
-
313
- def analyze_ticker(ticker_input, period, use_brave_search, enable_stock_data, enable_search):
314
- logging.debug(f"Analyzing ticker: {ticker_input}, period: {period}")
315
- try:
316
- input_data = StockAnalysisInput(ticker=ticker_input, period=period)
317
- except ValueError as e:
318
- logging.error(f"Validation error: {str(e)}")
319
- return None, str(e), None
320
-
321
- ticker = input_data.ticker
322
- period = input_data.period
323
-
324
- if not enable_stock_data:
325
- return None, "Real-time stock data is disabled.", None
326
 
 
327
  try:
328
- stock_data = get_stock_data(ticker, enable_stock_data)
329
- stock_history = get_stock_history(ticker, period, enable_stock_data)
330
- chart = create_stock_chart(ticker, period, enable_stock_data)
331
- chart_data_store[ticker] = {"history": stock_history, "stats": stock_data, "period": period}
332
- sentiment = get_market_sentiment(ticker, use_brave_search, enable_search, model_name="llama3-8b-8192")
333
- summary = f"""
334
- ### {ticker} Analysis
335
- **Current Price:** ${stock_data['current_price']}
336
- **52-Week High:** ${stock_data['52wk_high']}
337
- **Market Cap:** ${stock_data['market_cap']:,}
338
- **P/E Ratio:** {stock_data['pe_ratio']}
339
- **Market Sentiment:** {sentiment}
340
- """
341
- logging.debug(f"Ticker {ticker} analyzed successfully")
342
- return chart, summary, ticker
343
  except Exception as e:
344
- logging.error(f"Error analyzing ticker {ticker}: {str(e)}")
345
- return None, f"Error analyzing ticker {ticker}: {str(e)}", None
346
 
347
- def generate_response(message, session_id, model_name, history, current_ticker, use_brave_search, enable_search, enable_stock_data):
348
- logging.debug(f"Generating response for message: {message}")
349
  if not message:
350
  return history
351
-
352
- try:
353
- model_name = ModelName.validate(model_name)
354
- except ValueError as e:
355
- logging.error(f"Model validation error: {str(e)}")
356
- return history + [(message, str(e))]
357
-
358
  try:
359
  context = ""
360
- if session_id in user_vectorstores:
361
  vectorstore = user_vectorstores[session_id]
362
  docs = vectorstore.similarity_search(message, k=3)
363
  if docs:
364
  context = "\n\nRelevant information from uploaded PDF:\n" + "\n".join(f"- {doc.page_content}" for doc in docs)
365
 
 
366
  if message.startswith("$") and len(message) > 1 and len(message) <= 6:
367
  ticker = message[1:].upper()
368
- stock_data = get_stock_data(ticker, enable_stock_data)
369
- news = get_financial_news(ticker, use_brave_search, enable_search, model_name)
370
- sentiment = get_market_sentiment(ticker, use_brave_search, enable_search, model_name)
371
- response = f"**Stock Information for {ticker}**\n\n"
372
- if "message" in stock_data:
373
- response += stock_data["message"] + "\n"
374
- else:
375
- 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"
376
- response += f"**Market Sentiment:**\n{sentiment}\n\n**Recent News:**\n"
377
- for i, item in enumerate(news[:3]):
378
- response += f"{i+1}. [{item['title']}]({item['link']})\n {item['snippet'][:100]}...\n"
379
- history.append((message, response))
380
- logging.debug(f"Stock response generated for {ticker}")
381
- return history
 
 
 
 
 
 
 
 
 
382
 
 
383
  if message.lower().startswith("/news "):
384
  topic = message[6:].strip()
385
- news = get_financial_news(topic, use_brave_search, enable_search, model_name)
386
- response = f"**Latest Financial News on {topic}:**\n\n"
387
- for i, item in enumerate(news[:5]):
388
- response += f"{i+1}. **{item['title']}**\n {item['snippet']}\n [Read more]({item['link']})\n\n"
 
 
 
 
 
 
 
 
 
389
  history.append((message, response))
390
- logging.debug(f"News response generated for {topic}")
391
  return history
392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  system_prompt = "You are a financial assistant specializing in analyzing financial reports, statements, and market trends."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  if context:
395
- system_prompt += " Use the following context if relevant: " + context
 
396
  completion = client.chat.completions.create(
397
  model=model_name,
398
- messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": message}],
 
 
 
399
  temperature=0.7,
400
  max_tokens=1024
401
  )
402
  response = completion.choices[0].message.content
403
  history.append((message, response))
404
- logging.debug("Chat response generated")
405
  return history
406
  except Exception as e:
407
- logging.error(f"Error generating response: {str(e)}")
408
- return history + [(message, f"Error generating response: {str(e)}")]
409
 
410
- def update_pdf_viewer(pdf_state: PDFState):
411
- if not pdf_state.total_pages:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  return 0, None, "No PDF uploaded yet"
413
- img_data = base64.b64decode(pdf_state.page_images[0])
414
- img = Image.open(io.BytesIO(img_data))
415
- return pdf_state.total_pages, img, f"**Total Pages:** {pdf_state.total_pages}\n**Total Words:** {pdf_state.total_words}"
 
 
 
 
416
 
417
- def update_image(page_num, pdf_state: PDFState):
418
- if not pdf_state.total_pages or page_num < 1 or page_num > pdf_state.total_pages:
 
 
 
 
 
 
 
419
  return None
420
- img_data = base64.b64decode(pdf_state.page_images[page_num - 1])
421
- return Image.open(io.BytesIO(img_data))
422
 
423
- # Gradio Interface
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  def create_interface():
425
- with gr.Blocks(css=custom_css, js=custom_js) as demo:
426
- gr.HTML(custom_html)
427
  current_session_id = gr.State(None)
428
- pdf_state = gr.State(PDFState(page_images=[], total_pages=0, total_words=0))
429
  current_ticker = gr.State(None)
430
 
431
  gr.HTML("""
@@ -437,114 +739,149 @@ def create_interface():
437
 
438
  with gr.Row(elem_classes="container"):
439
  with gr.Column(scale=1, min_width=300):
440
- pdf_file = gr.File(label="Upload PDF Document", file_types=[".pdf"], type="binary", elem_id="pdf_file")
441
  upload_button = gr.Button("Process PDF", variant="primary")
442
  pdf_status = gr.Markdown("No PDF uploaded yet")
443
 
444
- with gr.Group():
445
- gr.Markdown("### Real-Time Data Settings")
446
- enable_stock_data = gr.Checkbox(label="Enable Real-Time Stock Data", value=True)
447
- enable_search = gr.Checkbox(label="Enable Real-Time Search", value=True)
448
- use_brave_search = gr.Checkbox(label="Use Brave Search (unchecked = Serper)", value=False)
449
-
 
 
 
450
  model_dropdown = gr.Dropdown(
451
  choices=["llama3-70b-8192", "llama3-8b-8192", "mixtral-8x7b-32768", "gemma-7b-it"],
452
  value="llama3-70b-8192",
453
  label="Select Groq Model"
454
  )
455
- theme_button = gr.Button("Toggle Theme")
456
 
 
 
457
  with gr.Group(elem_classes="tool-container"):
458
- ticker_input = gr.Textbox(label="Enter Ticker Symbol (e.g., AAPL)", placeholder="AAPL", elem_id="ticker_input")
459
- period_dropdown = gr.Dropdown(choices=["1mo", "3mo", "6mo", "1y", "2y", "5y", "max"], value="1y", label="Time Period")
460
- analyze_button = gr.Button("Analyze Stock")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
 
462
  with gr.Column(scale=2, min_width=600):
463
  with gr.Tabs():
464
  with gr.TabItem("PDF Viewer"):
465
- page_slider = gr.Slider(minimum=1, maximum=1, step=1, label="Page Number", value=1)
466
- pdf_image = gr.Image(label="PDF Page", type="pil", elem_classes="pdf-viewer-image")
467
- stats_display = gr.Markdown("No PDF uploaded yet", elem_classes="stats-box")
 
468
 
469
  with gr.TabItem("Stock Analysis"):
470
- stock_chart = gr.Plot(label="Stock Price Chart", elem_classes="chart-container")
471
- stock_summary = gr.Markdown("Enter a ticker symbol to see analysis")
 
 
 
 
 
472
 
473
  with gr.Row(elem_classes="container"):
474
- chatbot = gr.Chatbot(height=500, bubble_full_width=False, show_copy_button=True, elem_classes="chat-container")
475
- with gr.Row():
476
- msg = gr.Textbox(show_label=False, placeholder="Ask about your financial document...", scale=5)
477
- send_btn = gr.Button("Send", scale=1)
478
-
 
 
 
 
 
479
  # Event Handlers
480
  upload_button.click(
481
- fn=process_pdf,
482
  inputs=[pdf_file],
483
- outputs=[current_session_id, pdf_status, pdf_state],
484
- _js="showSpinner"
485
  ).then(
486
- fn=update_pdf_viewer,
487
  inputs=[pdf_state],
488
  outputs=[page_slider, pdf_image, stats_display]
489
- ).then(
490
- fn=None,
491
- _js="hideSpinner",
492
- inputs=[],
493
- outputs=[]
494
- )
495
-
496
- analyze_button.click(
497
- fn=analyze_ticker,
498
- inputs=[ticker_input, period_dropdown, use_brave_search, enable_stock_data, enable_search],
499
- outputs=[stock_chart, stock_summary, current_ticker],
500
- _js="showSpinner"
501
- ).then(
502
- fn=None,
503
- _js="hideSpinner",
504
- inputs=[],
505
- outputs=[]
506
  )
507
 
508
  msg.submit(
509
- fn=generate_response,
510
- inputs=[msg, current_session_id, model_dropdown, chatbot, current_ticker, use_brave_search, enable_search, enable_stock_data],
511
  outputs=[chatbot]
512
- ).then(
513
- fn=lambda: "",
514
- inputs=None,
515
- outputs=[msg]
516
- )
517
 
518
  send_btn.click(
519
- fn=generate_response,
520
- inputs=[msg, current_session_id, model_dropdown, chatbot, current_ticker, use_brave_search, enable_search, enable_stock_data],
521
- outputs=[chatbot],
522
- _js="showSpinner"
523
- ).then(
524
- fn=lambda: "",
525
- inputs=None,
526
- outputs=[msg]
527
- ).then(
528
- fn=None,
529
- _js="hideSpinner",
530
- inputs=[],
531
- outputs=[]
532
  )
533
 
534
  page_slider.change(
535
- fn=update_image,
536
  inputs=[page_slider, pdf_state],
537
  outputs=[pdf_image]
538
  )
539
- theme_button.click(
540
- fn=None,
541
- _js="toggleTheme",
542
- inputs=[],
543
- outputs=[]
 
544
  )
545
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
546
  return demo
547
 
 
 
 
 
 
 
 
 
548
  if __name__ == "__main__":
549
  demo = create_interface()
550
- demo.launch(debug=True) # Enable debug mode to see logs in console
 
6
  import io
7
  import json
8
  import re
 
9
  from datetime import datetime, timedelta
10
 
11
  # Third-party imports
 
17
  import fitz # PyMuPDF
18
  from PIL import Image
19
  from dotenv import load_dotenv
 
 
 
20
 
21
  # LangChain imports
22
  from langchain_community.embeddings import HuggingFaceEmbeddings
23
  from langchain_community.vectorstores import FAISS
24
  from langchain.text_splitter import RecursiveCharacterTextSplitter
25
 
 
 
 
26
  # Load environment variables
27
  load_dotenv()
28
  client = groq.Client(api_key=os.getenv("GROQ_LEGAL_API_KEY"))
 
35
  if not os.path.exists(FAISS_INDEX_DIR):
36
  os.makedirs(FAISS_INDEX_DIR)
37
 
38
+ # Dictionary to store user-specific vectorstores
39
  user_vectorstores = {}
40
+
41
+ # Dictionary to store chart data
42
  chart_data_store = {}
43
 
44
+ # Custom CSS for Finance theme with new voice and speech buttons
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  custom_css = """
46
  :root {
 
 
47
  --primary-color: #0C4160;
48
  --secondary-color: #0D6980;
49
  --accent-color: #16A6DB;
50
  --light-color: #EBF5FA;
51
+ --dark-text: #333333;
52
+ --light-text: #F5F5F5;
53
  --border-color: #E5E7EB;
54
  }
55
+ body { background-color: var(--light-color); font-family: 'IBM Plex Sans', sans-serif; }
 
 
 
 
 
 
 
 
 
 
 
56
  .container { max-width: 1200px !important; margin: 0 auto !important; padding: 10px; }
57
  .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); }
58
+ .header-title { color: var(--light-text); font-size: 1.8rem; font-weight: 700; text-align: center; }
59
+ .header-subtitle { color: var(--light-text); opacity: 0.8; font-size: 1rem; text-align: center; margin-top: 5px; }
60
+ .chat-container { border-radius: 12px !important; box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important; background-color: #FFFFFF !important; border: 1px solid var(--border-color) !important; min-height: 500px; }
61
+ .message-user { background-color: var(--accent-color) !important; color: var(--light-text) !important; border-radius: 18px 18px 4px 18px !important; padding: 12px 16px !important; margin-left: auto !important; max-width: 80% !important; }
62
+ .message-bot { background-color: #F5F7FA !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; }
63
+ .input-area { background-color: #FFFFFF !important; border-top: 1px solid var(--border-color) !important; padding: 12px !important; border-radius: 0 0 12px 12px !important; }
64
  .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; }
65
+ .send-btn { background-color: var(--accent-color) !important; border-radius: 24px !important; color: var(--light-text) !important; padding: 10px 20px !important; font-weight: 500 !important; }
66
+ .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; }
67
+ .pdf-viewer-container { border-radius: 12px !important; box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important; background-color: #FFFFFF !important; border: 1px solid var(--border-color) !important; padding: 20px; }
68
  .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); }
69
  .stats-box { background-color: var(--light-color); padding: 10px; border-radius: 8px; margin-top: 10px; }
70
+ .tool-container { background-color: white; border-radius: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); padding: 15px; margin-bottom: 20px; }
71
  .tool-title { font-weight: bold; color: var(--primary-color); margin-bottom: 10px; font-size: 1.1rem; }
72
  .chart-container { height: 400px; width: 100%; border-radius: 8px; overflow: hidden; }
73
  .toggle-container { display: flex; align-items: center; margin-bottom: 15px; }
74
  .toggle-label { margin-right: 10px; font-weight: 500; }
75
  .search-toggle { margin-left: 5px; }
76
+ .audio-controls { display: flex; align-items: center; margin-top: 10px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  """
78
 
79
+ # Function to process PDF files
 
 
 
 
 
 
 
80
  def process_pdf(pdf_file):
 
81
  if pdf_file is None:
82
+ return None, "No file uploaded", {"page_images": [], "total_pages": 0, "total_words": 0}
 
83
  try:
84
  session_id = str(uuid.uuid4())
85
  with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as temp_file:
 
88
 
89
  doc = fitz.open(pdf_path)
90
  texts = [page.get_text() for page in doc]
91
+ page_images = []
92
+ for page in doc:
93
+ pix = page.get_pixmap()
94
+ img_bytes = pix.tobytes("png")
95
+ img_base64 = base64.b64encode(img_bytes).decode("utf-8")
96
+ page_images.append(img_base64)
97
  total_pages = len(doc)
98
  total_words = sum(len(text.split()) for text in texts)
99
  doc.close()
 
106
  user_vectorstores[session_id] = vectorstore
107
 
108
  os.unlink(pdf_path)
109
+ pdf_state = {"page_images": page_images, "total_pages": total_pages, "total_words": total_words}
 
110
  return session_id, f"✅ Successfully processed {len(chunks)} text chunks from your PDF", pdf_state
111
  except Exception as e:
 
112
  if "pdf_path" in locals() and os.path.exists(pdf_path):
113
  os.unlink(pdf_path)
114
+ return None, f"Error processing PDF: {str(e)}", {"page_images": [], "total_pages": 0, "total_words": 0}
115
 
116
+ # Serper API functions for enhanced financial data
117
+ def serper_search(query, search_type="search"):
118
+ """
119
+ Perform a search using Serper.dev API to get financial information
120
+ """
121
  if not SERPER_API_KEY:
122
+ return {"error": "Serper API key not configured. Set SERPER_API_KEY in environment variables."}
123
+
124
  url = "https://google.serper.dev/search"
125
+ payload = json.dumps({
126
+ "q": query,
127
+ "gl": "us",
128
+ "hl": "en",
129
+ "autocorrect": True
130
+ })
131
+ headers = {
132
+ 'X-API-KEY': SERPER_API_KEY,
133
+ 'Content-Type': 'application/json'
134
+ }
135
+
136
  try:
137
+ response = requests.request("POST", url, headers=headers, data=payload)
138
  return response.json()
139
  except Exception as e:
140
+ print(f"Error in Serper search: {e}")
141
  return {"error": str(e)}
142
 
143
+ # Brave Search API functions
144
+ def brave_search(query, search_type="search"):
145
+ """
146
+ Perform a search using Brave Search API to get financial information
147
+ """
148
  if not BRAVE_API_KEY:
149
+ return {"error": "Brave Search API key not configured. Set BRAVE_API_KEY in environment variables."}
150
+
151
  url = "https://api.search.brave.com/res/v1/web/search"
152
+ params = {
153
+ "q": query,
154
+ "count": 10,
155
+ "search_lang": "en",
156
+ "country": "us"
157
+ }
158
+ headers = {
159
+ 'Accept': 'application/json',
160
+ 'Accept-Encoding': 'gzip',
161
+ 'X-Subscription-Token': BRAVE_API_KEY
162
+ }
163
+
164
  try:
165
  response = requests.get(url, params=params, headers=headers)
166
  return response.json()
167
  except Exception as e:
168
+ print(f"Error in Brave search: {e}")
169
  return {"error": str(e)}
170
 
171
+ # Add this new function for LLM-based search
172
+ def llm_search(query, model_name="llama3-8b-8192"):
173
+ """
174
+ Fallback search using LLM when no search APIs are configured
175
+ """
176
+ try:
177
+ system_prompt = """You are a financial research assistant. Based on your knowledge,
178
+ provide relevant information about the query. Format your response as a list of 3-5
179
+ relevant pieces of information, each with a title and brief description."""
180
+
181
+ completion = client.chat.completions.create(
182
+ model=model_name,
183
+ messages=[
184
+ {"role": "system", "content": system_prompt},
185
+ {"role": "user", "content": query}
186
+ ],
187
+ temperature=0.3,
188
+ max_tokens=500
189
+ )
190
+
191
+ # Format response as search results
192
+ return [{
193
+ "title": "LLM-Generated Results",
194
+ "link": "",
195
+ "snippet": completion.choices[0].message.content,
196
+ "source": "AI Knowledge Base"
197
+ }]
198
+ except Exception as e:
199
+ print(f"Error in LLM search: {e}")
200
+ return []
201
+
202
+ # Update the get_financial_news function
203
+ def get_financial_news(ticker, use_brave_search=False, model_name="llama3-8b-8192"):
204
+ """
205
+ Get latest financial news about a stock using selected search API or LLM fallback
206
+ """
207
  query = f"{ticker} stock news financial analysis latest"
208
+ news_items = []
209
+
210
+ # Try Brave Search first if selected
211
  if use_brave_search and BRAVE_API_KEY:
212
  results = brave_search(query)
213
  if "web" in results and "results" in results["web"]:
214
+ for item in results["web"]["results"][:5]:
215
+ news_items.append({
216
+ "title": item.get("title", ""),
217
+ "link": item.get("url", ""),
218
+ "snippet": item.get("description", ""),
219
+ "source": item.get("source", "")
220
+ })
221
+ return news_items
222
+
223
+ # Try Serper API if Brave Search is not used or failed
224
+ if not news_items and SERPER_API_KEY:
225
  results = serper_search(query)
226
  if "organic" in results:
227
+ for item in results["organic"][:5]:
228
+ news_items.append({
229
+ "title": item.get("title", ""),
230
+ "link": item.get("link", ""),
231
+ "snippet": item.get("snippet", ""),
232
+ "source": item.get("source", "")
233
+ })
234
+ return news_items
235
+
236
+ # Fallback to LLM if no API results
237
+ if not news_items:
238
+ return llm_search(f"Provide recent financial news and analysis about {ticker} stock", model_name)
239
+
240
+ # Update the get_market_sentiment function
241
+ def get_market_sentiment(ticker, use_brave_search=False, model_name="llama3-8b-8192"):
242
+ """
243
+ Get market sentiment for a stock using selected search API or LLM fallback
244
+ """
245
  query = f"{ticker} stock market sentiment analysis"
246
  snippets = []
247
+
248
+ # Try Brave Search first if selected
249
  if use_brave_search and BRAVE_API_KEY:
250
  results = brave_search(query)
251
  if "web" in results and "results" in results["web"]:
252
+ for item in results["web"]["results"][:3]:
253
+ if "description" in item:
254
+ snippets.append(item["description"])
255
+
256
+ # Try Serper API if Brave Search is not used or failed
257
+ if not snippets and SERPER_API_KEY:
258
  results = serper_search(query)
259
  if "organic" in results:
260
+ for item in results["organic"][:3]:
261
+ if "snippet" in item:
262
+ snippets.append(item["snippet"])
263
+
264
+ # Generate sentiment analysis
265
  if snippets:
266
+ combined_snippets = "\n".join(snippets)
267
+ else:
268
+ # If no API results, use LLM to generate market sentiment directly
269
+ system_prompt = f"""You are a financial analyst. Based on your knowledge,
270
+ provide a brief market sentiment analysis for {ticker} stock. Consider recent
271
+ trends, company performance, and market conditions."""
272
+
273
  try:
274
  completion = client.chat.completions.create(
275
+ model=model_name,
276
  messages=[
277
+ {"role": "system", "content": system_prompt},
278
+ {"role": "user", "content": f"What is the current market sentiment for {ticker} stock?"}
279
  ],
280
  temperature=0.2,
281
  max_tokens=150
282
  )
283
  return completion.choices[0].message.content
284
  except Exception as e:
285
+ print(f"Error in LLM sentiment analysis: {e}")
286
  return "Unable to determine sentiment"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
288
+ # If we have API snippets, analyze them
289
  try:
290
+ completion = client.chat.completions.create(
291
+ model=model_name,
292
+ messages=[
293
+ {"role": "system", "content": "You are a financial sentiment analyzer. Based on the text provided, determine if the market sentiment for the stock is positive, negative, or neutral. Provide a brief explanation."},
294
+ {"role": "user", "content": combined_snippets}
295
+ ],
296
+ temperature=0.2,
297
+ max_tokens=150
298
+ )
299
+ return completion.choices[0].message.content
 
 
 
 
 
300
  except Exception as e:
301
+ print(f"Error analyzing sentiment: {e}")
302
+ return "Unable to determine sentiment"
303
 
304
+ # Function to generate chatbot responses with Finance theme
305
+ def generate_response(message, session_id, model_name, history, current_ticker=None, use_brave_search=False):
306
  if not message:
307
  return history
 
 
 
 
 
 
 
308
  try:
309
  context = ""
310
+ if session_id and session_id in user_vectorstores:
311
  vectorstore = user_vectorstores[session_id]
312
  docs = vectorstore.similarity_search(message, k=3)
313
  if docs:
314
  context = "\n\nRelevant information from uploaded PDF:\n" + "\n".join(f"- {doc.page_content}" for doc in docs)
315
 
316
+ # Check if it's a stock ticker query
317
  if message.startswith("$") and len(message) > 1 and len(message) <= 6:
318
  ticker = message[1:].upper()
319
+ try:
320
+ stock_data = get_stock_data(ticker)
321
+ news = get_financial_news(ticker, use_brave_search)
322
+ sentiment = get_market_sentiment(ticker, use_brave_search)
323
+
324
+ response = f"**Stock Information for {ticker}**\n\n"
325
+ response += f"Current Price: ${stock_data['current_price']}\n"
326
+ response += f"52-Week High: ${stock_data['52wk_high']}\n"
327
+ response += f"Market Cap: ${stock_data['market_cap']:,}\n"
328
+ response += f"P/E Ratio: {stock_data['pe_ratio']}\n\n"
329
+ response += f"**Market Sentiment:**\n{sentiment}\n\n"
330
+ response += "**Recent News:**\n"
331
+
332
+ for i, news_item in enumerate(news[:3]):
333
+ response += f"{i+1}. [{news_item['title']}]({news_item['link']})\n"
334
+ response += f" {news_item['snippet'][:100]}...\n\n"
335
+
336
+ response += f"More data available in the Stock Analysis tab."
337
+ history.append((message, response))
338
+ return history
339
+ except Exception as e:
340
+ history.append((message, f"Error retrieving stock data for {ticker}: {str(e)}"))
341
+ return history
342
 
343
+ # Check if it's a news search request
344
  if message.lower().startswith("/news "):
345
  topic = message[6:].strip()
346
+ news = get_financial_news(topic, use_brave_search)
347
+
348
+ if news:
349
+ search_provider = "Brave Search" if use_brave_search else "Serper"
350
+ response = f"**Latest Financial News on {topic} (via {search_provider}):**\n\n"
351
+ for i, news_item in enumerate(news[:5]):
352
+ response += f"{i+1}. **{news_item['title']}**\n"
353
+ response += f" Source: {news_item['source']}\n"
354
+ response += f" {news_item['snippet']}\n"
355
+ response += f" [Read more]({news_item['link']})\n\n"
356
+ else:
357
+ response = f"No recent news found for {topic}."
358
+
359
  history.append((message, response))
 
360
  return history
361
 
362
+ # Check if it's a chart analysis request
363
+ if message.lower() == "/chart" or message.lower().startswith("/analyze chart"):
364
+ if current_ticker and current_ticker in chart_data_store:
365
+ chart_context = generate_chart_context(current_ticker)
366
+
367
+ # Get additional market analysis using selected search API
368
+ market_context = ""
369
+ try:
370
+ news = get_financial_news(current_ticker, use_brave_search)
371
+ sentiment = get_market_sentiment(current_ticker, use_brave_search)
372
+ market_context = f"\n\nMarket Sentiment: {sentiment}\n\nRecent News Context:"
373
+ for item in news[:2]:
374
+ market_context += f"\n- {item['title']}: {item['snippet'][:150]}..."
375
+ except Exception as e:
376
+ print(f"Error getting additional market context: {e}")
377
+
378
+ system_prompt = "You are a financial analyst specializing in stock market analysis. You have been provided with chart and financial data for a stock, along with recent market sentiment and news. Analyze this data and provide insights about the stock's performance trends, potential support/resistance levels, and overall pattern."
379
+ completion = client.chat.completions.create(
380
+ model=model_name,
381
+ messages=[
382
+ {"role": "system", "content": system_prompt},
383
+ {"role": "user", "content": f"Analyze this stock data and chart information:\n\n{chart_context}{market_context}"}
384
+ ],
385
+ temperature=0.7,
386
+ max_tokens=1024
387
+ )
388
+ response = completion.choices[0].message.content
389
+ history.append((message, response))
390
+ return history
391
+ else:
392
+ history.append((message, "Please analyze a stock first using the Stock Analysis tab before requesting chart analysis."))
393
+ return history
394
+
395
  system_prompt = "You are a financial assistant specializing in analyzing financial reports, statements, and market trends."
396
+ system_prompt += " You can help with stock market information, financial terminology, ratio analysis, and investment concepts."
397
+
398
+ # Add chart context if available
399
+ if current_ticker and current_ticker in chart_data_store and ("chart" in message.lower() or "stock" in message.lower() or current_ticker.lower() in message.lower()):
400
+ chart_context = generate_chart_context(current_ticker)
401
+ context += f"\n\nRecent stock data for {current_ticker}:\n{chart_context}"
402
+
403
+ # Add news and sentiment if it's a stock-related query
404
+ try:
405
+ news = get_financial_news(current_ticker, use_brave_search)
406
+ sentiment = get_market_sentiment(current_ticker, use_brave_search)
407
+ context += f"\n\nMarket Sentiment: {sentiment}\n\nRecent News Headlines:"
408
+ for item in news[:2]:
409
+ context += f"\n- {item['title']}"
410
+ except Exception as e:
411
+ print(f"Error adding news context: {e}")
412
+
413
  if context:
414
+ system_prompt += " Use the following context to answer the question if relevant: " + context
415
+
416
  completion = client.chat.completions.create(
417
  model=model_name,
418
+ messages=[
419
+ {"role": "system", "content": system_prompt},
420
+ {"role": "user", "content": message}
421
+ ],
422
  temperature=0.7,
423
  max_tokens=1024
424
  )
425
  response = completion.choices[0].message.content
426
  history.append((message, response))
 
427
  return history
428
  except Exception as e:
429
+ history.append((message, f"Error generating response: {str(e)}"))
430
+ return history
431
 
432
+ # Helper function to generate chart context for LLM
433
+ def generate_chart_context(ticker):
434
+ data = chart_data_store[ticker]
435
+ df = data["history"]
436
+ stats = data["stats"]
437
+
438
+ # Calculate key metrics from the chart data
439
+ start_price = df["Close"].iloc[0]
440
+ end_price = df["Close"].iloc[-1]
441
+ percent_change = ((end_price - start_price) / start_price) * 100
442
+ highest = df["High"].max()
443
+ lowest = df["Low"].min()
444
+
445
+ # Calculate average volume
446
+ avg_volume = df["Volume"].mean()
447
+
448
+ # Calculate simple moving averages
449
+ if len(df) > 50:
450
+ sma_50 = df["Close"].rolling(window=50).mean().iloc[-1]
451
+ else:
452
+ sma_50 = "Not enough data"
453
+
454
+ if len(df) > 200:
455
+ sma_200 = df["Close"].rolling(window=200).mean().iloc[-1]
456
+ else:
457
+ sma_200 = "Not enough data"
458
+
459
+ # Calculate RSI (Relative Strength Index)
460
+ delta = df['Close'].diff()
461
+ gain = delta.where(delta > 0, 0).rolling(window=14).mean()
462
+ loss = -delta.where(delta < 0, 0).rolling(window=14).mean()
463
+ rs = gain / loss
464
+ rsi = 100 - (100 / (1 + rs.iloc[-1])) if not pd.isna(rs.iloc[-1]) and loss.iloc[-1] != 0 else 50
465
+
466
+ # Calculate volatility (standard deviation of returns)
467
+ returns = df['Close'].pct_change()
468
+ volatility = returns.std() * 100 # Annualize by multiplying by sqrt(252)
469
+
470
+ # Get recent price movement (last 5 days)
471
+ recent_prices = []
472
+ if len(df) >= 5:
473
+ for i in range(1, 6):
474
+ if i <= len(df):
475
+ recent_prices.append(df["Close"].iloc[-i])
476
+
477
+ # Format the context for the LLM
478
+ context = f"""
479
+ Ticker: {ticker}
480
+ Period: {data["period"]}
481
+ Current Price: ${end_price:.2f}
482
+ Price Change: {percent_change:.2f}%
483
+ 52-Week High: ${stats['52wk_high']}
484
+ 52-Week Low: ${lowest:.2f}
485
+ Market Cap: ${stats['market_cap']:,}
486
+ P/E Ratio: {stats['pe_ratio']}
487
+ Average Volume: {avg_volume:.0f}
488
+ Volatility: {volatility:.2f}%
489
+ RSI (14-day): {rsi:.2f}
490
+ """
491
+
492
+ if isinstance(sma_50, float):
493
+ context += f"50-day Moving Average: ${sma_50:.2f}\n"
494
+ if isinstance(sma_200, float):
495
+ context += f"200-day Moving Average: ${sma_200:.2f}\n"
496
+
497
+ context += "\nRecent Price Movement (last 5 days, most recent first):\n"
498
+ for i, price in enumerate(recent_prices):
499
+ context += f"Day {i+1}: ${price:.2f}\n"
500
+
501
+ return context
502
+
503
+ # Functions to update PDF viewer (unchanged)
504
+ def update_pdf_viewer(pdf_state):
505
+ if not pdf_state["total_pages"]:
506
  return 0, None, "No PDF uploaded yet"
507
+ try:
508
+ img_data = base64.b64decode(pdf_state["page_images"][0])
509
+ img = Image.open(io.BytesIO(img_data))
510
+ return pdf_state["total_pages"], img, f"**Total Pages:** {pdf_state['total_pages']}\n**Total Words:** {pdf_state['total_words']}"
511
+ except Exception as e:
512
+ print(f"Error decoding image: {e}")
513
+ return 0, None, "Error displaying PDF"
514
 
515
+ def update_image(page_num, pdf_state):
516
+ if not pdf_state["total_pages"] or page_num < 1 or page_num > pdf_state["total_pages"]:
517
+ return None
518
+ try:
519
+ img_data = base64.b64decode(pdf_state["page_images"][page_num - 1])
520
+ img = Image.open(io.BytesIO(img_data))
521
+ return img
522
+ except Exception as e:
523
+ print(f"Error decoding image: {e}")
524
  return None
 
 
525
 
526
+ # New Finance-specific tools
527
+ def get_stock_data(ticker):
528
+ """Tool to fetch latest stock data for a given ticker"""
529
+ try:
530
+ stock = yf.Ticker(ticker)
531
+ info = stock.info
532
+ return {
533
+ "current_price": info.get("currentPrice", info.get("regularMarketPrice", "N/A")),
534
+ "52wk_high": info.get("fiftyTwoWeekHigh", "N/A"),
535
+ "market_cap": info.get("marketCap", "N/A"),
536
+ "pe_ratio": info.get("trailingPE", "N/A"),
537
+ "dividend_yield": info.get("dividendYield", "N/A"),
538
+ "beta": info.get("beta", "N/A"),
539
+ "average_volume": info.get("averageVolume", "N/A")
540
+ }
541
+ except Exception as e:
542
+ print(f"Error fetching stock data: {e}")
543
+ raise e
544
+
545
+ def get_stock_history(ticker, period="1y"):
546
+ """Get historical data for charting"""
547
+ try:
548
+ stock = yf.Ticker(ticker)
549
+ hist = stock.history(period=period)
550
+ return hist
551
+ except Exception as e:
552
+ print(f"Error fetching stock history: {e}")
553
+ return pd.DataFrame()
554
+
555
+ def get_fred_data(indicator):
556
+ """Get economic data from FRED API"""
557
+ api_key = os.getenv("FRED_API_KEY", "")
558
+ if not api_key:
559
+ return "FRED API key not configured"
560
+
561
+ base_url = "https://api.stlouisfed.org/fred/series/observations"
562
+ params = {
563
+ "series_id": indicator,
564
+ "api_key": api_key,
565
+ "file_type": "json",
566
+ "sort_order": "desc",
567
+ "limit": 100
568
+ }
569
+
570
+ try:
571
+ response = requests.get(base_url, params=params)
572
+ data = response.json()
573
+ return data.get("observations", [])
574
+ except Exception as e:
575
+ print(f"Error fetching FRED data: {e}")
576
+ return []
577
+
578
+ def create_stock_chart(ticker, period="1y"):
579
+ """Create an interactive stock chart using Plotly"""
580
+ try:
581
+ df = get_stock_history(ticker, period)
582
+ if df.empty:
583
+ return None
584
+
585
+ fig = go.Figure()
586
+
587
+ # Add candlestick chart
588
+ fig.add_trace(
589
+ go.Candlestick(
590
+ x=df.index,
591
+ open=df['Open'],
592
+ high=df['High'],
593
+ low=df['Low'],
594
+ close=df['Close'],
595
+ name=ticker
596
+ )
597
+ )
598
+
599
+ # Add volume as bar chart on secondary y-axis
600
+ fig.add_trace(
601
+ go.Bar(
602
+ x=df.index,
603
+ y=df['Volume'],
604
+ name='Volume',
605
+ marker_color='rgba(0, 128, 0, 0.3)',
606
+ yaxis='y2'
607
+ )
608
+ )
609
+
610
+ # Update layout for dual y-axis
611
+ fig.update_layout(
612
+ title=f'{ticker} Stock Price',
613
+ yaxis_title='Price (USD)',
614
+ xaxis_title='Date',
615
+ template='plotly_white',
616
+ yaxis=dict(
617
+ domain=[0.3, 1.0]
618
+ ),
619
+ yaxis2=dict(
620
+ domain=[0, 0.2],
621
+ title='Volume'
622
+ ),
623
+ legend=dict(
624
+ orientation="h",
625
+ yanchor="bottom",
626
+ y=1.02,
627
+ xanchor="right",
628
+ x=1
629
+ ),
630
+ height=500
631
+ )
632
+
633
+ return fig
634
+ except Exception as e:
635
+ print(f"Error creating stock chart: {e}")
636
+ return None
637
+
638
+ def analyze_ticker(ticker_input, period, use_brave_search=False):
639
+ """Process the ticker input and return analysis"""
640
+ if not ticker_input:
641
+ return None, "Please enter a valid ticker symbol", None
642
+
643
+ ticker = ticker_input.strip().upper()
644
+ if ticker.startswith("$"):
645
+ ticker = ticker[1:]
646
+
647
+ try:
648
+ stock_data = get_stock_data(ticker)
649
+ stock_history = get_stock_history(ticker, period)
650
+ chart = create_stock_chart(ticker, period)
651
+
652
+ # Store chart data for LLM analysis
653
+ chart_data_store[ticker] = {
654
+ "history": stock_history,
655
+ "stats": stock_data,
656
+ "period": period
657
+ }
658
+
659
+ # Get market sentiment using selected search API or LLM fallback
660
+ try:
661
+ sentiment = get_market_sentiment(ticker, use_brave_search)
662
+ sentiment_summary = f"\n\n**Market Sentiment:**\n{sentiment}"
663
+ except Exception as e:
664
+ print(f"Error getting sentiment: {e}")
665
+ sentiment_summary = ""
666
+
667
+ # Create a formatted summary
668
+ search_provider = "Brave Search" if (use_brave_search and BRAVE_API_KEY) else "Serper" if SERPER_API_KEY else "AI Knowledge Base"
669
+ summary = f"""
670
+ ### {ticker} Analysis (Using {search_provider})
671
+
672
+ **Current Price:** ${stock_data['current_price']}
673
+ **52-Week High:** ${stock_data['52wk_high']}
674
+ **Market Cap:** ${stock_data['market_cap']:,}
675
+ **P/E Ratio:** {stock_data['pe_ratio']}
676
+ **Dividend Yield:** {stock_data['dividend_yield'] * 100 if stock_data['dividend_yield'] != 'N/A' else 'N/A'}%
677
+ **Beta:** {stock_data['beta']}
678
+ **Avg Volume:** {stock_data['average_volume']:,}
679
+ {sentiment_summary}
680
+
681
+ For in-depth analysis of this chart, ask the chatbot by typing "/chart" or "/analyze chart".
682
+ For latest news, type "/news {ticker}".
683
+ """
684
+
685
+ return chart, summary, ticker
686
+ except Exception as e:
687
+ return None, f"Error analyzing ticker {ticker}: {str(e)}", None
688
+
689
+ # Replace the load_docling_model function with a simpler image analysis function
690
+ def analyze_image(image_file):
691
+ """
692
+ Basic image analysis function that doesn't rely on external models
693
+ """
694
+ if image_file is None:
695
+ return "No image uploaded. Please upload an image to analyze."
696
+
697
+ try:
698
+ image = Image.open(image_file)
699
+ width, height = image.size
700
+ format = image.format
701
+ mode = image.mode
702
+
703
+ analysis = f"""## Technical Document Analysis
704
+
705
+ **Image Properties:**
706
+ - Dimensions: {width}x{height} pixels
707
+ - Format: {format}
708
+ - Color Mode: {mode}
709
+
710
+ **Technical Analysis:**
711
+ 1. Document Quality:
712
+ - Resolution: {'High' if width > 2000 or height > 2000 else 'Medium' if width > 1000 or height > 1000 else 'Low'}
713
+ - Color Depth: {mode}
714
+
715
+ 2. Recommendations:
716
+ - For text extraction, consider using PDF format
717
+ - For technical diagrams, ensure high resolution
718
+ - Consider OCR for text content
719
+
720
+ **Note:** For detailed technical analysis, please convert to PDF format
721
+ """
722
+ return analysis
723
+ except Exception as e:
724
+ return f"Error analyzing image: {str(e)}\n\nPlease try using PDF format instead."
725
+
726
+ # Update the Gradio interface
727
  def create_interface():
728
+ with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
 
729
  current_session_id = gr.State(None)
730
+ pdf_state = gr.State({"page_images": [], "total_pages": 0, "total_words": 0})
731
  current_ticker = gr.State(None)
732
 
733
  gr.HTML("""
 
739
 
740
  with gr.Row(elem_classes="container"):
741
  with gr.Column(scale=1, min_width=300):
742
+ pdf_file = gr.File(label="Upload PDF Document", file_types=[".pdf"], type="binary")
743
  upload_button = gr.Button("Process PDF", variant="primary")
744
  pdf_status = gr.Markdown("No PDF uploaded yet")
745
 
746
+ # Search Engine Toggle
747
+ with gr.Row(elem_classes="toggle-container"):
748
+ gr.Markdown("Search Provider:", elem_classes="toggle-label")
749
+ use_brave_search = gr.Checkbox(
750
+ label="Use Brave Search (unchecked = Serper)",
751
+ value=False,
752
+ elem_classes="search-toggle"
753
+ )
754
+
755
  model_dropdown = gr.Dropdown(
756
  choices=["llama3-70b-8192", "llama3-8b-8192", "mixtral-8x7b-32768", "gemma-7b-it"],
757
  value="llama3-70b-8192",
758
  label="Select Groq Model"
759
  )
 
760
 
761
+ # Finance Tools Section
762
+ gr.Markdown("### Financial Tools", elem_classes="tool-title")
763
  with gr.Group(elem_classes="tool-container"):
764
+ with gr.Tabs():
765
+ with gr.TabItem("Stock Analysis"):
766
+ ticker_input = gr.Textbox(label="Enter Ticker Symbol (e.g., AAPL)", placeholder="AAPL")
767
+ period_dropdown = gr.Dropdown(
768
+ choices=["1mo", "3mo", "6mo", "1y", "2y", "5y", "max"],
769
+ value="1y",
770
+ label="Time Period"
771
+ )
772
+ analyze_button = gr.Button("Analyze Stock")
773
+
774
+ with gr.TabItem("Image Analysis"):
775
+ gr.Markdown("""
776
+ ### Basic Image Analysis
777
+ Upload an image to see basic properties and recommendations.
778
+ For detailed document analysis, please use PDF format.
779
+ """)
780
+ image_input = gr.File(
781
+ label="Upload Document Image",
782
+ file_types=["image"],
783
+ type="filepath"
784
+ )
785
+ analyze_btn = gr.Button("Analyze Image")
786
 
787
  with gr.Column(scale=2, min_width=600):
788
  with gr.Tabs():
789
  with gr.TabItem("PDF Viewer"):
790
+ with gr.Column(elem_classes="pdf-viewer-container"):
791
+ page_slider = gr.Slider(minimum=1, maximum=1, step=1, label="Page Number", value=1)
792
+ pdf_image = gr.Image(label="PDF Page", type="pil", elem_classes="pdf-viewer-image")
793
+ stats_display = gr.Markdown("No PDF uploaded yet", elem_classes="stats-box")
794
 
795
  with gr.TabItem("Stock Analysis"):
796
+ with gr.Column(elem_classes="pdf-viewer-container"):
797
+ stock_chart = gr.Plot(label="Stock Price Chart", elem_classes="chart-container")
798
+ stock_summary = gr.Markdown("Enter a ticker symbol to see analysis")
799
+
800
+ with gr.TabItem("Image Analysis Results"):
801
+ image_analysis_results = gr.Markdown("Upload an image and click 'Analyze Image' to see analysis results")
802
+ image_preview = gr.Image(label="Image Preview", type="pil")
803
 
804
  with gr.Row(elem_classes="container"):
805
+ with gr.Column(scale=2, min_width=600):
806
+ chatbot = gr.Chatbot(height=500, bubble_full_width=False, show_copy_button=True, elem_classes="chat-container")
807
+ with gr.Row():
808
+ msg = gr.Textbox(
809
+ show_label=False,
810
+ placeholder="Ask about your financial document or click the microphone icon to speak...",
811
+ scale=5
812
+ )
813
+ send_btn = gr.Button("Send", scale=1)
814
+
815
  # Event Handlers
816
  upload_button.click(
817
+ process_pdf,
818
  inputs=[pdf_file],
819
+ outputs=[current_session_id, pdf_status, pdf_state]
 
820
  ).then(
821
+ update_pdf_viewer,
822
  inputs=[pdf_state],
823
  outputs=[page_slider, pdf_image, stats_display]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
824
  )
825
 
826
  msg.submit(
827
+ generate_response,
828
+ inputs=[msg, current_session_id, model_dropdown, chatbot, current_ticker, use_brave_search],
829
  outputs=[chatbot]
830
+ ).then(lambda: "", None, [msg])
 
 
 
 
831
 
832
  send_btn.click(
833
+ generate_response,
834
+ inputs=[msg, current_session_id, model_dropdown, chatbot, current_ticker, use_brave_search],
835
+ outputs=[chatbot]
836
+ ).then(lambda: "", None, [msg])
837
+
838
+ # Update display when search provider changes
839
+ use_brave_search.change(
840
+ lambda x: f"Using {'Brave Search' if x else 'Serper'} API for queries",
841
+ inputs=[use_brave_search],
842
+ outputs=[pdf_status]
 
 
 
843
  )
844
 
845
  page_slider.change(
846
+ update_image,
847
  inputs=[page_slider, pdf_state],
848
  outputs=[pdf_image]
849
  )
850
+
851
+ # Stock analysis handler
852
+ analyze_button.click(
853
+ analyze_ticker,
854
+ inputs=[ticker_input, period_dropdown, use_brave_search],
855
+ outputs=[stock_chart, stock_summary, current_ticker]
856
  )
857
+
858
+ # Image analysis button handler
859
+ analyze_btn.click(
860
+ analyze_image,
861
+ inputs=[image_input],
862
+ outputs=[image_analysis_results]
863
+ ).then(
864
+ lambda x: Image.open(x) if x else None,
865
+ inputs=[image_input],
866
+ outputs=[image_preview]
867
+ )
868
+
869
+ clear_btn.click(
870
+ lambda: ([], None, "No PDF uploaded yet", {"page_images": [], "total_pages": 0, "total_words": 0}, 0, None, "No PDF uploaded yet"),
871
+ None,
872
+ [chatbot, current_session_id, pdf_status, pdf_state, page_slider, pdf_image, stats_display]
873
+ )
874
+
875
  return demo
876
 
877
+ # Add footer with attribution
878
+ gr.HTML("""
879
+ <div style="text-align: center; margin-top: 20px; padding: 10px; color: #666; font-size: 0.8rem; border-top: 1px solid #eee;">
880
+ Created by Calvin Allen Crawford
881
+ </div>
882
+ """)
883
+
884
+ # Launch the app
885
  if __name__ == "__main__":
886
  demo = create_interface()
887
+ demo.launch()