tayyab-077 commited on
Commit
15fa2e5
Β·
1 Parent(s): a6c748c
Files changed (4) hide show
  1. app.py +108 -23
  2. requirements.txt +9 -8
  3. src/chatbot.py +16 -35
  4. src/model_loader.py +31 -13
app.py CHANGED
@@ -1,17 +1,39 @@
1
- import gradio as gr
 
 
 
 
2
  from datetime import datetime
3
- from pathlib import Path
4
- from src.model_loader import load_local_model
 
 
 
5
  from src.conversation import ConversationMemory
6
  from src.chatbot import LocalChatbot
7
 
8
- # ----------------------------
9
- # Model & Memory
10
- # ----------------------------
11
- MODEL_PATH = "togethercomputer/Gemini-2-2B" # public HF model
12
- llm = load_local_model(MODEL_PATH, device=-1) # CPU
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  memory = ConversationMemory(max_len=60)
14
- bot = LocalChatbot(llm, memory)
15
 
16
  INTENT_TEMPLATES = {
17
  "math": "You are a math solver. Solve step-by-step only.",
@@ -20,19 +42,20 @@ INTENT_TEMPLATES = {
20
  "exam": "Prepare concise exam-focused notes and important questions."
21
  }
22
 
 
 
 
23
  def now_ts():
24
  return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
25
 
26
- # ----------------------------
27
- # Chat Function
28
- # ----------------------------
29
- def generate_reply(user_msg, history=None):
30
  if history is None:
31
  history = []
32
 
33
  if not user_msg.strip():
34
  return history
35
 
 
36
  intent = None
37
  low = user_msg.lower()
38
  for key in INTENT_TEMPLATES:
@@ -42,8 +65,12 @@ def generate_reply(user_msg, history=None):
42
  break
43
 
44
  system_prefix = INTENT_TEMPLATES.get(intent, None)
45
- prompt = f"{system_prefix}\nUser: {user_msg}" if system_prefix else f"User: {user_msg}"
 
 
 
46
 
 
47
  bot_reply = bot.ask(prompt)
48
  ts = now_ts()
49
  bot_reply_ts = f"{bot_reply}\n\nπŸ•’ {ts}"
@@ -58,24 +85,82 @@ def generate_reply(user_msg, history=None):
58
 
59
  return history
60
 
61
- # ----------------------------
62
- # Gradio UI
63
- # ----------------------------
64
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
65
- gr.Markdown("## ⚑ Smart Learning Assistant - Tayyab")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
- chatbot = gr.Chatbot(height=480)
68
- msg = gr.Textbox(placeholder="Type your message...", lines=3)
69
- send_btn = gr.Button("Send")
70
- new_chat_btn = gr.Button("βž• New Chat")
71
 
 
72
  send_btn.click(generate_reply, inputs=[msg, chatbot], outputs=[chatbot])
73
  msg.submit(generate_reply, inputs=[msg, chatbot], outputs=[chatbot])
74
 
75
  def new_chat():
76
  memory.clear()
77
  return []
 
78
  new_chat_btn.click(new_chat, outputs=[chatbot])
79
 
 
 
 
 
 
 
 
 
 
80
  if __name__ == "__main__":
81
  demo.launch()
 
1
+ # app.py β€” Updated version for Hugging Face token & CPU
2
+
3
+ import os
4
+ import tempfile
5
+ import textwrap
6
  from datetime import datetime
7
+ from typing import List, Dict, Any, Optional
8
+
9
+ import gradio as gr
10
+ from transformers import AutoTokenizer, AutoModelForCausalLM
11
+
12
  from src.conversation import ConversationMemory
13
  from src.chatbot import LocalChatbot
14
 
15
+ # ----------------------
16
+ # HUGGING FACE SETTINGS
17
+ # ----------------------
18
+ HF_TOKEN = os.getenv("HF_TOKEN") # your Hugging Face token stored as secret variable
19
+ MODEL_PATH = "RedHatAI/gemma-2-2b-it-quantized.w4a16" # public or private model
20
+
21
+ # ----------------------
22
+ # LOAD MODEL + TOKENIZER
23
+ # ----------------------
24
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, use_fast=True, use_auth_token=HF_TOKEN)
25
+ llm = AutoModelForCausalLM.from_pretrained(
26
+ MODEL_PATH,
27
+ device_map="cpu", # CPU for multiple users
28
+ torch_dtype="auto",
29
+ use_auth_token=HF_TOKEN
30
+ )
31
+
32
+ # ----------------------
33
+ # MEMORY + CHATBOT
34
+ # ----------------------
35
  memory = ConversationMemory(max_len=60)
