aedupuga commited on
Commit
86e9058
·
verified ·
1 Parent(s): a95a33b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +342 -244
app.py CHANGED
@@ -1,256 +1,354 @@
1
- import json
2
- import smolagents
3
- import llama_cpp
4
- import pandas as pd
5
- import numpy as np
6
- import gradio as gr
7
-
8
- # 1. LLM Model Definition
9
- MODEL_REPO = "bartowski/Qwen_Qwen3-4B-Instruct-2507-GGUF"
10
- MODEL_FILE = "Qwen_Qwen3-4B-Instruct-2507-Q4_K_M.gguf"
11
-
12
- llm = llama_cpp.Llama.from_pretrained(
13
- repo_id=MODEL_REPO,
14
- filename=MODEL_FILE,
15
- n_ctx=4096*4,
16
- n_threads=8,
17
- n_layers=-1,
18
- verbose=False
19
- )
20
-
21
- # 2. Data Loading and Processing
22
- SHEET_ID = '1h6gl0reY5iT2Q3_hb8pemP5Hi4OMDYcE'
23
- BASE_URL = f'https://docs.google.com/spreadsheets/d/{SHEET_ID}/gviz/tq?tqx=out:csv&gid='
24
 
25
- COURSES_GID = 1645090689 # GID for the 'Courses' tab
26
- TOOLS_GID = 2123942435 # GID for the 'Tools' tab
27
 
