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.
- major_project/__init__.py +0 -0
- major_project/cli.py +186 -0
- major_project/config.py +17 -0
- major_project/core/__init__.py +0 -0
- major_project/core/config_store.py +20 -0
- major_project/core/model.py +112 -0
- major_project/core/train.py +149 -0
- major_project/core/utils.py +222 -0
- major_project/gui_app.py +227 -0
- major_project/streamlit_app.py +171 -0
- major_project/tui_app.py +241 -0
- prepforge-0.1.0.dist-info/METADATA +149 -0
- prepforge-0.1.0.dist-info/RECORD +17 -0
- prepforge-0.1.0.dist-info/WHEEL +5 -0
- prepforge-0.1.0.dist-info/entry_points.txt +2 -0
- prepforge-0.1.0.dist-info/licenses/LICENSE +21 -0
- prepforge-0.1.0.dist-info/top_level.txt +1 -0
major_project/gui_app.py
ADDED
|
@@ -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
|
+
)
|
major_project/tui_app.py
ADDED
|
@@ -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()
|