36
+ bot = LocalChatbot(llm, memory, tokenizer=tokenizer)
37
 
38
  INTENT_TEMPLATES = {
39
  "math": "You are a math solver. Solve step-by-step only.",
 
42
  "exam": "Prepare concise exam-focused notes and important questions."
43
  }
44
 
45
+ # ----------------------
46
+ # HELPER FUNCTIONS
47
+ # ----------------------
48
  def now_ts():
49
  return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
50
 
51
+ def generate_reply(user_msg: str, history: Optional[List[Dict[str, Any]]]):
 
 
 
52
  if history is None:
53
  history = []
54
 
55
  if not user_msg.strip():
56
  return history
57
 
58
+ # Detect intent
59
  intent = None
60
  low = user_msg.lower()
61
  for key in INTENT_TEMPLATES:
 
65
  break
66
 
67
  system_prefix = INTENT_TEMPLATES.get(intent, None)
68
+ if system_prefix:
69
+ prompt = f"{system_prefix}\nUser: {user_msg}"
70
+ else:
71
+ prompt = f"User: {user_msg}"
72
 
73
+ # Generate reply using LocalChatbot
74
  bot_reply = bot.ask(prompt)
75
  ts = now_ts()
76
  bot_reply_ts = f"{bot_reply}\n\nπŸ•’ {ts}"
 
85
 
86
  return history
87
 
88
+ # ----------------------
89
+ # EXPORT TXT/PDF
90
+ # ----------------------
91
+ def export_chat_files(history: List[Dict[str, Any]]) -> Dict[str, Optional[str]]:
92
+ tmpdir = tempfile.gettempdir()
93
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
94
+ txt_path = os.path.join(tmpdir, f"chat_history_{timestamp}.txt")
95
+
96
+ with open(txt_path, "w", encoding="utf-8") as f:
97
+ for msg in history:
98
+ content = msg.get("content", "")
99
+ lines = content.splitlines()
100
+ lines = [l.replace("USER:", "").replace("ASSISTANT:", "").strip() for l in lines]
101
+ f.write("\n".join(lines).strip() + "\n")
102
+ f.write("-" * 60 + "\n")
103
+
104
+ pdf_path = None
105
+ try:
106
+ from reportlab.lib.pagesizes import A4
107
+ from reportlab.pdfgen import canvas
108
+ pdf_path = os.path.join(tmpdir, f"chat_history_{timestamp}.pdf")
109
+ c = canvas.Canvas(pdf_path, pagesize=A4)
110
+ width, height = A4
111
+ margin = 40
112
+ textobject = c.beginText(margin, height - margin)
113
+ textobject.setFont("Helvetica", 10)
114
+ with open(txt_path, "r", encoding="utf-8") as fh:
115
+ for line in fh:
116
+ for wrapped in textwrap.wrap(line.rstrip(), 100):
117
+ textobject.textLine(wrapped)
118
+ c.drawText(textobject)
119
+ c.showPage()
120
+ c.save()
121
+ except:
122
+ pdf_path = None
123
+
124
+ return {"txt": txt_path, "pdf": pdf_path}
125
+
126
+ # ----------------------
127
+ # UI
128
+ # ----------------------
129
+ with gr.Blocks(title="Tayyab β€” Chatbot (API)") as demo:
130
+
131
+ with gr.Row():
132
+ with gr.Column(scale=1, min_width=220):
133
+ gr.Markdown("### ⚑ Tools & Export")
134
+ new_chat_btn = gr.Button("βž• New Chat")
135
+ export_btn = gr.Button("πŸ“₯ Export TXT/PDF")
136
+
137
+ with gr.Column(scale=3):
138
+ gr.Markdown("<h3>Smart Learning Assistant - Tayyab</h3>")
139
+ chatbot = gr.Chatbot(height=480, type="messages")
140
+ msg = gr.Textbox(placeholder="Type a message", show_label=False, lines=3)
141
+ send_btn = gr.Button("Send")
142
 
143
+ file_txt = gr.File(visible=False)
144
+ file_pdf = gr.File(visible=False)
 
 
145
 
146
+ # Chat actions
147
  send_btn.click(generate_reply, inputs=[msg, chatbot], outputs=[chatbot])
148
  msg.submit(generate_reply, inputs=[msg, chatbot], outputs=[chatbot])
149
 
150
  def new_chat():
151
  memory.clear()
152
  return []