28
- courses_url = f'{BASE_URL}{COURSES_GID}'
29
- tools_url = f'{BASE_URL}{TOOLS_GID}'
30
-
31
- courses_df = pd.read_csv(courses_url)
32
- tools_df = pd.read_csv(tools_url)
33
-
34
- course_info = {
35
- str(row["Code"]): {
36
- "name": row["Name"],
37
- "description": row["Description"]
38
- }
39
- for _, row in courses_df.iterrows()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
 
42
- machine_to_course = {}
43
- for _, row in tools_df.iterrows():
44
- machine_name = row["Name"]
45
- required_course = row["Required Course"]
46
-
47
- if pd.isna(required_course):
48
- machine_to_course[machine_name] = "No_Training_Required"
49
- else:
50
- machine_to_course[machine_name] = str(int(required_course))
51
-
52
- # 3. LlamaCppModel and MachineTrainingTool Class Definitions
53
- class LlamaCppModel(smolagents.Model):
54
- """
55
- Thin wrapper for a llama.cpp OpenAI-compatible client.
56
- Pass in an object exposing `create_chat_completion(...)` (e.g., from llama_cpp).
57
- """
58
-
59
- def __init__(self, llm, model_id: str = "llama", **gen_defaults):
60
- super().__init__()
61
- self.llm = llm
62
- self.model_id = model_id
63
- self.gen_defaults = {"max_tokens": 4096, "temperature": 0.2}
64
- self.gen_defaults.update(gen_defaults)
65
-
66
- @staticmethod
67
- def _content_to_str(content) -> str:
68
- if content is None:
69
- return ""
70
- if isinstance(content, str):
71
- return content
72
-
73
- if isinstance(content, list):
74
- parts = []
75
- for p in content:
76
- if isinstance(p, str):
77
- parts.append(p)
78
- elif isinstance(p, dict):
79
- if p.get("type") == "text" and "text" in p:
80
- parts.append(str(p["text"]))
81
- elif "text" in p:
82
- parts.append(str(p["text"]))
83
- else:
84
- parts.append(json.dumps(p, ensure_ascii=False))
85
- else:
86
- parts.append(str(p))
87
- return "\n".join([s for s in parts if s])
88
-
89
- if isinstance(content, dict):
90
- if content.get("type") == "text" and "text" in content:
91
- return str(content["text"])
92
- if "content" in content and isinstance(content["content"], str):
93
- return content["content"]
94
- return json.dumps(content, ensure_ascii=False)
95
-
96
- return str(content)
97
-
98
- @staticmethod
99
- def _safe_get(obj, *keys, default=None):
100
- if isinstance(obj, dict):
101
- for k in keys:
102
- if k in obj:
103
- return obj[k]
104
- return default
105
- for k in keys:
106
- if hasattr(obj, k):
107
- return getattr(obj, k)
108
- return default
109
-
110
- def _to_openai_messages(self, messages: list[smolagents.ChatMessage]) -> list[dict]:
111
- oa = []
112
- for m in messages:
113
- role = getattr(m, "role", None) or (m.get("role") if isinstance(m, dict) else None) or "user"
114
- content = getattr(m, "content", None) or (m.get("content") if isinstance(m, dict) else None)
115
- text = self._content_to_str(content)
116
-
117
- images = getattr(m, "images", None) or (m.get("images") if isinstance(m, dict) else None)
118
- if images:
119
- text = (text + f"\n[Note: {len(images)} image(s) omitted]").strip()
120
-
121
- oa.append({"role": role, "content": text})
122
- return oa
123
-
124
- def _from_openai_message(self, msg) -> smolagents.ChatMessage:
125
- role = self._safe_get(msg, "role", default="assistant")
126
- content = self._safe_get(msg, "content", default="")
127
- return smolagents.ChatMessage(role=role, content=content)
128
-
129
- def generate(
130
- self,
131
- messages: list[smolagents.ChatMessage],
132
- stop_sequences: list[str] | None = None,
133
- response_format: dict[str, str] | None = None,
134
- tools_to_call_from: list[smolagents.Tool] | None = None,
135
- **kwargs,
136
- ) -> smolagents.ChatMessage:
137
- oa_msgs = self._to_openai_messages(messages)
138
-
139
- params = dict(self.gen_defaults)
140
- params.update(kwargs)
141
- if stop_sequences:
142
- params["stop"] = stop_sequences
143
- if response_format:
144
- params["response_format"] = response_format
145
-
146
- resp = self.llm.create_chat_completion(
147
- model=self.model_id,
148
- messages=oa_msgs,
149
- **params,
150
- )
151
-
152
- choices = self._safe_get(resp, "choices", default=[])
153
- if not choices:
154
- text = self._safe_get(resp, "content", default=str(resp))
155
- return smolagents.ChatMessage(role="assistant", content=text)
156
-
157
- first = choices[0]
158
- message = self._safe_get(first, "message", default={})
159
- return self._from_openai_message(message)
160
-
161
- class MachineTrainingTool(smolagents.tools.Tool):
162
- name = "get_machine_training_info"
163
- description = (
164
- "Retrieves training information for a specific machine. The `machine_name` argument should exactly match the machine's name as listed in the system (e.g., 'Laser Cutters', '3D Printers')."
165
  )
166
- inputs = {
167
- "machine_name": {"type": "string", "description": "Name of the machine for which to retrieve training information"},
168
- }
169
- output_type = "string"
170
-
171
- def forward(self, machine_name: str) -> str:
172
- if machine_name in machine_to_course:
173
- course_code = machine_to_course[machine_name]
174
- if course_code in course_info:
175
- course_name = course_info[course_code]['name']
176
- return f"For {machine_name}, the required training is: '{course_name}' (Course Code: {course_code})."
177
- else:
178
- return f"No detailed course information found for course code '{course_code}' associated with {machine_name}."
179
- else:
180
- return f"No specific training information available for machine: {machine_name}."
181
-
182
- # 4. Agent Instantiation
183
- machine_training_tool_instance = MachineTrainingTool()
184
-
185
- llama_cpp_model_instance = LlamaCppModel(llm)
186
- llama_cpp_model_instance.gen_defaults['response_format'] = {'type': 'json_object'}
187
 
188
- from smolagents.agents import ToolCallingAgent
189
-
190
- machine_agent = ToolCallingAgent(
191
- model=llama_cpp_model_instance,
192
- name="MachineAgent",
193
- instructions=(
194
- "Your ONLY purpose is to call the 'get_machine_training_info' tool ONCE per user query. "
195
- "You MUST then output the EXACT string provided by the tool's observation. "
196
- "You MUST NOT add any text before or after the tool's observation. "
197
- "You MUST NOT summarize, paraphrase, or change the tool's output in any way. "
198
- "You MUST NOT call any other tool, especially 'final_answer'. "
199
- "The 'machine_name' argument MUST be an exact match from the available machine list."
200
- ),
201
- tools=[machine_training_tool_instance],
202
- verbosity_level=2
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  )
204
 
205
- # 5. Gradio Interface Code
206
- with gr.Blocks(title="Techspark Machine Training Agent") as demo:
207
- gr.Markdown("## Techspark Machine Training Agent — Custom Tool Selection (smolagents + llama.cpp)")
208
- chat = gr.Chatbot(height=420)
209
- inp = gr.Textbox(
210
- placeholder="Ask about machine training (e.g., 'What training do I need for the Laser Cutters?').",
211
- label="Your question"
212
- )
213
-
214
- def respond(message, history):
215
- try:
216
- result_object = machine_agent.run(message, return_full_result=True)
217
- tool_observation = None
218
-
219
- if isinstance(result_object, smolagents.RunResult):
220
- for step_dict in result_object.steps:
221
- if isinstance(step_dict, dict):
222
- if 'tool_calls' in step_dict and step_dict['tool_calls']:
223
- for tool_call_info in step_dict['tool_calls']:
224
- if tool_call_info.get('function') and tool_call_info['function'].get('name') == 'get_machine_training_info':
225
- if 'observations' in step_dict:
226
- tool_observation = step_dict['observations']
227
- break
228
- if tool_observation:
229
- break
230
-
231
- if tool_observation:
232
- out = tool_observation
233
- else:
234
- out = result_object.text if hasattr(result_object, 'text') else str(result_object)
235
-
236
- except Exception as e:
237
- out = f"[Error] {e}"
238
-
239
- history = (history or []) + [(message, out)]
240
- return "", history
241
 
242
- gr.Examples(
243
- fn=respond,
244
- examples=[
245
- "What training do I need for the Laser Cutters?",
246
- "What are the training requirements for the 3D Printer?",
247
- "Can you tell me about training for Metal CNC?"
248
- ],
249
- inputs=[inp],
250
- outputs=[chat]
251
  )
252
 
253
- inp.submit(respond, [inp, chat], [inp, chat])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
- # 6. Modified demo.launch() for Hugging Face Spaces
256
- demo.launch(server_name="0.0.0.0", server_port=7860, debug=False)
 
1
+ # --- TechSpark Courses Q&A: lightweight Gradio chat with a tiny LLM ---
2
+ # Works in Google Colab. Hard-coded course data, no uploads required.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ !pip -q install gradio==4.44.0 transformers==4.44.2 rapidfuzz==3.9.6
 
5
 
6
+ import gradio as gr
7
+ from rapidfuzz import process, fuzz
8
+
9
+ # Optional tiny LLM (fast): FLAN-T5-small
10
+ # If it fails to load (e.g., offline), we’ll just return the raw answer.
11
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
12
+
13
+ def load_llm():
14
+ try:
15
+ tok = AutoTokenizer.from_pretrained("google/flan-t5-small")
16
+ mdl = AutoModelForSeq2SeqLM.from_pretrained("google/flan-t5-small")
17
+ return tok, mdl
18
+ except Exception:
19
+ return None, None
20
+
21
+ TOK, MDL = load_llm()
22
+
23
+ def llm_paraphrase(text: str) -> str:
24
+ if not (TOK and MDL):
25
+ return text
26
+ prompt = f"Paraphrase clearly and concisely:\n{text}"
27
+ inputs = TOK(prompt, return_tensors="pt")
28
+ out_ids = MDL.generate(**inputs, max_new_tokens=128)
29
+ return TOK.decode(out_ids[0], skip_special_tokens=True)
30
+
31
+ # ------------------------
32
+ # HARD-CODED COURSE DATA
33
+ # (Copied from your CSV: /mnt/data/TechSpark.xlsx - Courses.csv)
34
+ # Columns: Name, Code, Description, Units, Length (Weeks),
35
+ # Laser Cutting, Wood Working, Wood CNC, Metal Machining, Metal CNC,
36
+ # 3D Printer, Welding, Electronics
37
+ # Note: Some descriptions were truncated with "..." in the source CSV; left as-is.
38
+ # ------------------------
39
+
40
+ COURSES = [
41
+ {
42
+ "Name": "Modern Making",
43
+ "Code": 24104,
44
+ "Description": "This course teaches the fundamental skills needed to plan, devel...bricating with 3D printers, and physical computing with Arduino.",
45
+ "Units": 3,
46
+ "Length (Weeks)": 7,
47
+ "Laser Cutting": 3,
48
+ "Wood Working": 0,
49
+ "Wood CNC": 0,
50
+ "Metal Machining": 0,
51
+ "Metal CNC": 0,
52
+ "3D Printer": 3,
53
+ "Welding": 0,
54
+ "Electronics": 3
55
+ },
56
+ {
57
+ "Name": "Laser Machine Training",
58
+ "Code": 24105,
59
+ "Description": "This is a course that allows students to work at their own pace....ed to use the laser cutting and engraving machines at TechSpark.",
60
+ "Units": 0,
61
+ "Length (Weeks)": 2,
62
+ "Laser Cutting": 2,
63
+ "Wood Working": 0,
64
+ "Wood CNC": 0,
65
+ "Metal Machining": 0,
66
+ "Metal CNC": 0,
67
+ "3D Printer": 0,
68
+ "Welding": 0,
69
+ "Electronics": 0
70
+ },
71
+ {
72
+ "Name": "Intro to Manual Machining",
73
+ "Code": 24200,
74
+ "Description": "This course teaches safe operation of manual machining equipment...gn projects, research equipment, and extracurricular activities.",
75
+ "Units": 1,
76
+ "Length (Weeks)": 7,
77
+ "Laser Cutting": 0,
78
+ "Wood Working": 0,
79
+ "Wood CNC": 0,
80
+ "Metal Machining": 0,
81
+ "Metal CNC": 0,
82
+ "3D Printer": 0,
83
+ "Welding": 0,
84
+ "Electronics": 0
85
+ },
86
+ {
87
+ "Name": "Project Fabrication and Assembly",
88
+ "Code": 24201,
89
+ "Description": "This course teaches the fundamental skills of fabrication and as...asses and is a portal (prerequisite) to other TechSpark courses.",
90
+ "Units": 1,
91
+ "Length (Weeks)": 7,
92
+ "Laser Cutting": 2,
93
+ "Wood Working": 1,
94
+ "Wood CNC": 0,
95
+ "Metal Machining": 0,
96
+ "Metal CNC": 0,
97
+ "3D Printer": 3,
98
+ "Welding": 0,
99
+ "Electronics": 3
100
+ },
101
+ {
102
+ "Name": "Machine Shop Principles",
103
+ "Code": 24203,
104
+ "Description": "This course teaches the safe operation of manual machining equip...course is required to use the student machine shop at TechSpark.",
105
+ "Units": 3,
106
+ "Length (Weeks)": 7,
107
+ "Laser Cutting": 0,
108
+ "Wood Working": 0,
109
+ "Wood CNC": 0,
110
+ "Metal Machining": 3,
111
+ "Metal CNC": 0,
112
+ "3D Printer": 0,
113
+ "Welding": 0,
114
+ "Electronics": 0
115
+ },
116
+ {
117
+ "Name": "Metal Jewelry",
118
+ "Code": 24204,
119
+ "Description": "This course teaches introductory-level metal jewelry fabrication...This course is required to use the hot metals room at TechSpark.",
120
+ "Units": 2,
121
+ "Length (Weeks)": 7,
122
+ "Laser Cutting": 0,
123
+ "Wood Working": 0,
124
+ "Wood CNC": 0,
125
+ "Metal Machining": 1,
126
+ "Metal CNC": 1,
127
+ "3D Printer": 0,
128
+ "Welding": 2,
129
+ "Electronics": 0
130
+ },
131
+ {
132
+ "Name": "Welding",
133
+ "Code": 24205,
134
+ "Description": "This course teaches the safe operation of welding equipment thro...is course is required to use the welding equipment at TechSpark.",
135
+ "Units": 2,
136
+ "Length (Weeks)": 7,
137
+ "Laser Cutting": 0,
138
+ "Wood Working": 0,
139
+ "Wood CNC": 0,
140
+ "Metal Machining": 1,
141
+ "Metal CNC": 1,
142
+ "3D Printer": 0,
143
+ "Welding": 3,
144
+ "Electronics": 0
145
+ },
146
+ {
147
+ "Name": "Wood Shop Principles",
148
+ "Code": 24206,
149
+ "Description": "This course teaches the safe operation of wood working equipment...is course is required to use the student wood shop at TechSpark.",
150
+ "Units": 3,
151
+ "Length (Weeks)": 7,
152
+ "Laser Cutting": 0,
153
+ "Wood Working": 3,
154
+ "Wood CNC": 0,
155
+ "Metal Machining": 0,
156
+ "Metal CNC": 0,
157
+ "3D Printer": 0,
158
+ "Welding": 0,
159
+ "Electronics": 0
160
+ },
161
+ {
162
+ "Name": "Wood Shop CNC Router",
163
+ "Code": 24207,
164
+ "Description": "This course builds upon previous skills taught in TechSpark's wo...o use the CNC wood router in the student wood shop at TechSpark.",
165
+ "Units": 3,
166
+ "Length (Weeks)": 7,
167
+ "Laser Cutting": 0,
168
+ "Wood Working": 2,
169
+ "Wood CNC": 3,
170
+ "Metal Machining": 0,
171
+ "Metal CNC": 1,
172
+ "3D Printer": 0,
173
+ "Welding": 0,
174
+ "Electronics": 0
175
+ },
176
+ {
177
+ "Name": "Machine Shop CNC Milling",
178
+ "Code": 24300,
179
+ "Description": "This course builds upon previous skills taught in TechSpark's ma...e CNC milling machines in the student machine shop at TechSpark.",
180
+ "Units": 2,
181
+ "Length (Weeks)": 7,
182
+ "Laser Cutting": 0,
183
+ "Wood Working": 0,
184
+ "Wood CNC": 1,
185
+ "Metal Machining": 0,
186
+ "Metal CNC": 3,
187
+ "3D Printer": 0,
188
+ "Welding": 0,
189
+ "Electronics": 0
190
+ }
191
+ ]
192
+
193
+
194
+ # ------------------------
195
+ # Simple retrieval helpers
196
+ # ------------------------
197
+
198
+ FIELD_ALIASES = {
199
+ "units": "Units",
200
+ "weeks": "Length (Weeks)",
201
+ "length": "Length (Weeks)",
202
+ "description": "Description",
203
+ "laser": "Laser Cutting",
204
+ "laser cutting": "Laser Cutting",
205
+ "wood": "Wood Working",
206
+ "woodworking": "Wood Working",
207
+ "wood cnc": "Wood CNC",
208
+ "metal": "Metal Machining",
209
+ "metal machining": "Metal Machining",
210
+ "metal cnc": "Metal CNC",
211
+ "3d": "3D Printer",
212
+ "3d printing": "3D Printer",
213
+ "printer": "3D Printer",
214
+ "weld": "Welding",
215
+ "welding": "Welding",
216
+ "electronics": "Electronics",
217
+ "code": "Code",
218
+ "name": "Name"
219
  }
220
 
221
+ COURSE_NAMES = [c["Name"] for c in COURSES]
222
+ COURSE_CODES = [str(c["Code"]) for c in COURSES]
223
+
224
+ def find_course(query: str):
225
+ # Try to match by name (fuzzy) or code (exact substring)
226
+ best_name = process.extractOne(query, COURSE_NAMES, scorer=fuzz.WRatio)
227
+ code_hits = [c for c in COURSES if str(c["Code"]) in query.replace(" ", "")]
228
+ if best_name and best_name[1] >= 70:
229
+ for c in COURSES:
230
+ if c["Name"] == best_name[0]:
231
+ return c
232
+ if code_hits:
233
+ return code_hits[0]
234
+ return None
235
+
236
+ def filter_by_skill(query: str):
237
+ # Return courses that have >0 level for any skill mentioned
238
+ hits = []
239
+ for key, field in FIELD_ALIASES.items():
240
+ if field in ["Units", "Length (Weeks)", "Description", "Code", "Name"]:
241
+ continue
242
+ if key in query.lower():
243
+ for c in COURSES:
244
+ try:
245
+ if c.get(field, 0) and int(c.get(field, 0)) > 0:
246
+ hits.append((field, c))
247
+ except Exception:
248
+ pass
249
+ return hits
250
+
251
+ def reply_for_course(c: dict, query: str) -> str:
252
+ # If user asked a specific field, show that; else show a compact summary
253
+ lower = query.lower()
254
+ # Check if they asked for a specific attribute
255
+ for key, field in FIELD_ALIASES.items():
256
+ if key in lower and field in c:
257
+ return f"{c['Name']} — {field}: {c[field]}"
258
+ # Default compact card
259
+ skills = ["Laser Cutting","Wood Working","Wood CNC","Metal Machining","Metal CNC","3D Printer","Welding","Electronics"]
260
+ taught = [s for s in skills if int(c.get(s,0))>0]
261
+ taught_str = ", ".join(taught) if taught else "General skills"
262
+ return (
263
+ f"{c['Name']} (Code {c['Code']})\n"
264
+ f"Units: {c['Units']} | Length: {c['Length (Weeks)']} weeks\n"
265
+ f"Focus: {taught_str}\n"
266
+ f"Description: {c['Description']}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
+ def list_all_courses():
270
+ return "Courses:\n" + "\n".join([f"- {c['Name']} (Code {c['Code']})" for c in COURSES])
271
+
272
+ def list_by_skill(hits):
273
+ if not hits:
274
+ return None
275
+ # Group by skill field
276
+ by = {}
277
+ for field, c in hits:
278
+ by.setdefault(field, []).append(c)
279
+ lines = []
280
+ for field, cs in by.items():
281
+ lines.append(f"{field} courses:")
282
+ for c in cs:
283
+ lines.append(f"- {c['Name']} (Code {c['Code']})")
284
+ return "\n".join(lines)
285
+
286
+ # ------------------------
287
+ # Chat handler
288
+ # ------------------------
289
+
290
+ HELP_TEXT = (
291
+ "You can ask:\n"
292
+ "• “List all courses”\n"
293
+ "• “What are the units for Modern Making?”\n"
294
+ "• “Which classes teach welding?”\n"
295
+ "• “What is Code 24205?” or “Tell me about Intro to CNC Machining”\n"
296
+ "• “Which courses cover laser cutting or 3D printing?”\n"
297
  )
298
 
299
+ def answer_fn(message, history):
300
+ q = (message or "").strip()
301
+ if not q:
302
+ return HELP_TEXT
303
+
304
+ # Quick intents
305
+ if "list" in q.lower() and "course" in q.lower():
306
+ ans = list_all_courses()
307
+ return llm_paraphrase(ans)
308
+
309
+ # Skill-filter intent
310
+ skill_hits = filter_by_skill(q)
311
+ skill_resp = list_by_skill(skill_hits)
312
+ if skill_resp:
313
+ return llm_paraphrase(skill_resp)
314
+
315
+ # Single-course intent
316
+ c = find_course(q)
317
+ if c:
318
+ ans = reply_for_course(c, q)
319
+ return llm_paraphrase(ans)
320
+
321
+ # Fallback: nearest name suggestion
322
+ best = process.extractOne(q, COURSE_NAMES, scorer=fuzz.WRatio)
323
+ if best and best[1] >= 55:
324
+ suggestion = best[0]
325
+ return llm_paraphrase(
326
+ f"I couldn't find an exact match. Did you mean “{suggestion}”? "
327
+ f"Try asking: ‘Tell me about {suggestion}’ or ‘What are the units for {suggestion}?’\n\n{HELP_TEXT}"
328
+ )
 
 
 
 
 
 
329
 
330
+ # Final fallback
331
+ return llm_paraphrase(
332
+ "I couldn't match that to a TechSpark course. "
333
+ "Try mentioning a course name or code, or a skill like welding, laser cutting, or 3D printing.\n\n" + HELP_TEXT
 
 
 
 
 
334
  )
335
 
336
+ # ------------------------
337
+ # Gradio UI
338
+ # ------------------------
339
+
340
+ demo = gr.ChatInterface(
341
+ answer_fn,
342
+ title="TechSpark Courses Assistant",
343
+ description="Ask about TechSpark courses by name, code, or skill. (Tiny LLM paraphrase enabled for clarity.)",
344
+ examples=[
345
+ "List all courses",
346
+ "What are the units for Modern Making?",
347
+ "Which courses teach welding?",
348
+ "Tell me about Intro to CNC Machining",
349
+ "What is Code 24301?",
350
+ "Which courses include laser cutting?"
351
+ ],
352
+ )
353
 
354
+ demo.launch()