prepforge 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,227 @@
1
+ import sys
2
+ import os
3
+ from PyQt6.QtWidgets import (
4
+ QApplication,
5
+ QWidget,
6
+ QVBoxLayout,
7
+ QHBoxLayout,
8
+ QTextEdit,
9
+ QLineEdit,
10
+ QPushButton,
11
+ QComboBox,
12
+ QLabel,
13
+ QFileDialog,
14
+ QSpinBox,
15
+ )
16
+ from PyQt6.QtCore import Qt
17
+
18
+ # 🔥 CORE IMPORTS
19
+ from major_project.core.model import load_model
20
+ from major_project.core.utils import (
21
+ build_prompt,
22
+ generate_stream,
23
+ chat_history,
24
+ last_mcq_block,
25
+ )
26
+ from major_project.config import DEFAULT_APP_NAME, DEFAULT_MODEL, DEFAULT_LORA
27
+
28
+ os.environ["TRANSFORMERS_OFFLINE"] = "1"
29
+ os.environ["HF_HUB_OFFLINE"] = "1"
30
+ os.environ["QT_LOGGING_RULES"] = "*.warning=false"
31
+ os.environ["QT_QPA_PLATFORMTHEME"] = "gtk3"
32
+
33
+ APP_NAME = DEFAULT_APP_NAME
34
+
35
+
36
+ class AIApp(QWidget):
37
+ def __init__(self):
38
+ super().__init__()
39
+
40
+ self.setWindowTitle(f"🎓 {APP_NAME}")
41
+ self.resize(900, 700)
42
+
43
+ self.model_name = DEFAULT_MODEL
44
+ self.lora_path = DEFAULT_LORA
45
+
46
+ layout = QVBoxLayout()
47
+
48
+ # TITLE
49
+ title = QLabel(APP_NAME)
50
+ title.setAlignment(Qt.AlignmentFlag.AlignCenter)
51
+ title.setStyleSheet("font-size: 28px; font-weight: bold; padding: 10px;")
52
+ layout.addWidget(title)
53
+
54
+ # MODEL CONTROLS
55
+ model_layout = QHBoxLayout()
56
+
57
+ self.model_mode = QComboBox()
58
+ self.model_mode.addItems(["Base Model", "LoRA Adapter"])
59
+
60
+ self.lora_btn = QPushButton("Select LoRA Folder")
61
+ self.lora_btn.clicked.connect(self.select_lora_folder)
62
+
63
+ self.load_btn = QPushButton("Load Model")
64
+ self.load_btn.clicked.connect(self.load_selected_model)
65
+
66
+ model_layout.addWidget(QLabel("Model:"))
67
+ model_layout.addWidget(self.model_mode)
68
+ model_layout.addWidget(self.lora_btn)
69
+ model_layout.addWidget(self.load_btn)
70
+
71
+ layout.addLayout(model_layout)
72
+
73
+ # SETTINGS
74
+ top_layout = QHBoxLayout()
75
+
76
+ self.mode = QComboBox()
77
+ self.mode.addItems(["Chat", "Notes", "Study Plan", "MCQ Generator", "Practice"])
78
+
79
+ self.length_input = QSpinBox()
80
+ self.length_input.setRange(100, 2000)
81
+ self.length_input.setValue(400)
82
+
83
+ self.count_input = QSpinBox()
84
+ self.count_input.setRange(1, 50)
85
+ self.count_input.setValue(5)
86
+
87
+ top_layout.addWidget(self.mode)
88
+ top_layout.addWidget(self.length_input)
89
+ top_layout.addWidget(self.count_input)
90
+
91
+ layout.addLayout(top_layout)
92
+
93
+ # INPUT
94
+ self.input_box = QLineEdit()
95
+ layout.addWidget(self.input_box)
96
+
97
+ # BUTTONS
98
+ btn_layout = QHBoxLayout()
99
+
100
+ self.generate_btn = QPushButton("Generate")
101
+ self.generate_btn.clicked.connect(self.handle_generate)
102
+
103
+ self.clear_btn = QPushButton("Clear")
104
+ self.clear_btn.clicked.connect(self.clear_all)
105
+
106
+ self.save_btn = QPushButton("Save Chat")
107
+ self.save_btn.clicked.connect(self.save_chat)
108
+
109
+ btn_layout.addWidget(self.generate_btn)
110
+ btn_layout.addWidget(self.clear_btn)
111
+ btn_layout.addWidget(self.save_btn)
112
+
113
+ layout.addLayout(btn_layout)
114
+
115
+ # OUTPUT
116
+ self.output = QTextEdit()
117
+ self.output.setReadOnly(True)
118
+ layout.addWidget(self.output)
119
+
120
+ self.setLayout(layout)
121
+
122
+ self.model = None
123
+ self.tokenizer = None
124
+
125
+ # -----------------------------
126
+ # MODEL
127
+ # -----------------------------
128
+ def select_lora_folder(self):
129
+ path = QFileDialog.getExistingDirectory(self, "Select LoRA Folder")
130
+ if path:
131
+ self.lora_path = path
132
+
133
+ def load_selected_model(self):
134
+ use_lora = self.model_mode.currentText() == "LoRA Adapter"
135
+
136
+ if use_lora and not self.lora_path:
137
+ self.output.append("❌ Select LoRA folder first")
138
+ return
139
+
140
+ self.output.append("🔄 Loading model...")
141
+ QApplication.processEvents()
142
+
143
+ self.model, self.tokenizer = load_model(
144
+ self.model_name, self.lora_path if use_lora else None
145
+ )
146
+
147
+ self.output.append("✅ Model loaded")
148
+
149
+ # -----------------------------
150
+ # UTIL
151
+ # -----------------------------
152
+ def clear_all(self):
153
+ global chat_history, last_mcq_block
154
+ self.output.clear()
155
+ chat_history.clear()
156
+ last_mcq_block = None
157
+
158
+ def save_chat(self):
159
+ file_path, _ = QFileDialog.getSaveFileName(
160
+ self, "Save Chat", "", "Text Files (*.txt)"
161
+ )
162
+ if not file_path:
163
+ return
164
+ try:
165
+ with open(file_path, "w", encoding="utf-8") as f:
166
+ f.write(self.output.toPlainText())
167
+ self.output.append(f"\n✅ Saved to {file_path}\n")
168
+ except Exception as e:
169
+ self.output.append(f"\n❌ Error saving file: {str(e)}\n")
170
+
171
+ # -----------------------------
172
+ # STREAM GENERATE
173
+ # -----------------------------
174
+ def handle_generate(self):
175
+ if self.model is None:
176
+ self.output.append("❌ Load model first")
177
+ return
178
+
179
+ user_text = self.input_box.text().strip()
180
+ if not user_text:
181
+ return
182
+
183
+ # ✅ FIX: proper spacing
184
+ self.output.append(f"🧑 {user_text}\n")
185
+
186
+ prompt = build_prompt(
187
+ self.mode.currentText(), user_text, self.count_input.value()
188
+ )
189
+
190
+ streamer = generate_stream(
191
+ self.model, self.tokenizer, prompt, self.length_input.value()
192
+ )
193
+
194
+ response = ""
195
+
196
+ cursor = self.output.textCursor()
197
+ cursor.movePosition(cursor.MoveOperation.End)
198
+ cursor.insertText("🤖 ")
199
+ self.output.setTextCursor(cursor)
200
+
201
+ for token in streamer:
202
+ response += token
203
+ cursor.insertText(token)
204
+ self.output.setTextCursor(cursor)
205
+ QApplication.processEvents()
206
+
207
+ self.output.append("\n")
208
+
209
+ chat_history.append({"role": "assistant", "content": response})
210
+
211
+ if "SECTION 1" in response and "SECTION 2" in response:
212
+ global last_mcq_block
213
+ last_mcq_block = response
214
+
215
+
216
+ # -----------------------------
217
+ # MAIN
218
+ # -----------------------------
219
+ def main():
220
+ app = QApplication(sys.argv)
221
+ window = AIApp()
222
+ window.show()
223
+ sys.exit(app.exec())
224
+
225
+
226
+ if __name__ == "__main__":
227
+ main()
@@ -0,0 +1,171 @@
1
+ import os
2
+
3
+ # -----------------------------
4
+ # 🔥 FORCE OFFLINE MODE
5
+ # -----------------------------
6
+ os.environ["TRANSFORMERS_OFFLINE"] = "1"
7
+ os.environ["HF_HUB_OFFLINE"] = "1"
8
+
9
+ import streamlit as st
10
+
11
+ # 🔥 CORE IMPORTS
12
+ from major_project.core.model import load_model
13
+ from major_project.core.utils import (
14
+ build_prompt,
15
+ generate_stream,
16
+ chat_history,
17
+ last_mcq_block,
18
+ )
19
+ from major_project.config import DEFAULT_APP_NAME, DEFAULT_LORA, DEFAULT_MODEL
20
+
21
+ # -----------------------------
22
+ # CONFIG
23
+ # -----------------------------
24
+ APP_NAME = DEFAULT_APP_NAME
25
+ st.set_page_config(page_title=APP_NAME, layout="wide")
26
+
27
+
28
+ # -----------------------------
29
+ # MODEL CACHE
30
+ # -----------------------------
31
+ @st.cache_resource
32
+ def cached_model(base_model, lora_path):
33
+ return load_model(base_model, lora_path)
34
+
35
+
36
+ # -----------------------------
37
+ # SIDEBAR
38
+ # -----------------------------
39
+ st.sidebar.title("⚙️ Model Settings")
40
+
41
+ model_name = DEFAULT_MODEL
42
+ model_mode = st.sidebar.selectbox("Model Type", ["Base Model", "LoRA Adapter"])
43
+
44
+ lora_path = DEFAULT_LORA
45
+ if model_mode == "LoRA Adapter":
46
+ lora_path = st.sidebar.text_input("LoRA Path")
47
+
48
+ load_btn = st.sidebar.button("🚀 Load Model")
49
+
50
+
51
+ # -----------------------------
52
+ # SESSION STATE
53
+ # -----------------------------
54
+ if "model" not in st.session_state:
55
+ st.session_state.model = None
56
+ st.session_state.tokenizer = None
57
+
58
+ if "history" not in st.session_state:
59
+ st.session_state.history = []
60
+
61
+ if "last_mcq" not in st.session_state:
62
+ st.session_state.last_mcq = ""
63
+
64
+
65
+ # -----------------------------
66
+ # LOAD MODEL
67
+ # -----------------------------
68
+ if load_btn:
69
+ if model_mode == "LoRA Adapter" and not lora_path:
70
+ st.sidebar.error("Provide LoRA path")
71
+ else:
72
+ with st.spinner("Loading model..."):
73
+ model, tokenizer = cached_model(
74
+ model_name,
75
+ lora_path if model_mode == "LoRA Adapter" else None,
76
+ )
77
+ st.session_state.model = model
78
+ st.session_state.tokenizer = tokenizer
79
+
80
+ st.sidebar.success("Model loaded")
81
+
82
+
83
+ model = st.session_state.model
84
+ tokenizer = st.session_state.tokenizer
85
+
86
+
87
+ # -----------------------------
88
+ # SAFETY
89
+ # -----------------------------
90
+ if model is None or tokenizer is None:
91
+ st.title(f"🎓 {APP_NAME}")
92
+ st.warning("Load model from sidebar")
93
+ st.stop()
94
+
95
+
96
+ # -----------------------------
97
+ # UI
98
+ # -----------------------------
99
+ st.title(f"🎓 {APP_NAME}")
100
+
101
+ mode = st.sidebar.selectbox(
102
+ "Mode", ["Chat", "Notes", "Study Plan", "MCQ Generator", "Practice"]
103
+ )
104
+
105
+ length = st.sidebar.slider("Response Length", 100, 2000, 600)
106
+ count = st.sidebar.number_input("Number of Questions", 1, 50, 5)
107
+
108
+
109
+ # -----------------------------
110
+ # RESET
111
+ # -----------------------------
112
+ if st.sidebar.button("🧹 Reset Chat"):
113
+ st.session_state.history = []
114
+ chat_history.clear()
115
+ st.session_state.last_mcq = ""
116
+
117
+
118
+ # -----------------------------
119
+ # DISPLAY HISTORY
120
+ # -----------------------------
121
+ for msg in st.session_state.history:
122
+ with st.chat_message(msg["role"]):
123
+ st.write(msg["content"])
124
+
125
+
126
+ # -----------------------------
127
+ # INPUT
128
+ # -----------------------------
129
+ user_input = st.chat_input(f"Ask {APP_NAME}...")
130
+
131
+ if user_input:
132
+ with st.chat_message("user"):
133
+ st.write(user_input)
134
+
135
+ # 🔥 USE SHARED PROMPT BUILDER
136
+ prompt = build_prompt(mode, user_input, count)
137
+
138
+ with st.chat_message("assistant"):
139
+ placeholder = st.empty()
140
+ response = ""
141
+
142
+ # 🔥 USE SHARED STREAM ENGINE
143
+ streamer = generate_stream(model, tokenizer, prompt, length)
144
+
145
+ for token in streamer:
146
+ response += token
147
+ placeholder.markdown(response)
148
+
149
+ st.session_state.history.append({"role": "user", "content": user_input})
150
+ st.session_state.history.append({"role": "assistant", "content": response})
151
+
152
+ # 🔥 MCQ STORAGE
153
+ if "SECTION 1" in response and "SECTION 2" in response:
154
+ st.session_state.last_mcq = response
155
+
156
+
157
+ # -----------------------------
158
+ # SAVE CHAT
159
+ # -----------------------------
160
+ if st.session_state.history:
161
+ chat_text = f"=== {APP_NAME} Notes ===\n\n"
162
+
163
+ for msg in st.session_state.history:
164
+ role = "User" if msg["role"] == "user" else APP_NAME
165
+ chat_text += f"{role}:\n{msg['content']}\n\n"
166
+
167
+ st.sidebar.download_button(
168
+ "💾 Download Chat",
169
+ chat_text,
170
+ file_name=f"{APP_NAME.lower()}_chat.txt",
171
+ )
@@ -0,0 +1,241 @@
1
+ import os
2
+ import subprocess
3
+ import warnings
4
+ from transformers.utils import logging
5
+ from rich.console import Console
6
+ from rich.prompt import Prompt
7
+ from rich.live import Live
8
+ from rich.panel import Panel
9
+
10
+ # prompt_toolkit
11
+ from prompt_toolkit.application import Application
12
+ from prompt_toolkit.key_binding import KeyBindings
13
+ from prompt_toolkit.layout import Layout
14
+ from prompt_toolkit.widgets import Box, Frame, TextArea
15
+
16
+ # 🔥 IMPORT SHARED MODULES
17
+ from major_project.core.model import load_model
18
+ from major_project.core.utils import (
19
+ build_prompt,
20
+ generate_stream,
21
+ chat_history,
22
+ last_mcq_block,
23
+ )
24
+ from major_project.config import DEFAULT_APP_NAME, DEFAULT_MODEL, DEFAULT_LORA
25
+
26
+ # -----------------------------
27
+ # CONFIG
28
+ # -----------------------------
29
+ APP_NAME = DEFAULT_APP_NAME
30
+ console = Console()
31
+
32
+ # -----------------------------
33
+ # LLAMA TEMPLATE
34
+ # -----------------------------
35
+ LLAMA3_TEMPLATE = (
36
+ "{% for message in messages %}"
37
+ "{% if loop.first and messages[0]['role'] == 'system' %}"
38
+ "{{ '<|begin_of_text|><|start_header_id|>system<|end_header_id|>\\n\\n' + message['content'] + '<|eot_id|>' }}"
39
+ "{% else %}"
40
+ "{{ '<|start_header_id|>' + message['role'] + '<|end_header_id|>\\n\\n' + message['content'] + '<|eot_id|>' }}"
41
+ "{% endif %}"
42
+ "{% endfor %}"
43
+ "{% if add_generation_prompt %}"
44
+ "{{ '<|start_header_id|>assistant<|end_header_id|>\\n\\n' }}"
45
+ "{% endif %}"
46
+ )
47
+
48
+
49
+ # -----------------------------
50
+ # BANNER
51
+ # -----------------------------
52
+ def print_banner():
53
+ try:
54
+ result = subprocess.run(["figlet", APP_NAME], capture_output=True, text=True)
55
+ console.print(f"[bold cyan]{result.stdout}[/bold cyan]", justify="center")
56
+ console.print("[dim]AI Exam Preparation System[/dim]\n", justify="center")
57
+ except Exception:
58
+ console.print(f"[bold cyan]\n=== {APP_NAME.upper()} ===\n[/bold cyan]")
59
+
60
+
61
+ # -----------------------------
62
+ # ENV
63
+ # -----------------------------
64
+ os.environ["TRANSFORMERS_VERBOSITY"] = "error"
65
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
66
+ warnings.filterwarnings("ignore")
67
+ logging.set_verbosity_error()
68
+
69
+ print_banner()
70
+
71
+
72
+ # -----------------------------
73
+ # INPUT HELPERS
74
+ # -----------------------------
75
+ def get_valid_length(default):
76
+ while True:
77
+ value = Prompt.ask("Response length (100-2000)", default=str(default))
78
+ try:
79
+ val = int(value)
80
+ if 100 <= val <= 2000:
81
+ return val
82
+ except:
83
+ pass
84
+ console.print("[red]Invalid input[/red]")
85
+
86
+
87
+ def get_question_count(default=5):
88
+ while True:
89
+ value = Prompt.ask("How many questions?", default=str(default))
90
+ try:
91
+ val = int(value)
92
+ if 1 <= val <= 50:
93
+ return val
94
+ except:
95
+ pass
96
+ console.print("[red]Enter a number between 1-50[/red]")
97
+
98
+
99
+ def save_chat():
100
+ file_path = Prompt.ask("Enter file path")
101
+ if not file_path:
102
+ return
103
+ try:
104
+ file_path = os.path.expanduser(file_path)
105
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
106
+ with open(file_path, "w", encoding="utf-8") as f:
107
+ f.write(f"=== {APP_NAME} Notes ===\n\n")
108
+ for msg in chat_history:
109
+ role = "User" if msg["role"] == "user" else "AI"
110
+ f.write(f"{role}:\n{msg['content']}\n\n")
111
+ console.print(f"[green]Saved to {file_path}[/green]")
112
+ except Exception as e:
113
+ console.print(f"[red]{str(e)}[/red]")
114
+
115
+
116
+ def select_model_mode():
117
+ return Prompt.ask("Choose model mode", choices=["base", "lora"], default="base")
118
+
119
+
120
+ def get_lora_path():
121
+ path = Prompt.ask("Enter LoRA adapter path (leave empty to cancel)")
122
+ return path if path else None
123
+
124
+
125
+ # -----------------------------
126
+ # MODEL
127
+ # -----------------------------
128
+ console.print(f"[bold green]Loading {APP_NAME} model...[/bold green]")
129
+
130
+ model_mode = select_model_mode()
131
+ lora_path = DEFAULT_LORA if model_mode == "lora" else None
132
+
133
+ if model_mode == "lora":
134
+ user_lora = get_lora_path()
135
+ if user_lora:
136
+ lora_path = user_lora
137
+
138
+ model, tokenizer = load_model(base_model=DEFAULT_MODEL, lora_path=lora_path)
139
+
140
+ tokenizer.padding_side = "left"
141
+ if tokenizer.pad_token is None:
142
+ tokenizer.pad_token = tokenizer.eos_token
143
+
144
+ tokenizer.chat_template = LLAMA3_TEMPLATE
145
+
146
+ console.print("Model loaded!\n")
147
+
148
+
149
+ # -----------------------------
150
+ # MODE MENU
151
+ # -----------------------------
152
+ def select_mode():
153
+ options = ["chat", "notes", "mcq", "plan", "practice", "exit"]
154
+ index = [0]
155
+
156
+ def get_text():
157
+ return "\n".join(
158
+ [f"{'>' if i == index[0] else ' '} {opt}" for i, opt in enumerate(options)]
159
+ )
160
+
161
+ text_area = TextArea(text=get_text(), focusable=False)
162
+ kb = KeyBindings()
163
+
164
+ @kb.add("up")
165
+ def _(event):
166
+ index[0] = (index[0] - 1) % len(options)
167
+ text_area.text = get_text()
168
+
169
+ @kb.add("down")
170
+ def _(event):
171
+ index[0] = (index[0] + 1) % len(options)
172
+ text_area.text = get_text()
173
+
174
+ @kb.add("enter")
175
+ def _(event):
176
+ event.app.exit(result=options[index[0]])
177
+
178
+ root = Frame(Box(text_area, padding=1), title=f" {APP_NAME} ")
179
+ app = Application(layout=Layout(root), key_bindings=kb)
180
+ return app.run()
181
+
182
+
183
+ # -----------------------------
184
+ # MAIN LOOP
185
+ # -----------------------------
186
+ def main():
187
+ global last_mcq_block
188
+
189
+ mode, length = None, None
190
+ first_run = True
191
+
192
+ while True:
193
+ console.print(f"\n=== {APP_NAME} ===")
194
+
195
+ if first_run:
196
+ mode = select_mode()
197
+ if mode == "exit":
198
+ break
199
+ length = get_valid_length(400)
200
+ first_run = False
201
+ else:
202
+ if Prompt.ask("Change settings(y/n)?", default="n") == "y":
203
+ mode = select_mode()
204
+ if mode == "exit":
205
+ break
206
+ length = get_valid_length(length)
207
+
208
+ console.print(f"Mode: {mode} | length: {length}")
209
+
210
+ try:
211
+ user_input = Prompt.ask("\nAsk your question (or 'save' to save chat)")
212
+ except KeyboardInterrupt:
213
+ break
214
+
215
+ if user_input == "exit":
216
+ break
217
+ if user_input == "save":
218
+ save_chat()
219
+ continue
220
+
221
+ count = get_question_count() if mode in ["mcq", "practice"] else None
222
+ prompt = build_prompt(mode, user_input, count)
223
+
224
+ console.print("\nThinking...\n")
225
+
226
+ streamer = generate_stream(model, tokenizer, prompt, length)
227
+ response = ""
228
+
229
+ with Live(Panel("", title="Response"), refresh_per_second=10) as live:
230
+ for token in streamer:
231
+ response += token
232
+ live.update(Panel(response, title="Response"))
233
+
234
+ chat_history.append({"role": "assistant", "content": response})
235
+
236
+ if "SECTION 1" in response and "SECTION 2" in response:
237
+ last_mcq_block = response
238
+
239
+
240
+ if __name__ == "__main__":
241
+ main()