mahreenfathima commited on
Commit
f391dd9
Β·
verified Β·
1 Parent(s): 0a565db

Upload 8 files

Browse files
Files changed (9) hide show
  1. .gitattributes +1 -0
  2. .gitignore +20 -0
  3. app.py +388 -0
  4. client.py +273 -0
  5. modal_tool.py +259 -0
  6. requirements.txt +0 -0
  7. server.py +515 -0
  8. static/fullnew.jpg +0 -0
  9. static/new.jpg +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ static/new.jpg filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+ *llama.cpp/
9
+
10
+ # Virtual environments
11
+ .venv
12
+
13
+ .env
14
+ *.env
15
+ .env.local
16
+ __pycache__/
17
+ *.pyc
18
+ .venv/
19
+ venv/
20
+ .uv/
app.py ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #Ψ¨Ψ³Ω… Ψ§Ω„Ω„Ω‡ Ψ§Ω„Ψ±Ψ­Ω…Ω† Ψ§Ω„Ψ±Ψ­ΩŠΩ…
2
+ import gradio as gr
3
+ import asyncio
4
+ import base64
5
+ from client import run_fistal
6
+ import asyncio
7
+ import os
8
+ from dotenv import load_dotenv
9
+
10
+ load_dotenv()
11
+
12
+ REQUIRED_SECRETS = [
13
+ "GOOGLE_API_KEY_1",
14
+ "GOOGLE_API_KEY_2",
15
+ "GOOGLE_API_KEY_3",
16
+ "GROQ_API_KEY",
17
+ "GEMINI_API_KEY",
18
+ "HUGGINGFACE_API_KEY",
19
+ "MODAL_TOKEN_ID",
20
+ "MODAL_TOKEN_SECRET"
21
+ ]
22
+
23
+ missing = [s for s in REQUIRED_SECRETS if not os.getenv(s)]
24
+ if missing:
25
+ raise ValueError(f"Missing secrets in HF Space: {', '.join(missing)}\nAdd them in Settings β†’ Variables and secrets")
26
+
27
+
28
+
29
+ def image_to_base64(filepath):
30
+ try:
31
+ with open(filepath, "rb") as image_file:
32
+ encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
33
+ mime_type = "image/jpeg" if filepath.lower().endswith((".jpg", ".jpeg")) else "image/png"
34
+ return f"data:{mime_type};base64,{encoded_string}"
35
+ except FileNotFoundError:
36
+ print(f"Error: Image file not found at {filepath}")
37
+ return ""
38
+
39
+
40
+ image_data_url = image_to_base64("static/new.jpg")
41
+ full_img = image_to_base64("static/fullnew.jpg")
42
+
43
+ def app():
44
+ css = f"""
45
+ /* Global App Styling */
46
+ .gradio-container {{
47
+ background: url('{full_img}') !important;
48
+ background-size: cover !important;
49
+ box-shadow: linear-gradient(to right, #008DDA, #6A1AAB, #C71585, #F56C40) !important;!important;
50
+ outline: linear-gradient(to right, #008DDA, #6A1AAB, #C71585, #F56C40) !important; !important;
51
+ }}
52
+ .gradio-container .block {{
53
+ background-color: #27272a !important;
54
+ }}
55
+ .gradio-container .wrap {{
56
+ background-color: linear-gradient(to right, #008DDA, #6A1AAB, #C71585, #F56C40) !important;
57
+ border: linear-gradient(to right, #008DDA, #6A1AAB, #C71585, #F56C40) !important;
58
+ border-width: 1px !important;
59
+
60
+ }}
61
+ .gradio-container .block,
62
+ .gradio-container .wrap {{
63
+ border: none !important;
64
+ box-shadow: none !important; /* removes shadow */
65
+ outline: none !important; /* removes focus outline */
66
+ }}
67
+
68
+ #tuner {{
69
+ background: linear-gradient(to right, #008DDA, #6A1AAB, #C71585, #F56C40) !important;
70
+ padding: 10px;
71
+ border-radius: 8px;
72
+ }}
73
+ #tuner .wrap {{
74
+ background-color: #5f5f5f !important;
75
+ }}
76
+ .laun {{
77
+ background: linear-gradient(to right, #008DDA, #6A1AAB, #C71585, #F56C40) !important;
78
+ padding: 10px;
79
+ border-radius: 8px;
80
+ color: white;
81
+ }}
82
+
83
+ .mark {{
84
+ background-color: #27272a !important;
85
+ padding: 6px;
86
+ }}
87
+ .me {{
88
+ background-color: #27272a !important;
89
+ color: white !important;
90
+ border: none !important;
91
+ }}
92
+ .me textarea {{
93
+ background-color: #5f5f5f !important;
94
+ color: white !important;
95
+ }}
96
+ .label, .form > div > label, .block > label {{
97
+ color: white !important;
98
+ }}
99
+ .drop {{
100
+ background-color: #27272a !important;
101
+ color: white !important;
102
+ }}
103
+
104
+ .drop li {{
105
+ background-color: #27272a !important;
106
+ color: white !important;
107
+ }}
108
+ .drop input {{
109
+ background-color: #5f5f5f !important;
110
+ background-size: cover !important;
111
+ color: white !important;
112
+ border: none !important;
113
+ padding: 6px 10px !important;
114
+ border-radius: 4px !important;
115
+ }}
116
+ .drop .wrap {{
117
+ background-color: #5f5f5f !important;
118
+ border-radius: 4px !important;
119
+ }}
120
+
121
+ .out {{
122
+ padding: 10px !important;
123
+ font-size: 25px !important;
124
+ /*margin-left: 10px !important;*/
125
+
126
+ }}
127
+
128
+ .login-container .wrap {{
129
+ background-color: green !important;
130
+ border-radius: 20px !important;
131
+ }}
132
+ .login-container {{
133
+ background-color: #5f5f5f !important;
134
+ display: flex;
135
+ height: 85vh;
136
+ width: 100%;
137
+ margin: 0;
138
+ padding: 0;
139
+ }}
140
+ .left-side {{
141
+ flex: 1;
142
+ background: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)),
143
+ url('{image_to_base64("static/new.jpg")}') center/cover;
144
+ display: flex;
145
+ flex-direction: column;
146
+ justify-content: center;
147
+ align-items: center;
148
+ color: white;
149
+ padding: 60px;
150
+ }}
151
+ .left-side h1 {{
152
+ font-size: 3.2rem;
153
+ font-weight: 700;
154
+ margin-bottom: 20px;
155
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
156
+ }}
157
+ .left-side p {{
158
+ font-size: 1.5rem;
159
+ font-weight: 300;
160
+ text-align: center;
161
+ max-width: 500px;
162
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
163
+ }}
164
+ .right-side {{
165
+ flex: 1;
166
+ display: flex;
167
+ flex-direction: column;
168
+ justify-content: center;
169
+ align-items: center;
170
+ background-image: linear-gradient(to bottom, #000000, #050505, #0b0b0b, #0f0f0f, #131313);
171
+ padding: 60px;
172
+ }}
173
+ .login-box {{
174
+ background: rgba(255, 255, 255, 0.15);
175
+ backdrop-filter: blur(10px);
176
+ border-radius: 20px;
177
+ padding: 50px 40px;
178
+ width: 100%;
179
+ max-width: 400px;
180
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
181
+ color: white;
182
+ }}
183
+ .login-box h2 {{
184
+ font-size: 2rem;
185
+ margin-bottom: 30px;
186
+ text-align: center;
187
+ margin-left: -50px;
188
+ }}
189
+ #launch_button {{
190
+ width: 100% !important;
191
+ }}
192
+ :root, .gradio-container * {{
193
+ --block-background-fill: #27272a !important;
194
+ --panel-background-fill: linear-gradient(to right, #008DDA, #6A1AAB, #C71585, #F56C40) !important;
195
+ --input-background-fill: #5f5f5f !important;
196
+ --color-background-primary: #27272a !important;
197
+ --block-border-width: 1px !important;
198
+ --block-border-color: linear-gradient(to right, #008DDA, #6A1AAB, #C71585, #F56C40) !important;
199
+ --panel-border-width: 1px !important;
200
+ --panel-border-color: linear-gradient(to right, #008DDA, #6A1AAB, #C71585, #F56C40) !important;
201
+ --neutral-50: #27272a !important;
202
+ }}
203
+ @media (max-width: 768px) {{
204
+ .login-container {{
205
+ flex-direction: column;
206
+ }}
207
+ .left-side {{
208
+ min-height: 40vh;
209
+ }}
210
+ .left-side h1 {{
211
+ font-size: 2.5rem;
212
+ }}
213
+ }}
214
+ """
215
+
216
+ with gr.Blocks(title="Fistal AI πŸš€", css=css, theme=gr.themes.Ocean()) as demo:
217
+
218
+ with gr.Group(visible=True) as login_block:
219
+ gr.HTML(f"""
220
+ <div class="login-container">
221
+ <div class="left-side">
222
+ <h1 style="color: white !important;">Fistal AI</h1>
223
+ <p style="color: white !important;">Finetune LLM's with ease</p>
224
+ </div>
225
+ <div class="right-side">
226
+ <div class="login-box">
227
+ <h2 style="color: white !important;">✨ Features</h2>
228
+ <div style="text-align: left; color: #fff; line-height: 1.8;">
229
+ <div style="margin-bottom: 20px;">
230
+ <strong style="color: #667eea;">πŸ€– Agentic AI</strong><br>
231
+ <span style="font-size: 0.9rem;color: white !important;">LangGraph-powered automation via Fistal MCP</span>
232
+ </div>
233
+ <div style="margin-bottom: 20px;">
234
+ <strong style="color: #667eea;">⚑ Modal GPU</strong><br>
235
+ <span style="font-size: 0.9rem;color: white !important;">Serverless T4 training, no setup needed</span>
236
+ </div>
237
+ <div style="margin-bottom: 20px;">
238
+ <strong style="color: #667eea;">πŸ¦₯ Unsloth</strong><br>
239
+ <span style="font-size: 0.9rem;color: white !important;">2x faster, 70% less memory</span>
240
+ </div>
241
+ <div style="margin-bottom: 25px;">
242
+ <strong style="color: #667eea;">πŸ“Š Auto Evaluation</strong><br>
243
+ <span style="font-size: 0.9rem;color: white !important;">LLM-as-a-judge with BLEU, ROUGE metrics assessment</span>
244
+ </div>
245
+ </div>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ """)
250
+
251
+ launch_btn = gr.Button(
252
+ value="πŸš€ Launch Fistal",
253
+ elem_id="launch_fistal_btn",
254
+ elem_classes="laun"
255
+ )
256
+
257
+ # ---------------- MAIN APP BLOCK ----------------
258
+ with gr.Group(visible=False) as main_block:
259
+ gr.HTML(f"""
260
+ <div class="start" style="
261
+ background: url('{image_data_url}');
262
+ background-size: cover;
263
+ background-position: center;
264
+ background-repeat: no-repeat;
265
+ padding: 20px;
266
+ margin-top:10px;
267
+ margin-bottom: 10px;
268
+ border-radius: 10px;">
269
+ <h1 style="color: white; font-size: 35px;">Fistal AI πŸš€</h1>
270
+ <p style="color: white; margin-top: -5px;">Seamlessly fine-tune LLMs with an Agentic AI powered by MCP, Modal, and Unsloth.</p>
271
+ <div style="display:flex; gap:5px; flex-wrap:wrap; align-items:center; margin-bottom:15px;">
272
+ <a href="https://huggingface.co/spaces/your-username/fistal-ai">
273
+ <img src="https://img.shields.io/badge/%F0%9F%A4%97%20-%20HF%20Space%20-%20orange" alt="HF Space">
274
+ </a>
275
+ <img src="https://img.shields.io/badge/Python-3.11-blue?logo=python" alt="Python">
276
+ <img src="https://img.shields.io/badge/Modal-Enabled-green" alt="Modal">
277
+ <img src="https://img.shields.io/badge/Unsloth-4bit-purple" alt="Unsloth">
278
+ <img src="https://img.shields.io/badge/MCP-Enabled-pink" alt="MCP">
279
+ <img src="https://img.shields.io/badge/%F0%9F%94%B6%20-%20Gradio%20-%20%23fc7280" alt="Gradio">
280
+ <img src="https://img.shields.io/badge/%F0%9F%A4%96%20-%20Agentic%20AI%20-%20%23472731" alt="Agentic AI">
281
+ <img src="https://img.shields.io/badge/%F0%9F%A7%AE%20-%201B%2F2B%2F3B%20models%20-%20teal" alt="1B-3B Models">
282
+ <img src="https://img.shields.io/badge/%F0%9F%93%9D%20-%20Evaluation%20Report%20-%20purple" alt="Evaluation Report">
283
+ </div>
284
+ </div>
285
+ """)
286
+
287
+ with gr.Group(elem_classes="me"):
288
+ with gr.Row():
289
+ topic = gr.Textbox(label="πŸ“š Dataset topic", placeholder="Python Questions, Return policy FAQS...", elem_classes="me")
290
+ samples = gr.Slider(label="πŸ“Š Number of samples", minimum=0, maximum=2000, interactive=True, step=5, value=1000, elem_classes="me")
291
+ task_type = gr.Dropdown(label="🎯 Task Type", choices=["text-generation","summarization","classification","question-answering"], interactive=True, elem_classes="drop")
292
+ model_name = gr.Dropdown(
293
+ label="πŸ€– Model to Fine-tune",
294
+ choices=[
295
+ "unsloth/Llama-3.2-1B-Instruct-bnb-4bit",
296
+ "unsloth/Phi-3-mini-4k-instruct",
297
+ "unsloth/Phi-3-medium-4k-instruct",
298
+ "unsloth/Llama-3.2-3B-Instruct-bnb-4bit",
299
+ "unsloth/Qwen2.5-3B-Instruct-bnb-4bit",
300
+ "unsloth/Qwen2.5-1.5B-Instruct-bnb-4bit",
301
+ "unsloth/Qwen2.5-0.5B-Instruct-bnb-4bit",
302
+ "unsloth/Qwen2.5-Coder-3B-Instruct-bnb-4bit",
303
+ "unsloth/gemma-2-2b-it-bnb-4bit",
304
+ "unsloth/SmolLM2-1.7B-Instruct-bnb-4bit",
305
+ "unsloth/Phi-3.5-mini-instruct-bnb-4bit",
306
+ "unsloth/Granite-3.0-2b-instruct-bnb-4bit",
307
+ "unsloth/granite-4.0-h-1b-bnb-4bit"
308
+ ], interactive=True, elem_classes="drop"
309
+ )
310
+ tuner = gr.Button("πŸš€ Start Finetuning", size="lg", elem_id="tuner")
311
+ gr.Markdown("""## <span style="color: white;">πŸ”€ Agent Activity Flow</span>""", elem_classes="mark")
312
+ status = gr.Textbox(label="Status", value="Ready to start...", interactive=False)
313
+ output = gr.Markdown(label="Output Log:", value="", elem_classes="out")
314
+ model_link = gr.Button(
315
+ value="πŸ€— View Model on Hugging Face",
316
+ visible=False,
317
+ elem_classes="out"
318
+ )
319
+ async def run_workflow(dataset_topic, samples, model, task, request = gr.Request):
320
+ output_log = "## Under the Hood" + "\n\n"
321
+ output_log += "πŸ“‹ **Configuration:**\n\n"
322
+ output_log += f" β€’ Topic: {dataset_topic}\n\n"
323
+ output_log += f" β€’ Samples: {samples}\n\n"
324
+ output_log += f" β€’ Model: {model}\n\n"
325
+ output_log += f" β€’ Task: {task}\n\n"
326
+
327
+ yield (" Starting workflow...", output_log, "")
328
+
329
+ try:
330
+ in_eval_report = False
331
+ eval_report_buffer = ""
332
+
333
+ async for chunk in run_fistal(
334
+ dataset_topic=dataset_topic,
335
+ num_samples=samples,
336
+ model_name=model,
337
+ task_type=task
338
+ ):
339
+ if "evaluating" in str(chunk).lower() or "llm_as_judge" in str(chunk).lower():
340
+ in_eval_report = True
341
+
342
+ if in_eval_report:
343
+ eval_report_buffer += str(chunk)
344
+ else:
345
+ output_log += str(chunk)
346
+
347
+ import re
348
+ urls = re.findall(r'https://huggingface\.co/[^\s\)]+', output_log + eval_report_buffer)
349
+ model_url = urls[0] if urls else ""
350
+ model_url = model_url.rstrip('.')
351
+ model_url = re.sub(r'[^a-zA-Z0-9:/._-].*$', '', model_url)
352
+
353
+ yield ("🟑 Processing...", output_log + eval_report_buffer, model_url)
354
+ await asyncio.sleep(0.1)
355
+
356
+ # Final output
357
+ final_output = output_log
358
+ if eval_report_buffer:
359
+ final_output += "πŸ“Š **EVALUATION REPORT**\n\n"
360
+ final_output += eval_report_buffer
361
+
362
+ final_output += "\n\n✨ **Fistal AI has completed the process!**"
363
+ yield ("🟒 Complete!", final_output, gr.Button(
364
+ value="πŸ€— View Model on Hugging Face",
365
+ visible=True,
366
+ interactive=True,
367
+ link=model_url
368
+ ))
369
+
370
+ except Exception as e:
371
+ import traceback
372
+ error_log = output_log + f"\n\n❌ **ERROR:**\n```\n{str(e)}\n{traceback.format_exc()}\n```"
373
+ yield ("πŸ”΄ Error", error_log, "")
374
+
375
+
376
+ tuner.click(run_workflow, [topic, samples, model_name, task_type], [status, output, model_link])
377
+
378
+
379
+ launch_btn.click(
380
+ lambda: (gr.update(visible=False), gr.update(visible=True)),
381
+ None,
382
+ [login_block, main_block]
383
+ )
384
+
385
+ return demo
386
+
387
+ if __name__ == "__main__":
388
+ app().launch()
client.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.graph import StateGraph, START
2
+ from dotenv import load_dotenv
3
+ from langchain_google_genai import ChatGoogleGenerativeAI
4
+ from typing import TypedDict, Annotated
5
+ from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
6
+ from langgraph.graph.message import add_messages
7
+ from langgraph.prebuilt import ToolNode, tools_condition
8
+ import asyncio
9
+ from langchain_mcp_adapters.client import MultiServerMCPClient
10
+ import os
11
+ from typing import Optional
12
+ import sys
13
+
14
+ load_dotenv()
15
+
16
+ api_key = os.getenv("GEMINI_API_KEY")
17
+
18
+ llm = ChatGoogleGenerativeAI(
19
+ model="gemini-2.5-flash",
20
+ temperature=0.2,
21
+ google_api_key=api_key
22
+ )
23
+
24
+ client = MultiServerMCPClient(
25
+ {
26
+ "FistalMCP": {
27
+ "transport": "stdio",
28
+ "command": sys.executable,
29
+ "args": ["-u", os.path.join(os.path.dirname(__file__), "server.py")]
30
+ }
31
+ }
32
+ )
33
+
34
+ if client:
35
+ print("βœ“ Client initialized!")
36
+ else:
37
+ print("βœ— Failed to initialize MCP Client")
38
+
39
+
40
+ class ChatState(TypedDict):
41
+ messages: Annotated[list[BaseMessage], add_messages]
42
+ dataset_topic: str
43
+ num_samples: int
44
+ model_name: str
45
+ task_type: str
46
+ dataset_path: Optional[str]
47
+ converted_path: Optional[str]
48
+ model_path: Optional[str]
49
+ hf_url: Optional[str]
50
+
51
+
52
+ async def my_graph():
53
+ """Agent graph that handles mcp tools"""
54
+ tools = await client.get_tools()
55
+
56
+ available_tools = []
57
+ tool_order = ["generate_json_data", "format_json", "finetune_model", "llm_as_judge"]
58
+ available_tools = []
59
+ for tool_name in tool_order:
60
+ for tool in tools:
61
+ if tool.name == tool_name:
62
+ available_tools.append(tool)
63
+ break
64
+
65
+ llm_toolkit = llm.bind_tools(available_tools)
66
+
67
+ async def chat_node(state: ChatState):
68
+ messages = state["messages"]
69
+ dataset_topic = state['dataset_topic']
70
+ if isinstance(dataset_topic, list):
71
+ dataset_topic = dataset_topic[0] if dataset_topic else "unknown"
72
+
73
+ num_samples = state['num_samples']
74
+ if isinstance(num_samples, list):
75
+ num_samples = num_samples[0] if num_samples else 100
76
+
77
+ model_name = state['model_name']
78
+ if isinstance(model_name, list):
79
+ model_name = model_name[0] if model_name else "unknown"
80
+
81
+ task_type = state['task_type']
82
+ if isinstance(task_type, list):
83
+ task_type = task_type[0] if task_type else "text-generation"
84
+
85
+ system_msg = f"""You are Fistal, an AI fine-tuning assistant.
86
+
87
+ **User's Configuration:**
88
+ - Dataset Topic: {dataset_topic}
89
+ - Number of Samples: {num_samples}
90
+ - Model to Fine-tune: {model_name}
91
+ - Task Type: {task_type}
92
+ - Evaluation : Using LLM
93
+
94
+ **Your Workflow:**
95
+ 1. Use generate_json_data with topic="{dataset_topic}", task_type="{task_type}", num_samples={num_samples}
96
+ - This returns a dictionary with a "data" field containing the raw dataset
97
+
98
+ 2. Use format_json with the "data" field from step 1
99
+ - Pass: raw_data=<the data list from step 1>
100
+ - This returns a dictionary with a "data" field containing formatted data
101
+
102
+ 3. Use finetune_model with the "data" field from step 2 and model_name="{model_name}"
103
+ - Pass: formatted_data=<the data list from step 2>, model_name="{model_name}"
104
+ - This returns the Hugging Face repo URL
105
+
106
+ 4. Use llm_as_judge with the repo_id from step 3
107
+ - Pass: repo_id=<the HF repo from step 3>, topic="{dataset_topic}", task_type="{task_type}"
108
+
109
+ **FINAL STEP - CRITICAL:**
110
+ 5. After completing all tools, you MUST return:
111
+ - The Hugging Face model URL from step 3
112
+ - The evaluation report from step 4
113
+ - Format your final response as:
114
+
115
+ πŸŽ‰ **Fine-tuning Complete!**
116
+
117
+ **πŸ€— Model Repository:** [HF Repo Link] \n\n
118
+ **πŸ“Š Evaluation Report:** [Full report from llm_as_judge]
119
+
120
+ **IMPORTANT:**
121
+ - Tools pass DATA directly, not file paths
122
+ - Always mention the tool you are going to use first and then proceed with the tool action
123
+ - Extract the "data" field from each tool's response and pass it to the next tool
124
+ - After llm_as_judge completes, return both the HF URL and evaluation report
125
+ - Keep the user informed of progress at each step
126
+ - If a step takes time, do not stay idle. Inform users about short interesting facts
127
+ - Report any errors clearly
128
+ - Do not mention internal data structures or file paths"""
129
+
130
+
131
+ full_messages = [SystemMessage(content=system_msg)] + messages
132
+ response = await llm_toolkit.ainvoke(full_messages)
133
+ return {'messages': [response]}
134
+
135
+ tool_node = ToolNode(available_tools)
136
+
137
+ graph = StateGraph(ChatState)
138
+
139
+ graph.add_node("chat_node", chat_node)
140
+ graph.add_node("tools", tool_node)
141
+
142
+ graph.add_edge(START, "chat_node")
143
+ graph.add_conditional_edges("chat_node", tools_condition)
144
+ graph.add_edge("tools", "chat_node")
145
+
146
+ chat = graph.compile()
147
+
148
+ return chat
149
+
150
+
151
+
152
+
153
+ async def run_fistal(
154
+ dataset_topic: str,
155
+ num_samples: int,
156
+ model_name: str,
157
+ task_type: str
158
+ ):
159
+ chatbot = await my_graph()
160
+ user_message = f"""Execute the complete fine-tuning workflow:
161
+ - Generate {num_samples} training examples about {dataset_topic}
162
+ - Fine-tune {model_name}
163
+ - Evaluate for {task_type} task
164
+
165
+ Start now!"""
166
+ initial_state = {
167
+ "messages": [HumanMessage(content=user_message)],
168
+ "dataset_topic": dataset_topic,
169
+ "num_samples": num_samples,
170
+ "model_name": model_name,
171
+ "task_type": task_type,
172
+ "dataset_path": None,
173
+ "converted_path": None,
174
+ "model_path": None,
175
+ "hf_url": None
176
+ }
177
+ facts = {
178
+ "generate_json_data": [
179
+ "πŸ’‘ Using parallel batch generation with multiple API keys for 3x speed!",
180
+ "πŸ“Š Quality over quantity - diverse examples lead to better models!",
181
+ "🎯 Generating diverse prompt-response pairs...",
182
+ ],
183
+ "format_json": [
184
+ "πŸ”„ Converting to chat format optimized for instruction tuning...",
185
+ "πŸ’¬ Proper formatting helps models understand conversation structure!",
186
+ "🎨 Applying ChatML format for consistency...",
187
+ "βœ… Validating JSON structure for training compatibility...",
188
+ "πŸ”§ Optimizing token distribution across examples..."
189
+ ],
190
+ "finetune_model": [
191
+ "πŸ‹οΈ Training on Modal's serverless T4 GPU...",
192
+ "πŸ’‘ Using 4-bit quantization to fit in 16GB VRAM!",
193
+ "πŸ¦₯ Unsloth makes training 2x faster with 70% less memory!",
194
+ "⚑ LoRA fine-tuning updates only 0.1% of model parameters!",
195
+ "🎯 Typical training time: 10-20 minutes for 500 samples...",
196
+ "πŸ”₯ Your model is learning patterns from authentic data!",
197
+ "☁️ Uploading to HuggingFace - your model will be public soon!"
198
+ ],
199
+ "llm_as_judge": [
200
+ "πŸ“Š Generating evaluation test cases...",
201
+ "πŸ€– LLM-as-judge provides qualitative insights!",
202
+ "✨ Testing model coherence, relevance, and accuracy...",
203
+ "πŸ“ Creating comprehensive evaluation report...",
204
+ "πŸ” Analyzing response quality and task alignment...",
205
+ "πŸ“ Creating comprehensive evaluation report...",
206
+ "πŸ“ˆ Comparing outputs against expected responses...",
207
+ "🎯 Assessing model's understanding of the domain...",
208
+ "βœ… Finalizing evaluation metrics.."
209
+ ]
210
+ }
211
+
212
+ current_tool = None
213
+ fact_i = 0
214
+
215
+ async for event in chatbot.astream(initial_state):
216
+ if "tools" in event:
217
+ messages = event["tools"].get("messages", [])
218
+ for msg in messages:
219
+ if hasattr(msg,"name"):
220
+ tool_name = msg.name
221
+ current_tool = tool_name
222
+ fact_i = 0
223
+ yield f"\n{'-'*60}\n"
224
+ yield f"πŸ”„ **Using: {tool_name}**\n\n"
225
+ if tool_name in facts:
226
+ yield f"{facts[tool_name][0]}\n"
227
+ await asyncio.sleep(0.3)
228
+
229
+ if "chat_node" in event:
230
+ messages = event["chat_node"].get("messages", [])
231
+ for msg in messages:
232
+ if hasattr(msg, 'content') and msg.content:
233
+ raw_content = msg.content
234
+ content = ""
235
+
236
+ if isinstance(raw_content, list):
237
+ for item in raw_content:
238
+ if isinstance(item, dict) and item.get('type') == 'text':
239
+ content += item.get('text', '')
240
+ content = content.strip()
241
+ elif isinstance(raw_content, str):
242
+ content = raw_content
243
+ else:
244
+ content = str(raw_content)
245
+
246
+ if content and len(content) > 20 and "tool_calls" not in content.lower():
247
+ yield f"\nπŸ€– **Fistal:** {content}\n"
248
+
249
+ if current_tool and current_tool in facts:
250
+ fact_i += 1
251
+ if fact_i < len(facts[current_tool]):
252
+ yield f"\nπŸ’‘ {facts[current_tool][fact_i]}\n"
253
+ await asyncio.sleep(0.3)
254
+ yield "βœ… **Successfully finetuned!**\n"
255
+
256
+
257
+
258
+ async def main():
259
+ """Test the agent. Only for running client.py"""
260
+ print("Testing Fistal Agent\n")
261
+
262
+ result = await run_fistal(
263
+ "python programming",
264
+ 5,
265
+ "unsloth/Llama-3.2-1B-Instruct-bnb-4bit",
266
+ "text-generation"
267
+ )
268
+
269
+ print(f"\nAgent Response:\n{result}")
270
+
271
+
272
+ if __name__ == '__main__':
273
+ asyncio.run(main())
modal_tool.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import modal
2
+ import json
3
+ from datasets import Dataset
4
+ import time
5
+
6
+
7
+ modal.enable_output()
8
+
9
+ app = modal.App("fistalfinetuner")
10
+
11
+ volume = modal.Volume.from_name("fistal-models", create_if_missing=True )
12
+
13
+
14
+
15
+
16
+
17
+ modal_image = (
18
+ modal.Image.debian_slim(python_version="3.11")
19
+ .apt_install("git")
20
+ .pip_install(
21
+ "torch>=2.6.0",
22
+ "torchvision",
23
+ "torchaudio",
24
+ extra_index_url="https://download.pytorch.org/whl/cu121",
25
+
26
+ )
27
+ .pip_install(
28
+ "transformers",
29
+ "datasets",
30
+ "accelerate",
31
+ "trl",
32
+ "bitsandbytes",
33
+ "peft",
34
+ "unsloth_zoo",
35
+ "datasets==4.3.0"
36
+ )
37
+ .pip_install(
38
+ "unsloth @ git+https://github.com/unslothai/unsloth.git"
39
+ )
40
+ )
41
+
42
+ @app.function(
43
+ image=modal_image,
44
+ gpu="T4",
45
+ timeout=3600,
46
+ volumes={"/models":volume},
47
+ retries=modal.Retries(max_retries=0, backoff_coefficient=1.0)
48
+ )
49
+ def train_with_modal(ft_data: str, model_name: str):
50
+ """
51
+ Finetuning model using Modal's GPU
52
+ """
53
+ import torch
54
+
55
+ if not torch.cuda.is_available():
56
+ return {"status": "error", "message": "No GPU available!"}
57
+
58
+ from unsloth import FastLanguageModel, is_bf16_supported
59
+ from transformers import TrainingArguments
60
+ from trl import SFTTrainer
61
+ import os
62
+
63
+ data = []
64
+ for line in ft_data.strip().split('\n'):
65
+ if line.strip():
66
+ data.append(json.loads(line))
67
+
68
+ model, tokenizer = FastLanguageModel.from_pretrained(
69
+ model_name=model_name,
70
+ max_seq_length=512,
71
+ load_in_4bit=True,
72
+ dtype=None
73
+ )
74
+
75
+ print("Configuring LoRA...")
76
+ model = FastLanguageModel.get_peft_model(
77
+ model,
78
+ r=128,
79
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
80
+ lora_alpha=16,
81
+ lora_dropout=0,
82
+ bias="none",
83
+ random_state=2001,
84
+ use_gradient_checkpointing="unsloth",
85
+ loftq_config=None,
86
+ use_rslora=False
87
+ )
88
+
89
+ def format_example(example):
90
+ text = tokenizer.apply_chat_template(
91
+ example['messages'],
92
+ tokenize=False,
93
+ add_generation_prompt=False
94
+ )
95
+ return {"text": text}
96
+
97
+ dataset = Dataset.from_list(data)
98
+ dataset = dataset.map(format_example)
99
+
100
+ trainer = SFTTrainer(
101
+ model=model,
102
+ tokenizer=tokenizer,
103
+ train_dataset=dataset,
104
+ dataset_text_field="text",
105
+ max_seq_length=2000,
106
+ dataset_num_proc=2,
107
+ args=TrainingArguments(
108
+ per_device_train_batch_size=2,
109
+ gradient_accumulation_steps=8,
110
+ warmup_steps=5,
111
+ num_train_epochs=1,
112
+ max_steps=30,
113
+ learning_rate=2e-4,
114
+ fp16=not is_bf16_supported(),
115
+ bf16=is_bf16_supported(),
116
+ logging_steps=1,
117
+ optim="adamw_8bit",
118
+ lr_scheduler_type="linear",
119
+ output_dir="/tmp/training_output",
120
+ seed=42,
121
+ report_to="none",
122
+ dataloader_num_workers=0
123
+ )
124
+ )
125
+ print("Training started...")
126
+ trainer.train()
127
+ print("Training complete!")
128
+
129
+ timestamp = int(time.time())
130
+ volume_path = f"/models/finetuned-{timestamp}"
131
+
132
+ os.makedirs(volume_path, exist_ok=True)
133
+ print(f"Saving to: {volume_path}")
134
+
135
+
136
+ model.save_pretrained_merged(volume_path, tokenizer, save_method="merged_16bit")
137
+ print("Model saved!")
138
+ model.config.save_pretrained(volume_path)
139
+
140
+ trainer.save_model(volume_path)
141
+ tokenizer.save_pretrained(volume_path)
142
+
143
+
144
+
145
+
146
+
147
+ volume.commit()
148
+ print("Volume has been committed!")
149
+
150
+ del model
151
+ del trainer
152
+ import gc
153
+ gc.collect()
154
+ torch.cuda.empty_cache()
155
+
156
+ return {
157
+ "status":"success",
158
+ "volume_path":volume_path,
159
+ "timestamp": timestamp
160
+
161
+ }
162
+
163
+
164
+
165
+
166
+ @app.function(
167
+ image=modal_image,
168
+ volumes={"/models": volume},
169
+ timeout=900,
170
+ secrets=[modal.Secret.from_name("huggingface-secret")]
171
+ )
172
+ def upload_to_hf_from_volume(volume_path: str, timestamp: int, repoName: str):
173
+ """
174
+ Upload model directly from Modal Volume to HuggingFace
175
+ This runs on Modal's fast network - no download to local machine needed!
176
+ """
177
+ from huggingface_hub import HfApi, create_repo
178
+ import os
179
+
180
+ print(f"πŸ“€ Uploading from {volume_path} to HuggingFace...")
181
+
182
+ if not os.path.exists(volume_path):
183
+ raise FileNotFoundError(f"Model not found at: {volume_path}")
184
+
185
+ hf_token = os.environ.get("HF_TOKEN")
186
+ if not hf_token:
187
+ raise ValueError("HF_TOKEN not found in Modal secrets")
188
+
189
+ hf_api = HfApi()
190
+ repo_id = f"mahreenfathima/finetuned-{repoName}-{timestamp}"
191
+
192
+ print(f"Creating HuggingFace repo: {repo_id}")
193
+ create_repo(
194
+ repo_id=repo_id,
195
+ token=hf_token,
196
+ private=False,
197
+ exist_ok=True,
198
+ repo_type="model"
199
+ )
200
+
201
+ print(f"Uploading files to {repo_id}...")
202
+ hf_api.upload_folder(
203
+ folder_path=volume_path,
204
+ repo_id=repo_id,
205
+ token=hf_token,
206
+ commit_message=f"Fine-tuned model (timestamp: {timestamp})"
207
+ )
208
+
209
+ model_url = f"https://huggingface.co/{repo_id}"
210
+ print(f"βœ… Successfully uploaded to {model_url}")
211
+
212
+ return {
213
+ "model_url": model_url,
214
+ "repo_id": repo_id
215
+ }
216
+
217
+ @app.function(
218
+ gpu="T4",
219
+ timeout=600,
220
+ image=modal_image
221
+ )
222
+ def evaluate_model(repo_id: str, test_inputs: list[str]):
223
+ """Load model and run inference on test cases"""
224
+ from unsloth import FastLanguageModel
225
+ from transformers import AutoTokenizer
226
+ import torch
227
+
228
+ print(f"Loading model: {repo_id}")
229
+ model, tokenizer = FastLanguageModel.from_pretrained(
230
+ model_name=repo_id,
231
+ max_seq_length=512,
232
+ load_in_4bit=True,
233
+ dtype=None,
234
+ )
235
+ if tokenizer.pad_token is None:
236
+ tokenizer.pad_token = tokenizer.eos_token
237
+
238
+
239
+ outputs = []
240
+ for test_input in test_inputs:
241
+ print(f"Processing: {test_input[:50]}...")
242
+ inputs = tokenizer(test_input, return_tensors="pt").to(model.device)
243
+
244
+ with torch.no_grad():
245
+ output = model.generate(
246
+ **inputs,
247
+ max_new_tokens=100,
248
+ temperature=0.5,
249
+ do_sample=True
250
+ )
251
+
252
+ decoded = tokenizer.decode(output[0], skip_special_tokens=True)
253
+ if decoded.startswith(test_input):
254
+ decoded = decoded[len(test_input):].strip()
255
+ outputs.append(decoded)
256
+
257
+ return outputs
258
+
259
+
requirements.txt ADDED
Binary file (5.61 kB). View file
 
