AnilNiraula commited on
Commit
caedbee
·
verified ·
1 Parent(s): a0265bc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +370 -0
app.py CHANGED
@@ -1,3 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  if not history:
2
  logger.warning("History is empty, initializing with user message.")
3
  history = [{"role": "user", "content": ""}]
 
1
+ import os
2
+ import sys
3
+ import subprocess
4
+ import re
5
+ import multiprocessing
6
+ from collections.abc import Iterator
7
+ import gradio as gr
8
+ from huggingface_hub import hf_hub_download, login
9
+ import logging
10
+
11
+ # Set up logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Install llama-cpp-python if not present
16
+ try:
17
+ from llama_cpp import Llama
18
+ except ModuleNotFoundError:
19
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "llama-cpp-python"])
20
+ from llama_cpp import Llama
21
+
22
+ # Install yfinance if not present (for CAGR calculations)
23
+ try:
24
+ import yfinance as yf
25
+ except ModuleNotFoundError:
26
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "yfinance"])
27
+ import yfinance as yf
28
+
29
+ # Import pandas for handling DataFrame column structures
30
+ import pandas as pd
31
+
32
+ # Additional imports for visualization and file handling
33
+ try:
34
+ import matplotlib.pyplot as plt
35
+ from PIL import Image
36
+ import io
37
+ except ModuleNotFoundError:
38
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "matplotlib", "pillow"])
39
+
40
+ import matplotlib.pyplot as plt
41
+ from PIL import Image
42
+ import io
43
+
44
+ # Additional imports for PEFT fine-tuning
45
+ try:
46
+ import torch
47
+ from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, TrainingArguments
48
+ from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
49
+ from trl import SFTTrainer
50
+ from datasets import load_dataset
51
+ import accelerate
52
+ except ModuleNotFoundError:
53
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "torch", "transformers", "peft", "trl", "datasets", "accelerate", "bitsandbytes"])
54
+
55
+ import torch
56
+ from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, TrainingArguments
57
+ from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
58
+ from trl import SFTTrainer
59
+ from datasets import load_dataset
60
+
61
+ MAX_MAX_NEW_TOKENS = 512
62
+ DEFAULT_MAX_NEW_TOKENS = 128
63
+ MAX_INPUT_TOKEN_LENGTH = int(os.getenv("MAX_INPUT_TOKEN_LENGTH", "256"))
64
+
65
+ DESCRIPTION = """\
66
+ # FinChat: Investing Q&A (CPU-Only, Ultra-Fast Optimization)
67
+ This application delivers an interactive chat interface powered by a highly efficient, small AI model adapted for addressing investing and finance inquiries through specialized prompt engineering. It ensures rapid, reasoned responses to user queries. Duplicate this Space for customization or queue-free deployment.
68
+ <p>Running on CPU 🥶 Inference is heavily optimized for responses in under 10 seconds for simple queries, with output limited to 128 tokens maximum. For longer responses, increase 'Max New Tokens' in Advanced Settings. Brief delays may occur in free-tier environments due to shared resources, but typical generation speeds reach 20-40 tokens per second. CAGR calculations for stocks are computed accurately using historical data.</p>
69
+ """
70
+
71
+ LICENSE = """\
72
+ <p/>
73
+ ---
74
+ This application employs the Llama-2-7B-Chat model, fine-tuned on financial Q&A data, governed by Meta AI's Terms of Use. Refer to the [model card](https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGUF) for details.
75
+ """
76
+
77
+ # Define paths
78
+ base_model_id = "meta-llama/Llama-2-7b-chat-hf"
79
+ fine_tuned_model_path = "fine_tuned_llama2.gguf"
80
+ quantized_model_path = "llama-2-7b-chat-finetuned.Q4_K_M.gguf"
81
+ lora_adapter_path = "lora_adapter"
82
+
83
+ # Hugging Face login (required for fine-tuning)
84
+ hf_token = os.getenv("HF_TOKEN")
85
+ if hf_token:
86
+ login(hf_token)
87
+ else:
88
+ logger.warning("HF_TOKEN not set. Fine-tuning may fail if authentication is required.")
89
+
90
+ # One-time fine-tuning process if the fine-tuned GGUF does not exist
91
+ if not os.path.exists(quantized_model_path):
92
+ logger.info("Attempting one-time PEFT fine-tuning on Finance-Alpaca dataset...")
93
+ try:
94
+ tokenizer = AutoTokenizer.from_pretrained(base_model_id)
95
+ model = AutoModelForCausalLM.from_pretrained(
96
+ base_model_id,
97
+ torch_dtype=torch.bfloat16,
98
+ device_map="cpu"
99
+ )
100
+ dataset = load_dataset("gbharti/finance-alpaca", split="train[0:500]")
101
+
102
+ def formatting_func(example):
103
+ text = f"<s>[INST] {example['instruction']}\n{example['input']} [/INST] {example['output']} </s>"
104
+ return {"text": text}
105
+
106
+ dataset = dataset.map(formatting_func)
107
+
108
+ lora_config = LoraConfig(
109
+ r=8,
110
+ lora_alpha=16,
111
+ target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
112
+ lora_dropout=0.05,
113
+ bias="none",
114
+ task_type="CAUSAL_LM"
115
+ )
116
+
117
+ model = prepare_model_for_kbit_training(model)
118
+ model = get_peft_model(model, lora_config)
119
+
120
+ training_args = TrainingArguments(
121
+ output_dir=lora_adapter_path,
122
+ num_train_epochs=1,
123
+ per_device_train_batch_size=1,
124
+ gradient_accumulation_steps=4,
125
+ learning_rate=2e-4,
126
+ fp16=False,
127
+ save_steps=100,
128
+ logging_steps=10,
129
+ optim="adamw_torch",
130
+ report_to="none"
131
+ )
132
+
133
+ trainer = SFTTrainer(
134
+ model=model,
135
+ tokenizer=tokenizer,
136
+ train_dataset=dataset,
137
+ dataset_text_field="text",
138
+ max_seq_length=512,
139
+ args=training_args
140
+ )
141
+
142
+ trainer.train()
143
+
144
+ model = model.merge_and_unload()
145
+ model.save_pretrained("merged_model")
146
+ tokenizer.save_pretrained("merged_model")
147
+
148
+ subprocess.check_call(["git", "clone", "https://github.com/ggerganov/llama.cpp"])
149
+ os.chdir("llama.cpp")
150
+ subprocess.check_call(["make"])
151
+ subprocess.check_call([sys.executable, "convert_hf_to_gguf.py", "--outfile", "../" + fine_tuned_model_path, "--outtype", "f16", "../merged_model"])
152
+ subprocess.check_call(["./quantize", "../" + fine_tuned_model_path, "../" + quantized_model_path, "Q4_K_M"])
153
+ os.chdir("..")
154
+
155
+ logger.info("Fine-tuning and conversion complete. Using fine-tuned model.")
156
+ except Exception as e:
157
+ logger.error(f"Error during fine-tuning: {str(e)}")
158
+ print("Falling back to the pre-trained model without fine-tuning.")
159
+
160
+ # Load the model
161
+ try:
162
+ model_path = quantized_model_path if os.path.exists(quantized_model_path) else hf_hub_download(
163
+ repo_id="TheBloke/Llama-2-7B-Chat-GGUF",
164
+ filename="llama-2-7b-chat.Q4_K_M.gguf"
165
+ )
166
+ llm = Llama(
167
+ model_path=model_path,
168
+ n_ctx=256,
169
+ n_batch=512,
170
+ n_threads=multiprocessing.cpu_count(),
171
+ n_gpu_layers=0,
172
+ chat_format="llama-2"
173
+ )
174
+ logger.info("Model loaded successfully.")
175
+ except Exception as e:
176
+ logger.error(f"Error loading model: {str(e)}")
177
+ raise
178
+
179
+ DEFAULT_SYSTEM_PROMPT = """You are FinChat, a knowledgeable AI assistant specializing in investing and finance. Provide accurate, helpful, reasoned, and concise answers to investing questions. Always base responses on reliable information and advise users to consult professionals for personalized advice.
180
+ Always respond exclusively in English. Use bullet points for clarity.
181
+ Example:
182
+ User: average return for TSLA between 2010 and 2020
183
+ Assistant:
184
+ - TSLA CAGR (2010-2020): ~63.01%
185
+ - Represents average annual return with compounding
186
+ - Past performance not indicative of future results
187
+ - Consult a financial advisor"""
188
+
189
+ def generate(
190
+ message: str,
191
+ chat_history: list[dict],
192
+ system_prompt: str = DEFAULT_SYSTEM_PROMPT,
193
+ max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS,
194
+ temperature: float = 0.6,
195
+ top_p: float = 0.9,
196
+ top_k: int = 50,
197
+ repetition_penalty: float = 1.2,
198
+ ) -> Iterator[str]:
199
+ logger.info(f"Generating response for message: {message}")
200
+
201
+ lower_message = message.lower().strip()
202
+ if lower_message in ["hi", "hello"]:
203
+ response = "I'm FinChat, your financial advisor. Ask me anything finance-related!"
204
+ logger.info("Quick response for 'hi'/'hello' generated.")
205
+ yield response
206
+ return
207
+
208
+ # Check for CAGR/average return queries
209
+ match = re.match(r'(?:average return|cagr) for ([\w\s,]+(?:and [\w\s,]+)?) between (\d{4}) and (\d{4})', lower_message)
210
+ if match:
211
+ tickers_str, start_year, end_year = match.groups()
212
+ tickers = [t.strip().upper() for t in re.split(r',|\band\b', tickers_str) if t.strip()]
213
+ responses = []
214
+ if int(end_year) <= int(start_year):
215
+ yield "The specified time period is invalid (end year must be after start year)."
216
+ return
217
+ for ticker in tickers:
218
+ try:
219
+ data = yf.download(ticker, start=f"{start_year}-01-01", end=f"{end_year}-12-31")
220
+ if not data.empty:
221
+ if isinstance(data.columns, pd.MultiIndex):
222
+ data = data.droplevel('Ticker', axis=1)
223
+ initial = data['Close'].iloc[0]
224
+ final = data['Close'].iloc[-1]
225
+ start_date = data.index[0]
226
+ end_date = data.index[-1]
227
+ days = (end_date - start_date).days
228
+ years = days / 365.25
229
+ if years > 0:
230
+ cagr = ((final / initial) ** (1 / years) - 1) * 100
231
+ responses.append(f"- {ticker}: ~{cagr:.2f}%")
232
+ else:
233
+ responses.append(f"- {ticker}: Invalid period (no elapsed time).")
234
+ else:
235
+ responses.append(f"- {ticker}: No historical data available between {start_year} and {end_year}.")
236
+ except Exception as e:
237
+ responses.append(f"- {ticker}: Error calculating CAGR - {str(e)}")
238
+ full_response = f"CAGR for the requested stocks from {start_year} to {end_year}:\n" + "\n".join(responses) + "\n- Represents average annual returns with compounding\n- Past performance not indicative of future results\n- Consult a financial advisor"
239
+ logger.info("CAGR response generated.")
240
+ yield full_response
241
+ return
242
+
243
+ # Build conversation messages
244
+ conversation = [{"role": "system", "content": system_prompt}]
245
+ for msg in chat_history[-5:]: # Limit history to last 5 exchanges
246
+ if msg["role"] == "user":
247
+ conversation.append({"role": "user", "content": msg["content"]})
248
+ elif msg["role"] == "assistant":
249
+ conversation.append({"role": "assistant", "content": msg["content"]})
250
+ conversation.append({"role": "user", "content": message})
251
+
252
+ # Approximate token length check
253
+ prompt_text = "\n".join(d["content"] for d in conversation)
254
+ input_tokens = llm.tokenize(prompt_text.encode("utf-8"), add_bos=False)
255
+
256
+ # Generate response
257
+ try:
258
+ response = ""
259
+ stream = llm.create_chat_completion(
260
+ messages=conversation,
261
+ max_tokens=max_new_tokens,
262
+ temperature=temperature,
263
+ top_p=top_p,
264
+ top_k=top_k,
265
+ repeat_penalty=repetition_penalty,
266
+ stream=True
267
+ )
268
+ for chunk in stream:
269
+ delta = chunk["choices"][0]["delta"]
270
+ if "content" in delta and delta["content"] is not None:
271
+ response += delta["content"]
272
+ yield response
273
+ if chunk["choices"][0]["finish_reason"] is not None:
274
+ break
275
+ logger.info("Response generation completed.")
276
+ except Exception as e:
277
+ logger.error(f"Error during response generation: {str(e)}")
278
+ yield f"Error generating response: {str(e)}"
279
+
280
+ def process_portfolio(ticker1, shares1, cost1, price1, ticker2, shares2, cost2, price2, ticker3, shares3, cost3, price3, growth_rate):
281
+ portfolio = {}
282
+ if ticker1:
283
+ value1 = shares1 * price1
284
+ portfolio[ticker1.upper()] = {'shares': shares1, 'cost': cost1, 'price': price1, 'value': value1}
285
+ if ticker2:
286
+ value2 = shares2 * price2
287
+ portfolio[ticker2.upper()] = {'shares': shares2, 'cost': cost2, 'price': price2, 'value': value2}
288
+ if ticker3:
289
+ value3 = shares3 * price3
290
+ portfolio[ticker3.upper()] = {'shares': shares3, 'cost': cost3, 'price': price3, 'value': value3}
291
+ if not portfolio:
292
+ return "", None
293
+
294
+ total_value_now = sum(v['value'] for v in portfolio.values())
295
+ allocations = {k: v['value'] / total_value_now for k, v in portfolio.items()} if total_value_now > 0 else {}
296
+
297
+ fig_alloc, ax_alloc = plt.subplots()
298
+ ax_alloc.pie(allocations.values(), labels=allocations.keys(), autopct='%1.1f%%')
299
+ ax_alloc.set_title('Portfolio Allocation')
300
+ buf_alloc = io.BytesIO()
301
+ fig_alloc.savefig(buf_alloc, format='png')
302
+ buf_alloc.seek(0)
303
+ chart_alloc = Image.open(buf_alloc)
304
+ plt.close(fig_alloc) # Close the figure to free memory
305
+
306
+ def project_value(value, years, rate):
307
+ return value * (1 + rate / 100) ** years
308
+
309
+ total_value_1yr = sum(project_value(v['value'], 1, growth_rate) for v in portfolio.values())
310
+ total_value_2yr = sum(project_value(v['value'], 2, growth_rate) for v in portfolio.values())
311
+ total_value_5yr = sum(project_value(v['value'], 5, growth_rate) for v in portfolio.values())
312
+ total_value_10yr = sum(project_value(v['value'], 10, growth_rate) for v in portfolio.values())
313
+
314
+ data_str = (
315
+ "User portfolio:\n" +
316
+ "\n".join(f"- {k}: {v['shares']} shares, avg cost {v['cost']}, current price {v['price']}, value ${v['value']:,.2f}" for k, v in portfolio.items()) +
317
+ f"\nTotal value now: ${total_value_now:,.2f}\nProjected (at {growth_rate}% annual growth):\n"
318
+ f"- 1 year: ${total_value_1yr:,.2f}\n- 2 years: ${total_value_2yr:,.2f}\n- 5 years: ${total_value_5yr:,.2f}\n- 10 years: ${total_value_10yr:,.2f}"
319
+ )
320
+ return data_str, chart_alloc
321
+
322
+ # Gradio interface setup
323
+ with gr.Blocks(css="""#chatbot {height: 800px; overflow: auto;}""") as demo:
324
+ gr.Markdown(DESCRIPTION)
325
+ chatbot = gr.Chatbot(label="FinChat", type="messages")
326
+ msg = gr.Textbox(label="Ask a finance question", placeholder="e.g., 'What is CAGR?' or 'Average return for AAPL between 2010 and 2020'")
327
+
328
+ with gr.Row():
329
+ with gr.Column():
330
+ ticker1 = gr.Textbox(label="Ticker 1")
331
+ shares1 = gr.Number(label="Shares 1")
332
+ cost1 = gr.Number(label="Avg Cost/Share 1")
333
+ price1 = gr.Number(label="Current Price 1")
334
+ with gr.Column():
335
+ ticker2 = gr.Textbox(label="Ticker 2")
336
+ shares2 = gr.Number(label="Shares 2")
337
+ cost2 = gr.Number(label="Avg Cost/Share 2")
338
+ price2 = gr.Number(label="Current Price 2")
339
+ with gr.Column():
340
+ ticker3 = gr.Textbox(label="Ticker 3")
341
+ shares3 = gr.Number(label="Shares 3")
342
+ cost3 = gr.Number(label="Avg Cost/Share 3")
343
+ price3 = gr.Number(label="Current Price 3")
344
+
345
+ growth_rate = gr.Slider(minimum=5, maximum=50, step=5, value=10, label="Annual Growth Rate (%)", interactive=True, info="Selected Growth Rate: 10%")
346
+ growth_rate_label = gr.Markdown("**Selected Growth Rate: 10%**")
347
+
348
+ with gr.Row():
349
+ submit = gr.Button("Submit")
350
+ clear = gr.Button("Clear")
351
+
352
+ with gr.Accordion("Advanced Settings", open=False):
353
+ system_prompt = gr.Textbox(label="System Prompt", value=DEFAULT_SYSTEM_PROMPT, lines=6)
354
+ temperature = gr.Slider(label="Temperature", value=0.6, minimum=0.0, maximum=1.0, step=0.05)
355
+ top_p = gr.Slider(label="Top P", value=0.9, minimum=0.0, maximum=1.0, step=0.05)
356
+ top_k = gr.Slider(label="Top K", value=50, minimum=1, maximum=100, step=1)
357
+ repetition_penalty = gr.Slider(label="Repetition Penalty", value=1.2, minimum=1.0, maximum=2.0, step=0.05)
358
+ max_new_tokens = gr.Slider(label="Max New Tokens", value=DEFAULT_MAX_NEW_TOKENS, minimum=1, maximum=MAX_MAX_NEW_TOKENS, step=1)
359
+
360
+ gr.Markdown(LICENSE)
361
+
362
+ def update_growth_rate_label(growth_rate):
363
+ return f"**Selected Growth Rate: {growth_rate}%**"
364
+
365
+ def user(message, history):
366
+ if not message:
367
+ return "", history
368
+ return "", history + [{"role": "user", "content": message}]
369
+
370
+ def bot(history, sys_prompt, temp, tp, tk, rp, mnt, ticker1, shares1, cost1, price1, ticker2, shares2, cost2, price2, ticker3, shares3, cost3, price3, growth_rate):
371
  if not history:
372
  logger.warning("History is empty, initializing with user message.")
373
  history = [{"role": "user", "content": ""}]