153
+
154
  new_chat_btn.click(new_chat, outputs=[chatbot])
155
 
156
+ def export_handler(history):
157
+ files = export_chat_files(history or [])
158
+ return (
159
+ gr.update(value=files.get("txt"), visible=True),
160
+ gr.update(value=files.get("pdf"), visible=bool(files.get("pdf")))
161
+ )
162
+
163
+ export_btn.click(export_handler, inputs=[chatbot], outputs=[file_txt, file_pdf])
164
+
165
  if __name__ == "__main__":
166
  demo.launch()
requirements.txt CHANGED
@@ -1,9 +1,10 @@
1
  gradio==3.42
2
- torch
3
- transformers
4
- numpy
5
- pillow
6
- pdfplumber
7
- python-dotenv
8
- requests
9
- reportlab
 
 
1
  gradio==3.42
2
+ transformers==4.43.0
3
+ torch==2.2.0
4
+ numpy==1.25.2
5
+ reportlab==3.5.67
6
+ python-dotenv==1.0.0
7
+ pillow==10.0.0
8
+ pdfplumber==0.9.0
9
+ opencv-python==4.8.1.78
10
+ requests==2.31.0
src/chatbot.py CHANGED
@@ -1,31 +1,26 @@
1
- # src/chatbot.py
2
  from typing import Dict, Any, Optional
3
  from src.intent import detect_intent
4
  from src.templates import TEMPLATES
5
- import time
6
 
7
- # Default generation args (tweakable)
8
  DEFAULT_GEN_ARGS = {
9
  "max_tokens": 300,
10
  "temperature": 0.7,
11
- "top_p": 0.95,
12
- # "stop": ["User:", "Assistant:"] # enable if your llama binding supports stop tokens
13
  }
14
 
15
  MSG_SEPARATOR = "\n"
16
 
17
  class LocalChatbot:
18
- def __init__(self, llm, memory, default_template: Optional[str] = "general"):
19
  self.llm = llm
20
  self.memory = memory
 
21
  self.default_template = default_template
22
 
23
  def _build_system_prompt(self, intent: str) -> str:
24
- # get template for intent
25
  return TEMPLATES.get(intent, TEMPLATES.get(self.default_template, TEMPLATES["general"]))
26
 
27
  def _build_prompt(self, user_message: str, intent: str, max_pairs: int = 12) -> str:
28
- # Trim memory to recent pairs before building prompt
29
  try:
30
  self.memory.trim_to_recent_pairs(max_pairs)
31
  except Exception:
@@ -40,50 +35,36 @@ class LocalChatbot:
40
  f"User: {user_message}",
41
  "Assistant:"
42
  ]
43
- # join non-empty parts
44
- return MSG_SEPARATOR.join([p for p in parts if p is not None and p != ""])
45
 
46
  def ask(self, user_message: str, gen_args: Optional[Dict[str, Any]] = None) -> str:
47
- if not user_message or not user_message.strip():
48
  return "Please enter a message."
49
 
50
- # Detect intent
51
  intent = detect_intent(user_message)
 
52
 
53
- # Build prompt
54
- prompt = self._build_prompt(user_message, intent, max_pairs=12)
55
-
56
- # Merge generation args
57
  gen = DEFAULT_GEN_ARGS.copy()
58
  if gen_args:
59
  gen.update(gen_args)
60
 
61
- # Attempt to call the LLM (defensive: handle different API variants)
62
- try:
63
- output = self.llm(prompt, **gen)
64
- except TypeError:
65
- # fallback mapping: map max_tokens -> max_new_tokens
66
- alt_gen = gen.copy()
67
- if "max_tokens" in alt_gen:
68
- alt_gen["max_new_tokens"] = alt_gen.pop("max_tokens")
69
- output = self.llm(prompt, **alt_gen)
70
-
71
- # Parse the output robustly
72
- bot_reply = ""
73
  try:
74
- if isinstance(output, dict) and "choices" in output:
75
- bot_reply = output["choices"][0].get("text", "").strip()
76
- elif isinstance(output, str):
77
- bot_reply = output.strip()
 
78
  else:
79
- bot_reply = str(output).strip()
 
 
 
80
  except Exception:
81
- bot_reply = ""
82
 
83
  if not bot_reply:
84
  bot_reply = "Sorry β€” I couldn't generate a response. Please try again."
85
 
86
- # Add to memory
87
  try:
88
  self.memory.add(user_message, bot_reply)
89
  except Exception:
 
 
1
  from typing import Dict, Any, Optional
2
  from src.intent import detect_intent