server.py ADDED
@@ -0,0 +1,515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #Ψ¨Ψ³Ω… Ψ§Ω„Ω„Ω‡ Ψ§Ω„Ψ±Ψ­Ω…Ω† Ψ§Ω„Ψ±Ψ­ΩŠΩ…
2
+ from unittest import result
3
+ from fastmcp import FastMCP, Context
4
+ import asyncio
5
+ import json
6
+ import os
7
+ import time
8
+ import re
9
+ from langchain_core.prompts import ChatPromptTemplate
10
+ from langchain_google_genai import ChatGoogleGenerativeAI
11
+ from langchain_community.tools import DuckDuckGoSearchRun
12
+ from langchain_groq import ChatGroq
13
+ from dotenv import load_dotenv
14
+ import nltk
15
+ from modal_tool import train_with_modal, app, upload_to_hf_from_volume, evaluate_model
16
+
17
+ load_dotenv()
18
+
19
+ GROQ_API_KEY = os.getenv('GROQ_API_KEY')
20
+ HF_TOKEN = os.getenv('HF_TOKEN')
21
+
22
+ try:
23
+ nltk.data.find('tokenizers/punkt')
24
+ except LookupError:
25
+ nltk.download('punkt', quiet=True)
26
+
27
+ mcp = FastMCP(name="FistalMCP")
28
+
29
+ GOOGLE_API_KEYS = [
30
+ os.getenv("GOOGLE_API_KEY_1"),
31
+ os.getenv("GOOGLE_API_KEY_2"),
32
+ os.getenv("GOOGLE_API_KEY_3")
33
+ ]
34
+
35
+ GOOGLE_API_KEYS = [key for key in GOOGLE_API_KEYS if key]
36
+
37
+ if not GOOGLE_API_KEYS:
38
+ raise ValueError("Where are your keys?")
39
+
40
+
41
+ async def genBatch(topic: str, samples_per_batch: int, batch_num: int, api_key: str, task_type: str) -> list:
42
+ """Generate one batch of samples using a single API key"""
43
+
44
+ if not api_key or api_key == "YOUR_API_KEY":
45
+ return []
46
+
47
+ llm = ChatGoogleGenerativeAI(
48
+ model="gemini-2.5-flash",
49
+ temperature=0.7,
50
+ google_api_key=api_key
51
+ )
52
+
53
+ prompt_template = """
54
+ You are an expert dataset generator.
55
+ Generate authentic, high-quality data on the topic: {topic} for task type: {task_type} using your knowledge.
56
+ Generate exactly {num} concise, varied, and high-quality samples.
57
+ Return a JSON list of objects, each with keys: instruction, input, and output.
58
+ Do not add extra texts, markdown, or code fences.
59
+ RESPONSE:
60
+ """
61
+
62
+ promptJSON = ChatPromptTemplate.from_template(prompt_template)
63
+ chain = promptJSON | llm
64
+
65
+ try:
66
+ user_input = {
67
+ "topic": topic,
68
+ "num": samples_per_batch,
69
+ "task_type": task_type
70
+ }
71
+
72
+ response = await asyncio.to_thread(chain.invoke, user_input)
73
+ content = response.content.strip()
74
+
75
+ if content.startswith("```json"):
76
+ content = content[7:]
77
+ if content.startswith("```"):
78
+ content = content[3:]
79
+ if content.endswith("```"):
80
+ content = content[:-3]
81
+
82
+ content = content.strip()
83
+ data = json.loads(content)
84
+
85
+ return data if isinstance(data, list) else [data]
86
+
87
+ except json.JSONDecodeError as e:
88
+ print(f"JSON decode error in batch {batch_num}: {e}")
89
+ return []
90
+ except Exception as e:
91
+ print(f"Error in batch {batch_num}: {e}")
92
+ return []
93
+
94
+
95
+ @mcp.tool()
96
+ async def generate_json_data(topic: str, task_type: str, num_samples: int = 1000) -> str:
97
+ """
98
+ Generate a training dataset with instruction, input, and output fields.
99
+ Uses parallel batching for efficiency. Can generate up to 2000 samples.
100
+
101
+ Args:
102
+ topic: The topic or theme for the dataset
103
+ num_samples: Number of training examples to generate (recommended: 100-2000)
104
+
105
+ Returns:
106
+ JSON string with status, topic, total_samples, and data array
107
+ """
108
+ topic = str(topic).strip() if topic else ""
109
+ task_type = str(task_type).strip() if task_type else "text-generation"
110
+
111
+ try:
112
+ num_samples = int(num_samples)
113
+ except (ValueError, TypeError):
114
+ num_samples = 100
115
+
116
+ if not topic:
117
+ return json.dumps({
118
+ "status": "error",
119
+ "message": "Topic cannot be empty"
120
+ })
121
+ if num_samples <= 0 or num_samples > 2000:
122
+ num_samples = min(max(50, num_samples), 2000)
123
+
124
+
125
+ valid_keys = [k for k in GOOGLE_API_KEYS if k and k.strip() and k != "YOUR_API_KEY"]
126
+ if not valid_keys:
127
+ return json.dumps({
128
+ "status": "error",
129
+ "message": "No valid Google API keys configured"
130
+ })
131
+
132
+ start_time = time.time()
133
+ samples_per_batch = 50
134
+ total_batches = (num_samples + samples_per_batch - 1) // samples_per_batch
135
+
136
+ try:
137
+ tasks = []
138
+
139
+ for batch_num in range(total_batches):
140
+ api_key = valid_keys[batch_num % len(valid_keys)]
141
+ task = genBatch(
142
+ topic=topic.strip(),
143
+ samples_per_batch=samples_per_batch,
144
+ batch_num=batch_num + 1,
145
+ api_key=api_key,
146
+ task_type=task_type.strip()
147
+ )
148
+ tasks.append(task)
149
+
150
+ results = await asyncio.gather(*tasks, return_exceptions=True)
151
+
152
+ all_samples = []
153
+ for batch_result in results:
154
+ if isinstance(batch_result, Exception):
155
+ continue
156
+ if isinstance(batch_result, list):
157
+ all_samples.extend(batch_result)
158
+
159
+ all_samples = all_samples[:num_samples]
160
+ end_time = time.time()
161
+ gen_time = end_time - start_time
162
+
163
+ return json.dumps({
164
+ "status": "success",
165
+ "topic": topic,
166
+ "task_type": task_type,
167
+ "total_samples": len(all_samples),
168
+ "requested_samples": num_samples,
169
+ "total_batches": total_batches,
170
+ "generation_time_seconds": round(gen_time, 1),
171
+ "generation_time_minutes": round(gen_time / 60, 2),
172
+ "samples_per_second": round(len(all_samples) / gen_time, 2) if gen_time > 0 else 0,
173
+ "data": all_samples
174
+ })
175
+
176
+ except Exception as e:
177
+ return json.dumps({
178
+ "status": "error",
179
+ "message": f"Error generating dataset: {str(e)}"
180
+ })
181
+
182
+
183
+ @mcp.tool()
184
+ async def format_json(raw_data) -> str:
185
+ """
186
+ Convert raw dataset to ChatML format for training
187
+
188
+ Args:
189
+ raw_data: List or JSON string of samples with instruction/input/output
190
+
191
+ Returns:
192
+ JSON string with status, num_samples, and formatted data
193
+ """
194
+ try:
195
+ if isinstance(raw_data, list):
196
+ data = raw_data
197
+ elif isinstance(raw_data, str):
198
+ parsed = json.loads(raw_data)
199
+ if isinstance(parsed, dict) and "data" in parsed:
200
+ data = parsed["data"]
201
+ else:
202
+ data = parsed
203
+ elif isinstance(raw_data, dict) and "data" in raw_data:
204
+ data = raw_data["data"]
205
+ else:
206
+ return json.dumps({
207
+ "status": "error",
208
+ "message": f"Unexpected input type: {type(raw_data).__name__}"
209
+ })
210
+
211
+ if not isinstance(data, list):
212
+ return json.dumps({
213
+ "status": "error",
214
+ "message": "Data must be a list of samples"
215
+ })
216
+
217
+ # Convert to ChatML format
218
+ converted = []
219
+ for item in data:
220
+ if not isinstance(item, dict):
221
+ continue
222
+
223
+ if 'instruction' not in item or 'output' not in item:
224
+ continue
225
+
226
+ user_msg = str(item['instruction'])
227
+ if item.get('input'):
228
+ user_msg += f"\n\n{item['input']}"
229
+
230
+ converted.append({
231
+ "messages": [
232
+ {"role": "system", "content": "You are a helpful assistant."},
233
+ {"role": "user", "content": user_msg},
234
+ {"role": "assistant", "content": str(item['output'])}
235
+ ]
236
+ })
237
+
238
+ if not converted:
239
+ return json.dumps({
240
+ "status": "error",
241
+ "message": "No valid samples to format"
242
+ })
243
+
244
+ return json.dumps({
245
+ "status": "success",
246
+ "num_samples": len(converted),
247
+ "data": converted,
248
+ "message": f"βœ… Formatted {len(converted)} samples"
249
+ }, ensure_ascii=False)
250
+
251
+ except Exception as e:
252
+ import traceback
253
+ return json.dumps({
254
+ "status": "error",
255
+ "message": f"Formatting failed: {str(e)}",
256
+ "traceback": traceback.format_exc()
257
+ })
258
+
259
+
260
+
261
+ @mcp.tool()
262
+ async def finetune_model(formatted_data, model_name: str, topic: str, task_type: str) -> str:
263
+ """
264
+ Fine-tune model on Modal GPU
265
+
266
+ Args:
267
+ formatted_data: List or JSON string with formatted training samples
268
+ model_name: Base model to fine-tune
269
+
270
+ Returns:
271
+ JSON string with status, repo_id, model_url
272
+ """
273
+ model_name = str(model_name).strip()
274
+
275
+ models = [
276
+ "unsloth/Llama-3.2-1B-Instruct-bnb-4bit",
277
+ "unsloth/Phi-3-mini-4k-instruct",
278
+ "unsloth/Phi-3-medium-4k-instruct",
279
+ "unsloth/Llama-3.2-3B-Instruct-bnb-4bit",
280
+ "unsloth/Qwen2.5-3B-Instruct-bnb-4bit",
281
+ "unsloth/Qwen2.5-1.5B-Instruct-bnb-4bit",
282
+ "unsloth/Qwen2.5-0.5B-Instruct-bnb-4bit",
283
+ "unsloth/Qwen2.5-Coder-3B-Instruct-bnb-4bit",
284
+ "unsloth/gemma-2-2b-it-bnb-4bit",
285
+ "unsloth/SmolLM2-1.7B-Instruct-bnb-4bit",
286
+ "unsloth/Phi-3.5-mini-instruct-bnb-4bit",
287
+ "unsloth/Granite-3.0-2b-instruct-bnb-4bit",
288
+ "unsloth/granite-4.0-h-1b-bnb-4bit"
289
+ ]
290
+
291
+ if model_name not in models:
292
+ return json.dumps({
293
+ "status": "error",
294
+ "message": f"Model not supported. Choose from: {', '.join(models[:3])}..."
295
+ })
296
+
297
+ try:
298
+ if isinstance(formatted_data, list):
299
+ training_data = formatted_data
300
+ elif isinstance(formatted_data, str):
301
+ parsed = json.loads(formatted_data)
302
+ if isinstance(parsed, dict) and "data" in parsed:
303
+ training_data = parsed["data"]
304
+ else:
305
+ training_data = parsed
306
+ elif isinstance(formatted_data, dict) and "data" in formatted_data:
307
+ training_data = formatted_data["data"]
308
+ else:
309
+ return json.dumps({
310
+ "status": "error",
311
+ "message": f"Unexpected input type: {type(formatted_data).__name__}"
312
+ })
313
+
314
+ if not isinstance(training_data, list) or not training_data:
315
+ return json.dumps({
316
+ "status": "error",
317
+ "message": "No training samples provided"
318
+ })
319
+
320
+ jsonl_content = "\n".join([json.dumps(s, ensure_ascii=False) for s in training_data])
321
+
322
+ with app.run():
323
+ result = train_with_modal.remote(jsonl_content, model_name)
324
+
325
+ if result["status"] != "success":
326
+ return json.dumps({
327
+ "status": "error",
328
+ "message": "Training failed"
329
+ })
330
+
331
+ repoTemp = """
332
+ Generate a short repository name for an unsloth finetuned model based on {topic} and {task_type}.
333
+ Use '_' instead of spaces. Only return the name without quotations.
334
+ """
335
+ repoPrompt = ChatPromptTemplate.from_template(repoTemp)
336
+ llm = ChatGroq(
337
+ model="llama-3.1-8b-instant",
338
+ temperature=0.4,
339
+ api_key=GROQ_API_KEY
340
+ )
341
+
342
+ chain = repoPrompt | llm
343
+
344
+ inp = {
345
+ "topic": topic,
346
+ "task_type": task_type
347
+ }
348
+
349
+ repoName = await asyncio.to_thread(chain.invoke, inp)
350
+ repoName = repoName.content.strip()
351
+
352
+
353
+
354
+ with app.run():
355
+ hf_result = upload_to_hf_from_volume.remote(
356
+ result["volume_path"],
357
+ result["timestamp"],
358
+ repoName
359
+ )
360
+
361
+ return json.dumps({
362
+ "status": "success",
363
+ "repo_id": str(hf_result["repo_id"]),
364
+ "model_url": str(hf_result["model_url"]),
365
+ "model_path": str(hf_result["repo_id"]),
366
+ "num_samples": len(training_data),
367
+ "message": f"βœ… Model at {hf_result['model_url']}"
368
+ })
369
+
370
+ except Exception as e:
371
+ import traceback
372
+ return json.dumps({
373
+ "status": "error",
374
+ "message": f"Training failed: {str(e)}",
375
+ "traceback": traceback.format_exc()
376
+ })
377
+
378
+
379
+ @mcp.tool()
380
+ async def llm_as_judge(repo_id:str, topic: str, task_type: str) -> dict:
381
+ """Use LLM to judge model quality based on topic and task type"""
382
+ import evaluate
383
+ eval_llm = ChatGroq(
384
+ model="llama-3.1-8b-instant",
385
+ temperature=0.2,
386
+ api_key=GROQ_API_KEY
387
+ )
388
+ test_prompt_text = f"""Generate 3 test cases for evaluating a model fine-tuned strictly based on **{topic} for {task_type}**.
389
+ Return ONLY a JSON array with this exact format, no other text:
390
+ [{{"input": "test question 1", "expected_output": "expected answer 1"}}, {{"input": "test question 2", "expected_output": "expected answer 2"}}, {{"input": "test question 3", "expected_output": "expected answer 3"}}]"""
391
+ try:
392
+ text_responses = await eval_llm.ainvoke(test_prompt_text)
393
+ response = text_responses.content.strip()
394
+ response = response.replace("```json", "").replace("```", "").strip()
395
+ import re
396
+ match = re.search(r'\[.*\]', response, re.DOTALL)
397
+ if match:
398
+ response = match.group(0)
399
+
400
+ test_cases = json.loads(response)[:3]
401
+
402
+ test_inputs = [case['input'] for case in test_cases]
403
+
404
+ with app.run():
405
+ ft_output = evaluate_model.remote(repo_id, test_inputs)
406
+
407
+ outputs = []
408
+ for i, case in enumerate(test_cases):
409
+ outputs.append(
410
+ {
411
+ "input": case['input'],
412
+ "expected_output": case['expected_output'],
413
+ "model_output": ft_output[i]
414
+
415
+ }
416
+ )
417
+ #METRICS:
418
+ bleu = evaluate.load("bleu")
419
+ rouge = evaluate.load("rouge")
420
+
421
+ predictions = [output['model_output'] for output in outputs]
422
+ references = [[output['expected_output']] for output in outputs]
423
+
424
+ bleu_score = bleu.compute(predictions=predictions, references=references)
425
+ rouge_score = rouge.compute(predictions=predictions, references=references)
426
+ additional_metrics = {}
427
+ if task_type.lower() in ["classification", "question-answering"]:
428
+ accuracy_metric = evaluate.load("accuracy")
429
+ f1_metric = evaluate.load("f1")
430
+
431
+ predictions_binary = [1 if pred.strip().lower() == ref[0].strip().lower() else 0
432
+ for pred, ref in zip(predictions, references)]
433
+ references_binary = [1] * len(predictions_binary)
434
+
435
+ accuracy_score = accuracy_metric.compute(predictions=predictions_binary, references=references_binary)
436
+ f1_score = f1_metric.compute(predictions=predictions_binary, references=references_binary, average="binary")
437
+
438
+ additional_metrics["accuracy"] = accuracy_score["accuracy"]
439
+ additional_metrics["f1_score"] = f1_score["f1"]
440
+ eval_prompt_text = f"""You are evaluating a model fine-tuned using Unsloth on the topic "{topic}" for {task_type} tasks.
441
+
442
+ **Your Task:** Provide an accurate, positive markdown evaluation report focusing on the model's strengths and capabilities based on your judgement and metrics.
443
+
444
+ **Test Results:**
445
+
446
+ Test Cases:
447
+ {json.dumps(test_cases, indent=2)}
448
+
449
+ Model Outputs:
450
+ {json.dumps(outputs, indent=2)}
451
+
452
+ **Metrics**
453
+ - BLEU Score: {bleu_score['bleu']:.4f}
454
+ - ROUGE-L Score: {rouge_score['rougeL']:.4f}
455
+ {f"- Accuracy: {additional_metrics.get('accuracy', 0):.4f}" if task_type.lower() in ["classification", "question-answering"] else ""}
456
+ {f"- F1 Score: {additional_metrics.get('f1_score', 0):.4f}" if task_type.lower() in ["classification", "question-answering"] else ""}
457
+
458
+ **Report Structure:**
459
+
460
+ ## πŸŽ‰ Evaluation Report
461
+
462
+ ### πŸ“Š Performance Overview
463
+ Create a comparison table with columns: Test Input | Expected Output | Model Output | βœ… Assessment
464
+
465
+ ### πŸš€ Metrics:
466
+ - Explain each evaluated metrics and categorize the performance based on average threshold
467
+ - Use percentages and numerical figures to stance yoir report
468
+
469
+ ### πŸ’ͺ Key Strengths
470
+ Highlight what the model does well:
471
+ - Accuracy and relevance
472
+ - Response coherence
473
+ - Task-specific capabilities
474
+ - Language quality
475
+
476
+
477
+ ### ✨ Conclusion
478
+ Summarize the model's overall performance and recommended use cases.
479
+
480
+
481
+ Now write the complete evaluation report following this structure. Be enthusiastic and highlight strengths! πŸŽ‰"""
482
+
483
+
484
+ eval_response = await eval_llm.ainvoke(eval_prompt_text)
485
+
486
+ return {
487
+ "status": "success",
488
+ "report": str(eval_response.content),
489
+ "test_cases": test_cases,
490
+ "model_outputs": outputs
491
+ }
492
+
493
+ except Exception as e:
494
+ return {
495
+ "status": "error",
496
+ "message": str(e),
497
+ "error_type": type(e).__name__
498
+ }
499
+
500
+
501
+
502
+
503
+
504
+
505
+
506
+ if __name__ == "__main__":
507
+ mcp.run()
508
+
509
+
510
+
511
+
512
+
513
+
514
+
515
+
static/fullnew.jpg ADDED
static/new.jpg ADDED

Git LFS Details

  • SHA256: c3af9b06d53a1d88930e230b0dae155cc9103dbb9cee8ab683126eb6a66aed40
  • Pointer size: 131 Bytes
  • Size of remote file: 231 kB