Sathvika-Alla commited on
Commit
46c9c21
·
verified ·
1 Parent(s): d02954f

Upload 2 files

Browse files
Files changed (2) hide show
  1. RagImplementation.py +758 -0
  2. requirements.txt +7 -0
RagImplementation.py ADDED
@@ -0,0 +1,758 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ import gradio as gr
5
+ from transformers import pipeline, AutoTokenizer
6
+ from langchain_core.documents import Document
7
+ from langchain_huggingface import HuggingFaceEmbeddings
8
+ from langchain_community.vectorstores import FAISS
9
+ from langchain_core.prompts import ChatPromptTemplate
10
+ from typing import List, TypedDict
11
+ from langgraph.graph import StateGraph, START
12
+ from dotenv import load_dotenv
13
+
14
+ from transformers import GPT2LMHeadModel, GPT2Tokenizer
15
+
16
+ # Load the model and tokenizer
17
+ llm_model = GPT2LMHeadModel.from_pretrained("./results")
18
+ llm_tokenizer = GPT2Tokenizer.from_pretrained("./results")
19
+ llm_tokenizer.pad_token = llm_tokenizer.eos_token
20
+
21
+
22
+ # --- Configuration ---
23
+
24
+ load_dotenv()
25
+ os.environ["HUGGINGFACEHUB_API_TOKEN"] = os.getenv("HUGGINGFACEHUB_API_TOKEN")
26
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
27
+
28
+ file_path = "./converters_with_links_and_pricelist.json"
29
+ try:
30
+ with open(file_path, 'r', encoding='utf-8') as f:
31
+ product_data = json.load(f)
32
+ except Exception as e:
33
+ print(f"Error loading product data: {e}")
34
+ product_data = {}
35
+
36
+ tokenizer = AutoTokenizer.from_pretrained("facebook/blenderbot-400M-distill")
37
+ tokenizer.truncation_side = "left"
38
+ max_length = tokenizer.model_max_length
39
+
40
+ docs = [Document(page_content=str(value), metadata={"source": key}) for key, value in product_data.items()]
41
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2")
42
+ vector_store = FAISS.from_documents(docs, embeddings)
43
+ chatbot = pipeline("text-generation", model="facebook/blenderbot-400M-distill")
44
+
45
+ # --- Helper Functions ---
46
+
47
+ def parse_float(s):
48
+ try:
49
+ if isinstance(s, (list, tuple)):
50
+ s = s[0]
51
+ return float(str(s).replace(',', '.').strip())
52
+ except Exception:
53
+ return float('inf')
54
+
55
+ def parse_price(val):
56
+ if isinstance(val, float) or isinstance(val, int):
57
+ return float(val)
58
+ try:
59
+ return float(str(val).replace(',', '.'))
60
+ except Exception:
61
+ return float('inf')
62
+
63
+ def normalize_artnr(artnr):
64
+ try:
65
+ return str(int(float(artnr)))
66
+ except Exception:
67
+ return str(artnr)
68
+
69
+ def normalize_ip(ip):
70
+ if isinstance(ip, (int, float)):
71
+ return f"IP{int(ip)}"
72
+ elif isinstance(ip, str):
73
+ ip_part = ip.replace("IP", "").split(".")[0]
74
+ return f"IP{ip_part}"
75
+ else:
76
+ return "N/A"
77
+
78
+ def get_product_by_artnr(artnr, tech_info):
79
+ artnr_str = normalize_artnr(artnr)
80
+ for value in tech_info.values():
81
+ if normalize_artnr(value.get("ARTNR", "")) == artnr_str:
82
+ return value
83
+ return None
84
+
85
+ def extract_converter_and_lamp(user_message: str):
86
+ match = re.search(r"how many (\w+) lamps?.*converter (\d+)", user_message.lower())
87
+ if match:
88
+ lamp_name = match.group(1)
89
+ converter_number = match.group(2)
90
+ return lamp_name, converter_number
91
+ return None, None
92
+
93
+ def get_technical_fit_info(product_data: dict) -> dict:
94
+ results = {}
95
+ for key, value in product_data.items():
96
+ results[key] = {
97
+ "TYPE": value.get("TYPE", "N/A"),
98
+ "ARTNR": value.get("ARTNR", "N/A"),
99
+ "CONVERTER DESCRIPTION": value.get("CONVERTER DESCRIPTION:", "N/A"),
100
+ "STRAIN RELIEF": value.get("STRAIN RELIEF", "N/A"),
101
+ "LOCATION": value.get("LOCATION", "N/A"),
102
+ "DIMMABILITY": value.get("DIMMABILITY", "N/A"),
103
+ "EFFICIENCY": value.get("EFFICIENCY @full load", "N/A"),
104
+ "OUTPUT VOLTAGE": value.get("OUTPUT VOLTAGE (V)", "N/A"),
105
+ "INPUT VOLTAGE": value.get("NOM. INPUT VOLTAGE (V)", "N/A"),
106
+ "SIZE": value.get("SIZE: L*B*H (mm)", "N/A"),
107
+ "WEIGHT": value.get("Gross Weight", "N/A"),
108
+ "Listprice": value.get("Listprice", "N/A"),
109
+ "LAMPS": value.get("lamps", {}),
110
+ "PDF_LINK": value.get("pdf_link", "N/A"),
111
+ "IP": value.get("IP", "N/A"),
112
+ "CLASS": value.get("CLASS", "N/A"),
113
+ "LifeCycle": value.get("LifeCycle", "N/A"),
114
+ "Name": value.get("Name", "N/A"),
115
+ }
116
+ return results
117
+
118
+ tech_info = get_technical_fit_info(product_data)
119
+
120
+ def recommend_converters_for_lamp(lamp_query, tech_info):
121
+ def normalize(s):
122
+ # Lowercase, remove commas and dots, strip spaces
123
+ return s.lower().replace(",", "").replace(".", "").strip()
124
+ norm_query = normalize(lamp_query)
125
+ query_words = set(norm_query.split())
126
+ results = []
127
+ for v in tech_info.values():
128
+ lamps = v.get("LAMPS", {})
129
+ for lamp_name, lamp_data in lamps.items():
130
+ norm_lamp = normalize(lamp_name)
131
+ lamp_words = set(norm_lamp.split())
132
+ # Match if all query words are in lamp name OR query is a substring of lamp name OR lamp name is a substring of query
133
+ if (
134
+ query_words.issubset(lamp_words)
135
+ or norm_query in norm_lamp
136
+ or norm_lamp in norm_query
137
+ ):
138
+ min_val = lamp_data.get("min", "N/A")
139
+ max_val = lamp_data.get("max", "N/A")
140
+ desc = v.get("CONVERTER DESCRIPTION", v.get("CONVERTER DESCRIPTION:", "N/A")).strip()
141
+ artnr = v.get("ARTNR", "N/A")
142
+ results.append(f"{desc} (ARTNR: {int(float(artnr)) if artnr != 'N/A' else 'N/A'}), supports {min_val} to {max_val} x \"{lamp_name}\"")
143
+ if not results:
144
+ return f"Sorry, I couldn't find a converter for '{lamp_query}'."
145
+ return "Recommended converters:\n" + "\n".join(results)
146
+
147
+
148
+ def get_lamp_quantity(converter_number: str, lamp_name: str, tech_info: dict) -> str:
149
+ v = get_product_by_artnr(converter_number, tech_info)
150
+ if not v:
151
+ return f"Sorry, I could not find converter {converter_number}."
152
+ for lamp_key, lamp_vals in v["LAMPS"].items():
153
+ if lamp_name.lower() in lamp_key.lower():
154
+ min_val = lamp_vals.get("min", "N/A")
155
+ max_val = lamp_vals.get("max", "N/A")
156
+ if min_val == max_val:
157
+ return f"You can use {min_val} {lamp_key} lamp(s) with converter {converter_number}."
158
+ else:
159
+ return f"You can use between {min_val} and {max_val} {lamp_key} lamp(s) with converter {converter_number}."
160
+ return f"Sorry, no data found for lamp '{lamp_name}' with converter {converter_number}."
161
+
162
+ def get_recommended_converter_any(user_message, tech_info):
163
+ match = re.search(r'(\d+)\s*x\s*([\w\d\s\-,.*]+)', user_message, re.IGNORECASE)
164
+ if not match:
165
+ return None
166
+ num_lamps = int(match.group(1))
167
+ lamp_query = match.group(2).strip().lower()
168
+ candidates = []
169
+ for v in tech_info.values():
170
+ for lamp, vals in v["LAMPS"].items():
171
+ lamp_norm = lamp.lower().replace(',', '.')
172
+ if all(word in lamp_norm for word in lamp_query.split()):
173
+ max_lamps = float(str(vals.get("max", 0)).replace(',', '.'))
174
+ if max_lamps >= num_lamps:
175
+ candidates.append((v, lamp, max_lamps))
176
+ if not candidates:
177
+ return f"Sorry, I couldn't find a converter that supports {num_lamps}x {lamp_query.title()}."
178
+ else:
179
+ return "\n".join([
180
+ f"You can use {v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])}) for {num_lamps}x {lamp_query.title()} (max supported: {max_lamps} for '{lamp}')."
181
+ for v, lamp, max_lamps in candidates
182
+ ])
183
+
184
+ def answer_technical_question(question: str, tech_info: dict) -> str:
185
+ q = question.lower()
186
+
187
+ # --- Lamp-only queries like "Which converter should I use for 'LEDLINE medium power 9.6W' strips?" ---
188
+ lamp_match = re.search(
189
+ r'(?:for|recommend|use|need)[\s:]*["“”\']?([a-zA-Z0-9 ,.\-]+w)[\s"”\']*(?:strips?|ledline|lamps?)?', q
190
+ )
191
+ if lamp_match:
192
+ lamp_query = lamp_match.group(1).strip()
193
+ result = recommend_converters_for_lamp(lamp_query, tech_info)
194
+ if result and "couldn't find" not in result:
195
+ return result
196
+
197
+ # Fallback: match any lamp in the database if all its words are in the question
198
+ def normalize_lamp_string(s):
199
+ return set(s.lower().replace(",", "").replace(".", "").split())
200
+ q_words = set(q.replace(",", "").replace(".", "").split())
201
+ for v in tech_info.values():
202
+ for lamp_name in v.get("LAMPS", {}):
203
+ lamp_words = normalize_lamp_string(lamp_name)
204
+ if lamp_words and lamp_words.issubset(q_words):
205
+ result = recommend_converters_for_lamp(lamp_name, tech_info)
206
+ if result and "couldn't find" not in result:
207
+ return result
208
+
209
+
210
+
211
+ def answer_technical_question(question: str, tech_info: dict) -> str:
212
+ q = question.lower()
213
+ # Try to extract lamp name after 'for', 'recommend', 'use', etc.
214
+ lamp_match = re.search(
215
+ r'(?:for|recommend|use|need)[\s:]*["“”\']?([a-zA-Z0-9 ,.\-]+w)[\s"”\']*(?:strips?|ledline|lamps?)?', q
216
+ )
217
+ if lamp_match:
218
+ lamp_query = lamp_match.group(1).strip()
219
+ result = recommend_converters_for_lamp(lamp_query, tech_info)
220
+ if result and "couldn't find" not in result:
221
+ return result
222
+
223
+ # Fallback: match any lamp in the database if all its words are in the question
224
+ def normalize_lamp_string(s):
225
+ return set(s.lower().replace(",", "").replace(".", "").split())
226
+ q_words = set(q.replace(",", "").replace(".", "").split())
227
+ for v in tech_info.values():
228
+ for lamp_name in v.get("LAMPS", {}):
229
+ lamp_words = normalize_lamp_string(lamp_name)
230
+ if lamp_words and lamp_words.issubset(q_words):
231
+ result = recommend_converters_for_lamp(lamp_name, tech_info)
232
+ if result and "couldn't find" not in result:
233
+ return result
234
+
235
+ # Efficiency at full load for all converters
236
+ if "efficiency at full load for each converter" in q or "efficiency for each converter" in q:
237
+ result = []
238
+ for v in tech_info.values():
239
+ description = v.get("CONVERTER DESCRIPTION", "N/A").strip()
240
+ efficiency = v.get("EFFICIENCY", "N/A")
241
+ result.append(f"{description}: {efficiency}")
242
+ return "\n".join(result)
243
+ # Generalized lamp fit for any type in the database
244
+ if re.search(r"\d+\s*x\s*[\w\d\s\-,.*]+", q):
245
+ result = get_recommended_converter_any(question, tech_info)
246
+ if result:
247
+ return result
248
+ # Outdoor installation
249
+ if "outdoor" in q:
250
+ return "\n".join([f"{v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])})"
251
+ for v in tech_info.values()
252
+ if "outdoor" in v["LOCATION"].lower() or "in&outdoor" in v["LOCATION"].lower()])
253
+ # Most efficient converter for any type
254
+ if "most efficient" in q:
255
+ type_match = re.search(r'(\d+\s*v|\d+\s*ma)', q)
256
+ if type_match:
257
+ search_type = type_match.group(1).replace(' ', '').lower()
258
+ candidates = [
259
+ v for v in tech_info.values()
260
+ if search_type in str(v["TYPE"]).replace(' ', '').lower()
261
+ and str(v.get("EFFICIENCY", v.get("EFFICIENCY @full load", ""))).replace(',', '.').replace('.', '').isdigit()
262
+ ]
263
+ if not candidates:
264
+ return f"No {search_type.upper()} converters found."
265
+ best = max(
266
+ candidates,
267
+ key=lambda x: float(str(x.get("EFFICIENCY", x.get("EFFICIENCY @full load", "0"))).replace(',', '.'))
268
+ )
269
+ desc = best.get("CONVERTER DESCRIPTION", best.get("CONVERTER DESCRIPTION:", "N/A")).strip()
270
+ artnr = int(float(best.get("ARTNR", "N/A"))) if best.get("ARTNR") else "N/A"
271
+ eff = best.get("EFFICIENCY", best.get("EFFICIENCY @full load", "N/A"))
272
+ return f"The most efficient {search_type.upper()} converter is {desc} (ARTNR: {artnr}) with efficiency {eff}."
273
+ else:
274
+ # fallback: show most efficient overall
275
+ candidates = [
276
+ v for v in tech_info.values()
277
+ if str(v.get("EFFICIENCY", v.get("EFFICIENCY @full load", ""))).replace(',', '.').replace('.', '').isdigit()
278
+ ]
279
+ if not candidates:
280
+ return "No converters with efficiency data found."
281
+ best = max(
282
+ candidates,
283
+ key=lambda x: float(str(x.get("EFFICIENCY", x.get("EFFICIENCY @full load", "0"))).replace(',', '.'))
284
+ )
285
+ desc = best.get("CONVERTER DESCRIPTION", best.get("CONVERTER DESCRIPTION:", "N/A")).strip()
286
+ artnr = int(float(best.get("ARTNR", "N/A"))) if best.get("ARTNR") else "N/A"
287
+ eff = best.get("EFFICIENCY", best.get("EFFICIENCY @full load", "N/A"))
288
+ return f"The most efficient converter overall is {desc} (ARTNR: {artnr}) with efficiency {eff}."
289
+
290
+ # Dimming support
291
+ if "dimmable" in q or "dimming" in q or "1-10v" in q or "dali" in q or "casambi" in q or "touchdim" in q:
292
+ type_match = re.search(r'(\d+\s*v|\d+\s*ma)', q)
293
+ type_query = type_match.group(1).replace(" ", "").lower() if type_match else None
294
+ results = []
295
+ for v in tech_info.values():
296
+ type_str = str(v.get("TYPE", "")).lower().replace(" ", "")
297
+ dim = v.get("DIMMABILITY", "").upper()
298
+ if ("DIM" in dim or "1-10V" in dim or "DALI" in dim or "CASAMBI" in dim or "TOUCHDIM" in dim) and (not type_query or type_query in type_str):
299
+ desc = v.get("CONVERTER DESCRIPTION", v.get("CONVERTER DESCRIPTION:", "N/A")).strip()
300
+ artnr = int(float(v.get("ARTNR", "N/A"))) if v.get("ARTNR") else "N/A"
301
+ results.append(f"{desc} (ARTNR: {artnr}), Dimming: {dim}")
302
+ if not results:
303
+ return f"No{' ' + type_query.upper() if type_query else ''} converters with dimming support found."
304
+ return "\n".join(results)
305
+
306
+ # Strain relief
307
+ if "strain relief" in q:
308
+ candidates = [v for v in tech_info.values() if v["STRAIN RELIEF"].lower() == "yes"]
309
+ yesno = "Yes" if candidates else "No"
310
+ details = "\n".join([f"{v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])})" for v in candidates])
311
+ return f"{yesno}. " + (details if details else "")
312
+ # Input voltage range for each converter
313
+ if "input voltage range for each converter" in q or "input voltage range" in q and "each" in q:
314
+ result = []
315
+ for v in tech_info.values():
316
+ description = v.get("CONVERTER DESCRIPTION", "N/A").strip()
317
+ input_voltage = v.get("INPUT VOLTAGE", "N/A")
318
+ result.append(f"{description}: {input_voltage}")
319
+ return "\n".join(result)
320
+
321
+ # Comparison
322
+ if "compare" in q:
323
+ numbers = re.findall(r'\d+', question)
324
+ if len(numbers) >= 2:
325
+ a = get_product_by_artnr(numbers[0], tech_info)
326
+ b = get_product_by_artnr(numbers[1], tech_info)
327
+ if a and b:
328
+ return (f"Comparison:\n"
329
+ f"- {a['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(a['ARTNR'])}): {a['DIMMABILITY']}, {a['LOCATION']}, Efficiency {a['EFFICIENCY']}\n"
330
+ f"- {b['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(b['ARTNR'])}): {b['DIMMABILITY']}, {b['LOCATION']}, Efficiency {b['EFFICIENCY']}")
331
+ # IP20 vs IP67
332
+ if "ip20" in q and "ip67" in q:
333
+ ip20 = [v for v in tech_info.values() if "ip20" in str(v["CONVERTER DESCRIPTION"]).lower()]
334
+ ip67 = [v for v in tech_info.values() if "ip67" in str(v["CONVERTER DESCRIPTION"]).lower()]
335
+ return (f"IP20 converters:\n" + "\n".join([f"- {v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])})" for v in ip20]) + "\n\n" +
336
+ f"IP67 converters:\n" + "\n".join([f"- {v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])})" for v in ip67]))
337
+ # Size/space
338
+ if "smallest" in q or "compact" in q:
339
+ candidates = [v for v in tech_info.values() if "24v" in v["TYPE"].lower()]
340
+ if not candidates:
341
+ return "No 24V converters found."
342
+ smallest = min(
343
+ candidates,
344
+ key=lambda x: parse_float(str(x["SIZE"].split('*')[0]))
345
+ )
346
+ return f"Smallest 24V converter: {smallest['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(smallest['ARTNR'])}), size: {smallest['SIZE']}"
347
+ if "under 100mm" in q or ("length" in q and "100" in q):
348
+ candidates = [v for v in tech_info.values() if parse_float(str(v["SIZE"].split('*')[0])) < 100]
349
+ return "\n".join([f"{v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])}), size: {v['SIZE']}" for v in candidates])
350
+ # Documentation
351
+ if "datasheet" in q or "manual" in q or "pdf" in q:
352
+ numbers = re.findall(r'\d+', question)
353
+ if numbers:
354
+ v = get_product_by_artnr(numbers[0], tech_info)
355
+ if v and v["PDF_LINK"] != "N/A":
356
+ return f"Datasheet/manual for {v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])}): {v['PDF_LINK']}"
357
+ # Pricing
358
+ if "price" in q or "affordable" in q:
359
+ if "most affordable" in q:
360
+ candidates = [v for v in tech_info.values() if "24v" in v["TYPE"].lower() and str(v["Listprice"]) != "N/A"]
361
+ if candidates:
362
+ cheapest = min(candidates, key=lambda x: float(str(x["Listprice"]).replace(',', '.')))
363
+ return f"Most affordable 24V converter: {cheapest['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(cheapest['ARTNR'])}), price: {cheapest['Listprice']}"
364
+ elif "price below" in q:
365
+ price_match = re.search(r'€(\d+)', question)
366
+ price = float(price_match.group(1)) if price_match else 65
367
+ candidates = [
368
+ v for v in tech_info.values()
369
+ if "24v" in v["TYPE"].lower()
370
+ and str(v["Listprice"]) != "N/A"
371
+ and parse_price(v["Listprice"]) < price
372
+ ]
373
+ return "\n".join([f"{v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])}), price: {v['Listprice']}" for v in candidates])
374
+ # Product info
375
+ if "weight" in q:
376
+ numbers = re.findall(r'\d+', question)
377
+ if numbers:
378
+ v = get_product_by_artnr(numbers[0], tech_info)
379
+ if v and v["WEIGHT"] != "N/A":
380
+ return f"Weight of {v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])}): {v['WEIGHT']} kg"
381
+
382
+
383
+ if "input voltage" in q:
384
+ numbers = re.findall(r'\d+', question)
385
+ if numbers:
386
+ v = get_product_by_artnr(numbers[0], tech_info)
387
+ if v and v["INPUT VOLTAGE"] != "N/A":
388
+ return f"Input voltage range of {v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])}): {v['INPUT VOLTAGE']}"
389
+ # All 24V converters
390
+ if "show me all 24v converters" in q:
391
+ candidates = [v for v in tech_info.values() if "24v" in v["TYPE"].lower()]
392
+ return "\n".join([f"{v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])})" for v in candidates])
393
+ # Lifecycle
394
+ if "active" in q or "lifecycle" in q:
395
+ candidates = [v for v in tech_info.values() if v.get("LifeCycle", "").upper() == "A"]
396
+ return "\n".join([f"{v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])}) is active." for v in candidates])
397
+
398
+ if "output voltage for each converter" in q or "output voltage for each model" in q:
399
+ result = []
400
+ for v in tech_info.values():
401
+ description = v.get("CONVERTER DESCRIPTION", "N/A").strip()
402
+ output_voltage = v.get("OUTPUT VOLTAGE", "N/A")
403
+ result.append(f"{description}: {output_voltage}")
404
+ return "\n".join(result)
405
+
406
+ if "ip rating for each converter" in q and "what does it mean" in q:
407
+ ip_meaning = {
408
+ "IP20": "Protected against solid foreign objects ≥12mm (e.g., fingers), no protection against water. Suitable for indoor use in protected environments like cabinets.",
409
+ "IP54": "Protected against limited dust ingress and water splashes from any direction. Suitable for outdoor use in sheltered locations.",
410
+ "IP65": "Dust-tight and protected against low-pressure water jets. Suitable for outdoor use.",
411
+ "IP66": "Dust-tight and protected against powerful water jets. Suitable for outdoor use in harsh environments.",
412
+ "IP67": "Dust-tight and protected against temporary immersion in water. Suitable for outdoor use, even in harsh environments."
413
+ }
414
+ result = ["IP rating for each converter and installation meaning:"]
415
+ for v in tech_info.values():
416
+ description = v.get("CONVERTER DESCRIPTION", "N/A").strip()
417
+ ip = v.get("IP", "N/A")
418
+ normalized_ip = normalize_ip(ip)
419
+ meaning = ip_meaning.get(normalized_ip, "No specific installation guidance available.")
420
+ result.append(f"{description}: {normalized_ip} – {meaning}")
421
+ return "\n".join(result)
422
+
423
+ if "class of each converter" in q or "class (electrical safety class) of each converter" in q:
424
+ result = ["Class (electrical safety class) for each converter:"]
425
+ for v in tech_info.values():
426
+ description = v.get("CONVERTER DESCRIPTION", "N/A").strip()
427
+ class_ = v.get("CLASS", "N/A")
428
+ result.append(f"{description}: Class {class_}")
429
+ return "\n".join(result)
430
+
431
+ if "dimensions" in q and "lbh" in q or ("dimensions" in q and "l*b*h" in q) or ("dimensions of each converter" in q):
432
+ result = ["Dimensions (LBH) for each converter:"]
433
+ for v in tech_info.values():
434
+ description = v.get("CONVERTER DESCRIPTION", "N/A").strip()
435
+ size = v.get("SIZE", "N/A")
436
+ result.append(f"{description}: {size}")
437
+ return "\n".join(result)
438
+
439
+ if "weight of converter" in q or "weight of each converter" in q or ("gross weight" in q and "each" in q):
440
+ result = ["Gross weight of each converter:"]
441
+ for v in tech_info.values():
442
+ description = v.get("CONVERTER DESCRIPTION", "N/A").strip()
443
+ weight = v.get("WEIGHT", v.get("Gross Weight", "N/A"))
444
+ result.append(f"{description}: {weight} kg")
445
+ return "\n".join(result)
446
+
447
+ # Example: "What is the difference between the 24V DC and 48V LED converters?"
448
+ if "difference between" in q and any(
449
+ (f"{x}v" in q and f"{y}v" in q) or
450
+ (f"{x}ma" in q and f"{y}ma" in q)
451
+ for x, y in [(24, 48), (180, 250), (250, 260), (260, 350), (350, 500), (500, 700)]
452
+ ):
453
+ # Extract the two types from the question (simplified for demo)
454
+ parts = q.split("between")[1].split("and")
455
+ type1 = parts[0].strip().lower()
456
+ type2 = parts[1].strip().lower()
457
+
458
+ # Build a technical explanation based on the types
459
+ if "24v" in type1 and "48v" in type2:
460
+ explanation = (
461
+ "Difference between 24V DC and 48V LED converters:\n"
462
+ "- **Power Delivery:** 48V converters can deliver the same power at half the current compared to 24V, reducing cable size and cost.\n"
463
+ "- **Efficiency:** 48V systems are generally more efficient, especially over longer cable runs, due to lower current and less voltage drop.\n"
464
+ "- **Safety:** Both 24V and 48V are considered Safety Extra Low Voltage (SELV), but 48V is still below the 60V SELV limit, so it remains safe for most installations.\n"
465
+ "- **Compatibility:** 48V converters are better for large LED systems or longer runs, while 24V is common for smaller or standard installations.\n"
466
+ "- **System Design:** 48V allows for higher power LED arrays and longer cable runs without significant voltage drop or power loss[2][3][4].\n"
467
+ )
468
+ elif any(f"{x}ma" in type1 and f"{y}ma" in type2 for x, y in [(180, 250), (250, 260), (260, 350), (350, 500), (500, 700)]):
469
+ # Example for current-based converters
470
+ current1 = type1.split("ma")[0].strip()
471
+ current2 = type2.split("ma")[0].strip()
472
+ explanation = (
473
+ f"Difference between {current1}mA and {current2}mA LED converters:\n"
474
+ f"- **Current Output:** {current2}mA converters can drive more power-hungry or larger LED installations compared to {current1}mA.\n"
475
+ f"- **Application:** {current1}mA is typically used for smaller LED strips or modules, while {current2}mA is used for larger or more demanding LED setups.\n"
476
+ f"- **Efficiency:** Higher current converters (like {current2}mA) may require thicker cables to minimize voltage drop and power loss over distance.\n"
477
+ )
478
+ else:
479
+ explanation = "Sorry, I couldn't find a technical comparison for those converter types. Please specify the types you want to compare (e.g., 24V vs 48V, or 180mA vs 350mA)."
480
+
481
+ return explanation
482
+
483
+ # Example: "What is the difference between remote and in-track LED converters?"
484
+ if "difference between remote and in-track" in q.lower() or "remote vs in-track" in q.lower():
485
+ explanation = (
486
+ "Difference between 'remote' and 'in-track' LED converters:\n\n"
487
+ "- **Remote Converters:**\n"
488
+ " - The converter (driver) is located outside the LED track or rail, often in a central location or remote enclosure.\n"
489
+ " - Multiple LED tracks or fixtures can be powered from a single remote converter.\n"
490
+ " - Remote converters are easier to access for maintenance or replacement.\n"
491
+ " - They are typically used for larger installations or when you want to centralize power management.\n"
492
+ " - Remote converters can be more efficient and reliable, as they are not limited by the space or heat constraints of the track.\n\n"
493
+ "- **In-Track Converters:**\n"
494
+ " - The converter is mounted directly inside or alongside the LED track or rail.\n"
495
+ " - Each track usually has its own dedicated converter.\n"
496
+ " - In-track converters are more compact and can be used for smaller installations or where a centralized converter is not practical.\n"
497
+ " - They are less visible and can be easier to install in tight spaces.\n"
498
+ " - Maintenance or replacement may require access to the track itself.\n\n"
499
+ "**Summary:**\n"
500
+ "Remote converters are best for larger, more complex systems with centralized power, while in-track converters are ideal for smaller, standalone tracks or where space and aesthetics are a concern."
501
+ )
502
+ return explanation
503
+
504
+ if "minimum and maximum number of lamps" in q or "min and max number of lamps" in q or "min max lamps" in q:
505
+ result = ["Minimum and maximum number of lamps that can be connected to each converter:"]
506
+ for v in tech_info.values():
507
+ description = v.get("CONVERTER DESCRIPTION", "N/A").strip()
508
+ lamps = v.get("LAMPS", {})
509
+ if not lamps:
510
+ result.append(f"{description}: No lamp compatibility data available.")
511
+ else:
512
+ for lamp_name, lamp_data in lamps.items():
513
+ min_val = lamp_data.get("min", "N/A")
514
+ max_val = lamp_data.get("max", "N/A")
515
+ result.append(f"{description}: {lamp_name} – min: {min_val}, max: {max_val}")
516
+ return "\n".join(result)
517
+
518
+
519
+
520
+ # Default fallback
521
+ return "I do not know the answer to this question."
522
+
523
+
524
+ # --- LLM fallback function ---
525
+ def llm_fallback(question):
526
+ prompt = f"User: {question}\nAssistant:"
527
+ inputs = llm_tokenizer(prompt, return_tensors="pt", truncation=True, max_length=256)
528
+ outputs = llm_model.generate(
529
+ input_ids=inputs["input_ids"],
530
+ attention_mask=inputs["attention_mask"],
531
+ max_new_tokens=64,
532
+ do_sample=True,
533
+ temperature=0.7,
534
+ pad_token_id=llm_tokenizer.eos_token_id
535
+ )
536
+ completion = llm_tokenizer.decode(outputs[0], skip_special_tokens=True)
537
+ # Extract only the assistant's answer
538
+ if "Assistant:" in completion:
539
+ return completion.split("Assistant:")[-1].strip()
540
+ else:
541
+ return completion.strip()
542
+
543
+ # --- Prompt and Graph ---
544
+
545
+ custom_prompt = ChatPromptTemplate.from_messages([
546
+ ("system", "You are a helpful technical assistant for TAL BV and assist users in finding information. Use the provided documentation to answer questions accurately and with necessary sources."),
547
+ ("human", """Context: {context}
548
+ Question: {question}
549
+ Answer:""")
550
+ ])
551
+
552
+ class State(TypedDict):
553
+ question: str
554
+ context: List[Document]
555
+ answer: str
556
+
557
+ def retrieve(state: State):
558
+ retriever = vector_store.as_retriever(search_kwargs={"k": 3})
559
+ retrieved_docs = retriever.invoke(state["question"])
560
+ return {"context": retrieved_docs}
561
+
562
+ def generate(state: State):
563
+ docs_content = "\n\n".join(doc.page_content for doc in state["context"])
564
+ prompt = f"""
565
+ You are a helpful technical assistant for TAL BV and assist users in finding information. Use the provided documentation to answer questions accurately and with necessary sources.
566
+
567
+ Context: {docs_content}
568
+ Question: {state["question"]}
569
+ Answer:
570
+ """
571
+ input_ids = tokenizer.encode(prompt, truncation=True, max_length=max_length, return_tensors="pt")
572
+ truncated_prompt = tokenizer.decode(input_ids[0])
573
+ response = chatbot(truncated_prompt, max_new_tokens=32, do_sample=True, temperature=0.2)
574
+ answer = response[0]['generated_text'].split("Answer:", 1)[-1].strip()
575
+ return {"answer": answer}
576
+
577
+ graph_builder = StateGraph(State)
578
+ graph_builder.add_node("retrieve", retrieve)
579
+ graph_builder.add_node("generate", generate)
580
+ graph_builder.add_edge(START, "retrieve")
581
+ graph_builder.add_edge("retrieve", "generate")
582
+ graph = graph_builder.compile()
583
+
584
+ # --- Main chatbot function ---
585
+ def tal_langchain_chatbot(user_message, history=None):
586
+ # 1. Try to answer from database/rules
587
+ answer = answer_technical_question(user_message, tech_info)
588
+ # 2. If no answer, use the LLM
589
+ if not answer or answer.lower() == "i do not know the answer to this question.":
590
+ answer = llm_fallback(user_message)
591
+ # 3. Update history and return
592
+ if history is None:
593
+ history = []
594
+ history.append({"role": "user", "content": user_message})
595
+ history.append({"role": "assistant", "content": answer})
596
+ return history, history, ""
597
+
598
+
599
+ # --- Gradio UI ---
600
+
601
+ custom_css = """
602
+ #chatbot-toggle-btn {
603
+ position: fixed;
604
+ bottom: 30px;
605
+ right: 30px;
606
+ z-index: 10001;
607
+ background-color: #ED1C24;
608
+ color: white;
609
+ border: none;
610
+ border-radius: 50%;
611
+ width: 56px;
612
+ height: 56px;
613
+ font-size: 28px;
614
+ font-weight: bold;
615
+ cursor: pointer;
616
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
617
+ display: flex;
618
+ align-items: center;
619
+ justify-content: center;
620
+ transition: all 0.3s ease;
621
+ }
622
+
623
+ #chatbot-panel {
624
+ position: fixed;
625
+ bottom: 100px;
626
+ right: 30px;
627
+ z-index: 10000;
628
+ width: 600px; /* Increased width */
629
+ height: 700px; /* Increased height */
630
+ background-color: #ffffff;
631
+ border-radius: 20px;
632
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
633
+ display: flex;
634
+ flex-direction: column;
635
+ overflow: hidden;
636
+ font-family: 'Arial', sans-serif;
637
+ }
638
+
639
+ #chatbot-panel.hide {
640
+ display: none !important;
641
+ }
642
+
643
+ #chat-header {
644
+ background-color: #ED1C24;
645
+ color: white;
646
+ padding: 20px;
647
+ font-weight: bold;
648
+ font-size: 22px;
649
+ display: flex;
650
+ align-items: center;
651
+ gap: 12px;
652
+ width: 100%;
653
+ box-sizing: border-box;
654
+ }
655
+
656
+ #chat-header img {
657
+ border-radius: 50%;
658
+ width: 40px;
659
+ height: 40px;
660
+ }
661
+
662
+ .gr-chatbot {
663
+ flex: 1;
664
+ overflow-y: auto;
665
+ padding: 20px;
666
+ background-color: #f9f9f9;
667
+ border-top: 1px solid #eee;
668
+ border-bottom: 1px solid #eee;
669
+ display: flex;
670
+ flex-direction: column;
671
+ gap: 12px;
672
+ box-sizing: border-box;
673
+ }
674
+
675
+ .gr-textbox {
676
+ padding: 16px 20px;
677
+ background-color: #fff;
678
+ display: flex;
679
+ align-items: center;
680
+ gap: 12px;
681
+ border-top: 1px solid #eee;
682
+ box-sizing: border-box;
683
+ }
684
+
685
+ .gr-textbox textarea {
686
+ flex: 1;
687
+ resize: none;
688
+ padding: 12px;
689
+ background-color: white;
690
+ border: 1px solid #ccc;
691
+ border-radius: 8px;
692
+ font-family: inherit;
693
+ font-size: 16px;
694
+ box-sizing: border-box;
695
+ height: 48px;
696
+ line-height: 1.5;
697
+ }
698
+
699
+ .gr-textbox button {
700
+ background-color: #ED1C24;
701
+ border: none;
702
+ color: white;
703
+ border-radius: 8px;
704
+ padding: 12px 20px;
705
+ cursor: pointer;
706
+ font-weight: bold;
707
+ transition: background-color 0.3s ease;
708
+ font-size: 16px;
709
+ }
710
+
711
+ .gr-textbox button:hover {
712
+ background-color: #c4161c;
713
+ }
714
+
715
+ footer {
716
+ display: none !important;
717
+ }
718
+
719
+ """
720
+
721
+ def toggle_visibility(current_state):
722
+ new_state = not current_state
723
+ return new_state, gr.update(visible=new_state)
724
+
725
+ with gr.Blocks(css=custom_css) as demo:
726
+ visibility_state = gr.State(False)
727
+ history = gr.State([])
728
+
729
+ chatbot_toggle = gr.Button("💬", elem_id="chatbot-toggle-btn")
730
+ with gr.Column(visible=False, elem_id="chatbot-panel") as chatbot_panel:
731
+ gr.HTML("""
732
+ <div id='chat-header'>
733
+ <img src="https://www.svgrepo.com/download/490283/pixar-lamp.svg" />
734
+ Lofty the TAL Bot
735
+ </div>
736
+ """)
737
+ chat = gr.Chatbot(label="Chat", elem_id="chat-window", type="messages")
738
+ msg = gr.Textbox(placeholder="Type your message here...", show_label=False)
739
+ send = gr.Button("Send")
740
+ send.click(
741
+ fn=tal_langchain_chatbot,
742
+ inputs=[msg, history],
743
+ outputs=[chat, history, msg]
744
+ )
745
+ msg.submit(
746
+ fn=tal_langchain_chatbot,
747
+ inputs=[msg, history],
748
+ outputs=[chat, history, msg]
749
+ )
750
+
751
+ chatbot_toggle.click(
752
+ fn=toggle_visibility,
753
+ inputs=visibility_state,
754
+ outputs=[visibility_state, chatbot_panel]
755
+ )
756
+
757
+ if __name__ == "__main__":
758
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ transformers
2
+ langchain-core
3
+ langchain-huggingface
4
+ langchain-community
5
+ langgraph
6
+ python-dotenv
7
+ faiss-cpu