3
  from src.templates import TEMPLATES
 
4
 
 
5
  DEFAULT_GEN_ARGS = {
6
  "max_tokens": 300,
7
  "temperature": 0.7,
8
+ "top_p": 0.95
 
9
  }
10
 
11
  MSG_SEPARATOR = "\n"
12
 
13
  class LocalChatbot:
14
+ def __init__(self, llm, memory, tokenizer=None, default_template: Optional[str] = "general"):
15
  self.llm = llm
16
  self.memory = memory
17
+ self.tokenizer = tokenizer
18
  self.default_template = default_template
19
 
20
  def _build_system_prompt(self, intent: str) -> str:
 
21
  return TEMPLATES.get(intent, TEMPLATES.get(self.default_template, TEMPLATES["general"]))
22
 
23
  def _build_prompt(self, user_message: str, intent: str, max_pairs: int = 12) -> str:
 
24
  try:
25
  self.memory.trim_to_recent_pairs(max_pairs)
26
  except Exception:
 
35
  f"User: {user_message}",
36
  "Assistant:"
37
  ]
38
+ return MSG_SEPARATOR.join([p for p in parts if p])
 
39
 
40
  def ask(self, user_message: str, gen_args: Optional[Dict[str, Any]] = None) -> str:
41
+ if not user_message.strip():
42
  return "Please enter a message."
43
 
 
44
  intent = detect_intent(user_message)
45
+ prompt = self._build_prompt(user_message, intent)
46
 
 
 
 
 
47
  gen = DEFAULT_GEN_ARGS.copy()
48
  if gen_args:
49
  gen.update(gen_args)
50
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  try:
52
+ if self.tokenizer:
53
+ # Transformers-style generation
54
+ inputs = self.tokenizer(prompt, return_tensors="pt")
55
+ outputs = self.llm.generate(**inputs, max_new_tokens=gen.get("max_tokens", 300))
56
+ bot_reply = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
57
  else:
58
+ # Fallback: callable LLM
59
+ bot_reply = self.llm(prompt, **gen)
60
+ if isinstance(bot_reply, dict) and "choices" in bot_reply:
61
+ bot_reply = bot_reply["choices"][0].get("text", "").strip()
62
  except Exception:
63
+ bot_reply = "Sorry β€” I couldn't generate a response. Please try again."
64
 
65
  if not bot_reply:
66
  bot_reply = "Sorry β€” I couldn't generate a response. Please try again."
67
 
 
68
  try:
69
  self.memory.add(user_message, bot_reply)
70
  except Exception:
src/model_loader.py CHANGED
@@ -1,17 +1,35 @@
1
- from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
 
2
 
3
- def load_local_model(model_path: str, device: int = -1):
4
  """
5
- Loads a Hugging Face model on CPU/GPU.
6
- device=-1 β†’ CPU, device=0 β†’ first GPU
 
 
 
 
 
 
 
7
  """
8
- tokenizer = AutoTokenizer.from_pretrained(model_path)
9
- model = AutoModelForCausalLM.from_pretrained(model_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- generator = pipeline(
12
- "text-generation",
13
- model=model,
14
- tokenizer=tokenizer,
15
- device=device # -1=CPU
16
- )
17
- return generator
 
1
+ from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig
2
+ import torch
3
 
4
+ def load_local_model(model_path: str, device: int = -1, token: str = None):
5
  """
6
+ Load a Hugging Face model (CPU by default) with optional token for private repos.
7
+
8
+ Args:
9
+ model_path (str): Hugging Face repo ID or local path.
10
+ device (int): -1 for CPU, >=0 for GPU index.
11
+ token (str): HF token for private models.
12
+
13
+ Returns:
14
+ model, tokenizer
15
  """
16
+ try:
17
+ tokenizer = AutoTokenizer.from_pretrained(model_path, use_auth_token=token)
18
+ except Exception as e:
19
+ raise RuntimeError(f"Failed to load tokenizer: {e}")
20
+
21
+ try:
22
+ config = AutoConfig.from_pretrained(model_path, use_auth_token=token)
23
+ model = AutoModelForCausalLM.from_pretrained(
24
+ model_path, config=config, use_auth_token=token
25
+ )
26
+
27
+ # Device mapping
28
+ if device >= 0 and torch.cuda.is_available():
29
+ model.to(f"cuda:{device}")
30
+ else:
31
+ model.to("cpu")
32
+ except Exception as e:
33
+ raise RuntimeError(f"Failed to load model: {e}")
34
 
35
+ return model, tokenizer