Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import re | |
| import gradio as gr | |
| from transformers import pipeline, AutoTokenizer | |
| from langchain_core.documents import Document | |
| from langchain_huggingface import HuggingFaceEmbeddings | |
| from langchain_community.vectorstores import FAISS | |
| from langchain_core.prompts import ChatPromptTemplate | |
| from typing import List, TypedDict | |
| from langgraph.graph import StateGraph, START | |
| from dotenv import load_dotenv | |
| from transformers import GPT2LMHeadModel, GPT2Tokenizer | |
| from huggingface_hub import login | |
| login(token=os.environ["HUGGINGFACEHUB_API_TOKEN"]) | |
| # --- Configuration --- | |
| load_dotenv() | |
| os.environ["HUGGINGFACEHUB_API_TOKEN"] = os.getenv("HUGGINGFACEHUB_API_TOKEN") | |
| os.environ["TOKENIZERS_PARALLELISM"] = "false" | |
| model_name = "Sathvika-Alla/TAL-RAGFallback" | |
| # Load the model and tokenizer | |
| llm_model = GPT2LMHeadModel.from_pretrained(model_name, token=os.environ["HUGGINGFACEHUB_API_TOKEN"]) | |
| llm_tokenizer = GPT2Tokenizer.from_pretrained(model_name, token=os.environ["HUGGINGFACEHUB_API_TOKEN"]) | |
| llm_tokenizer.pad_token = llm_tokenizer.eos_token | |
| file_path = "./converters_with_links_and_pricelist.json" | |
| try: | |
| with open(file_path, 'r', encoding='utf-8') as f: | |
| product_data = json.load(f) | |
| except Exception as e: | |
| print(f"Error loading product data: {e}") | |
| product_data = {} | |
| tokenizer = AutoTokenizer.from_pretrained("facebook/blenderbot-400M-distill") | |
| tokenizer.truncation_side = "left" | |
| max_length = tokenizer.model_max_length | |
| docs = [Document(page_content=str(value), metadata={"source": key}) for key, value in product_data.items()] | |
| embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2") | |
| vector_store = FAISS.from_documents(docs, embeddings) | |
| chatbot = pipeline("text-generation", model="facebook/blenderbot-400M-distill") | |
| # --- Helper Functions --- | |
| def parse_float(s): | |
| try: | |
| if isinstance(s, (list, tuple)): | |
| s = s[0] | |
| return float(str(s).replace(',', '.').strip()) | |
| except Exception: | |
| return float('inf') | |
| def parse_price(val): | |
| if isinstance(val, float) or isinstance(val, int): | |
| return float(val) | |
| try: | |
| return float(str(val).replace(',', '.')) | |
| except Exception: | |
| return float('inf') | |
| def normalize_artnr(artnr): | |
| try: | |
| return str(int(float(artnr))) | |
| except Exception: | |
| return str(artnr) | |
| def normalize_ip(ip): | |
| if isinstance(ip, (int, float)): | |
| return f"IP{int(ip)}" | |
| elif isinstance(ip, str): | |
| ip_part = ip.replace("IP", "").split(".")[0] | |
| return f"IP{ip_part}" | |
| else: | |
| return "N/A" | |
| def get_product_by_artnr(artnr, tech_info): | |
| artnr_str = normalize_artnr(artnr) | |
| for value in tech_info.values(): | |
| if normalize_artnr(value.get("ARTNR", "")) == artnr_str: | |
| return value | |
| return None | |
| def extract_converter_and_lamp(user_message: str): | |
| match = re.search(r"how many (\w+) lamps?.*converter (\d+)", user_message.lower()) | |
| if match: | |
| lamp_name = match.group(1) | |
| converter_number = match.group(2) | |
| return lamp_name, converter_number | |
| return None, None | |
| def get_technical_fit_info(product_data: dict) -> dict: | |
| results = {} | |
| for key, value in product_data.items(): | |
| results[key] = { | |
| "TYPE": value.get("TYPE", "N/A"), | |
| "ARTNR": value.get("ARTNR", "N/A"), | |
| "CONVERTER DESCRIPTION": value.get("CONVERTER DESCRIPTION:", "N/A"), | |
| "STRAIN RELIEF": value.get("STRAIN RELIEF", "N/A"), | |
| "LOCATION": value.get("LOCATION", "N/A"), | |
| "DIMMABILITY": value.get("DIMMABILITY", "N/A"), | |
| "EFFICIENCY": value.get("EFFICIENCY @full load", "N/A"), | |
| "OUTPUT VOLTAGE": value.get("OUTPUT VOLTAGE (V)", "N/A"), | |
| "INPUT VOLTAGE": value.get("NOM. INPUT VOLTAGE (V)", "N/A"), | |
| "SIZE": value.get("SIZE: L*B*H (mm)", "N/A"), | |
| "WEIGHT": value.get("Gross Weight", "N/A"), | |
| "Listprice": value.get("Listprice", "N/A"), | |
| "LAMPS": value.get("lamps", {}), | |
| "PDF_LINK": value.get("pdf_link", "N/A"), | |
| "IP": value.get("IP", "N/A"), | |
| "CLASS": value.get("CLASS", "N/A"), | |
| "LifeCycle": value.get("LifeCycle", "N/A"), | |
| "Name": value.get("Name", "N/A"), | |
| } | |
| return results | |
| tech_info = get_technical_fit_info(product_data) | |
| def recommend_converters_for_lamp(lamp_query, tech_info): | |
| def normalize(s): | |
| # Lowercase, remove commas and dots, strip spaces | |
| return s.lower().replace(",", "").replace(".", "").strip() | |
| norm_query = normalize(lamp_query) | |
| query_words = set(norm_query.split()) | |
| results = [] | |
| for v in tech_info.values(): | |
| lamps = v.get("LAMPS", {}) | |
| for lamp_name, lamp_data in lamps.items(): | |
| norm_lamp = normalize(lamp_name) | |
| lamp_words = set(norm_lamp.split()) | |
| # 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 | |
| if ( | |
| query_words.issubset(lamp_words) | |
| or norm_query in norm_lamp | |
| or norm_lamp in norm_query | |
| ): | |
| min_val = lamp_data.get("min", "N/A") | |
| max_val = lamp_data.get("max", "N/A") | |
| desc = v.get("CONVERTER DESCRIPTION", v.get("CONVERTER DESCRIPTION:", "N/A")).strip() | |
| artnr = v.get("ARTNR", "N/A") | |
| results.append(f"{desc} (ARTNR: {int(float(artnr)) if artnr != 'N/A' else 'N/A'}), supports {min_val} to {max_val} x \"{lamp_name}\"") | |
| if not results: | |
| return f"Sorry, I couldn't find a converter for '{lamp_query}'." | |
| return "Recommended converters:\n" + "\n".join(results) | |
| def get_lamp_quantity(converter_number: str, lamp_name: str, tech_info: dict) -> str: | |
| v = get_product_by_artnr(converter_number, tech_info) | |
| if not v: | |
| return f"Sorry, I could not find converter {converter_number}." | |
| for lamp_key, lamp_vals in v["LAMPS"].items(): | |
| if lamp_name.lower() in lamp_key.lower(): | |
| min_val = lamp_vals.get("min", "N/A") | |
| max_val = lamp_vals.get("max", "N/A") | |
| if min_val == max_val: | |
| return f"You can use {min_val} {lamp_key} lamp(s) with converter {converter_number}." | |
| else: | |
| return f"You can use between {min_val} and {max_val} {lamp_key} lamp(s) with converter {converter_number}." | |
| return f"Sorry, no data found for lamp '{lamp_name}' with converter {converter_number}." | |
| def get_recommended_converter_any(user_message, tech_info): | |
| match = re.search(r'(\d+)\s*x\s*([\w\d\s\-,.*]+)', user_message, re.IGNORECASE) | |
| if not match: | |
| return None | |
| num_lamps = int(match.group(1)) | |
| lamp_query = match.group(2).strip().lower() | |
| candidates = [] | |
| for v in tech_info.values(): | |
| for lamp, vals in v["LAMPS"].items(): | |
| lamp_norm = lamp.lower().replace(',', '.') | |
| if all(word in lamp_norm for word in lamp_query.split()): | |
| max_lamps = float(str(vals.get("max", 0)).replace(',', '.')) | |
| if max_lamps >= num_lamps: | |
| candidates.append((v, lamp, max_lamps)) | |
| if not candidates: | |
| return f"Sorry, I couldn't find a converter that supports {num_lamps}x {lamp_query.title()}." | |
| else: | |
| return "\n".join([ | |
| 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}')." | |
| for v, lamp, max_lamps in candidates | |
| ]) | |
| def answer_technical_question(question: str, tech_info: dict) -> str: | |
| q = question.lower() | |
| # --- Lamp-only queries like "Which converter should I use for 'LEDLINE medium power 9.6W' strips?" --- | |
| lamp_match = re.search( | |
| r'(?:for|recommend|use|need)[\s:]*["“”\']?([a-zA-Z0-9 ,.\-]+w)[\s"”\']*(?:strips?|ledline|lamps?)?', q | |
| ) | |
| if lamp_match: | |
| lamp_query = lamp_match.group(1).strip() | |
| result = recommend_converters_for_lamp(lamp_query, tech_info) | |
| if result and "couldn't find" not in result: | |
| return result | |
| # Fallback: match any lamp in the database if all its words are in the question | |
| def normalize_lamp_string(s): | |
| return set(s.lower().replace(",", "").replace(".", "").split()) | |
| q_words = set(q.replace(",", "").replace(".", "").split()) | |
| for v in tech_info.values(): | |
| for lamp_name in v.get("LAMPS", {}): | |
| lamp_words = normalize_lamp_string(lamp_name) | |
| if lamp_words and lamp_words.issubset(q_words): | |
| result = recommend_converters_for_lamp(lamp_name, tech_info) | |
| if result and "couldn't find" not in result: | |
| return result | |
| def answer_technical_question(question: str, tech_info: dict) -> str: | |
| q = question.lower() | |
| # Try to extract lamp name after 'for', 'recommend', 'use', etc. | |
| lamp_match = re.search( | |
| r'(?:for|recommend|use|need)[\s:]*["“”\']?([a-zA-Z0-9 ,.\-]+w)[\s"”\']*(?:strips?|ledline|lamps?)?', q | |
| ) | |
| if lamp_match: | |
| lamp_query = lamp_match.group(1).strip() | |
| result = recommend_converters_for_lamp(lamp_query, tech_info) | |
| if result and "couldn't find" not in result: | |
| return result | |
| # Fallback: match any lamp in the database if all its words are in the question | |
| def normalize_lamp_string(s): | |
| return set(s.lower().replace(",", "").replace(".", "").split()) | |
| q_words = set(q.replace(",", "").replace(".", "").split()) | |
| for v in tech_info.values(): | |
| for lamp_name in v.get("LAMPS", {}): | |
| lamp_words = normalize_lamp_string(lamp_name) | |
| if lamp_words and lamp_words.issubset(q_words): | |
| result = recommend_converters_for_lamp(lamp_name, tech_info) | |
| if result and "couldn't find" not in result: | |
| return result | |
| # Efficiency at full load for all converters | |
| if "efficiency at full load for each converter" in q or "efficiency for each converter" in q: | |
| result = [] | |
| for v in tech_info.values(): | |
| description = v.get("CONVERTER DESCRIPTION", "N/A").strip() | |
| efficiency = v.get("EFFICIENCY", "N/A") | |
| result.append(f"{description}: {efficiency}") | |
| return "\n".join(result) | |
| # Generalized lamp fit for any type in the database | |
| if re.search(r"\d+\s*x\s*[\w\d\s\-,.*]+", q): | |
| result = get_recommended_converter_any(question, tech_info) | |
| if result: | |
| return result | |
| # Outdoor installation | |
| if "outdoor" in q: | |
| return "\n".join([f"{v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])})" | |
| for v in tech_info.values() | |
| if "outdoor" in v["LOCATION"].lower() or "in&outdoor" in v["LOCATION"].lower()]) | |
| # Most efficient converter for any type | |
| if "most efficient" in q: | |
| type_match = re.search(r'(\d+\s*v|\d+\s*ma)', q) | |
| if type_match: | |
| search_type = type_match.group(1).replace(' ', '').lower() | |
| candidates = [ | |
| v for v in tech_info.values() | |
| if search_type in str(v["TYPE"]).replace(' ', '').lower() | |
| and str(v.get("EFFICIENCY", v.get("EFFICIENCY @full load", ""))).replace(',', '.').replace('.', '').isdigit() | |
| ] | |
| if not candidates: | |
| return f"No {search_type.upper()} converters found." | |
| best = max( | |
| candidates, | |
| key=lambda x: float(str(x.get("EFFICIENCY", x.get("EFFICIENCY @full load", "0"))).replace(',', '.')) | |
| ) | |
| desc = best.get("CONVERTER DESCRIPTION", best.get("CONVERTER DESCRIPTION:", "N/A")).strip() | |
| artnr = int(float(best.get("ARTNR", "N/A"))) if best.get("ARTNR") else "N/A" | |
| eff = best.get("EFFICIENCY", best.get("EFFICIENCY @full load", "N/A")) | |
| return f"The most efficient {search_type.upper()} converter is {desc} (ARTNR: {artnr}) with efficiency {eff}." | |
| else: | |
| # fallback: show most efficient overall | |
| candidates = [ | |
| v for v in tech_info.values() | |
| if str(v.get("EFFICIENCY", v.get("EFFICIENCY @full load", ""))).replace(',', '.').replace('.', '').isdigit() | |
| ] | |
| if not candidates: | |
| return "No converters with efficiency data found." | |
| best = max( | |
| candidates, | |
| key=lambda x: float(str(x.get("EFFICIENCY", x.get("EFFICIENCY @full load", "0"))).replace(',', '.')) | |
| ) | |
| desc = best.get("CONVERTER DESCRIPTION", best.get("CONVERTER DESCRIPTION:", "N/A")).strip() | |
| artnr = int(float(best.get("ARTNR", "N/A"))) if best.get("ARTNR") else "N/A" | |
| eff = best.get("EFFICIENCY", best.get("EFFICIENCY @full load", "N/A")) | |
| return f"The most efficient converter overall is {desc} (ARTNR: {artnr}) with efficiency {eff}." | |
| # Dimming support | |
| 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: | |
| type_match = re.search(r'(\d+\s*v|\d+\s*ma)', q) | |
| type_query = type_match.group(1).replace(" ", "").lower() if type_match else None | |
| results = [] | |
| for v in tech_info.values(): | |
| type_str = str(v.get("TYPE", "")).lower().replace(" ", "") | |
| dim = v.get("DIMMABILITY", "").upper() | |
| 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): | |
| desc = v.get("CONVERTER DESCRIPTION", v.get("CONVERTER DESCRIPTION:", "N/A")).strip() | |
| artnr = int(float(v.get("ARTNR", "N/A"))) if v.get("ARTNR") else "N/A" | |
| results.append(f"{desc} (ARTNR: {artnr}), Dimming: {dim}") | |
| if not results: | |
| return f"No{' ' + type_query.upper() if type_query else ''} converters with dimming support found." | |
| return "\n".join(results) | |
| # Strain relief | |
| if "strain relief" in q: | |
| candidates = [v for v in tech_info.values() if v["STRAIN RELIEF"].lower() == "yes"] | |
| yesno = "Yes" if candidates else "No" | |
| details = "\n".join([f"{v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])})" for v in candidates]) | |
| return f"{yesno}. " + (details if details else "") | |
| # Input voltage range for each converter | |
| if "input voltage range for each converter" in q or "input voltage range" in q and "each" in q: | |
| result = [] | |
| for v in tech_info.values(): | |
| description = v.get("CONVERTER DESCRIPTION", "N/A").strip() | |
| input_voltage = v.get("INPUT VOLTAGE", "N/A") | |
| result.append(f"{description}: {input_voltage}") | |
| return "\n".join(result) | |
| # Comparison | |
| if "compare" in q: | |
| numbers = re.findall(r'\d+', question) | |
| if len(numbers) >= 2: | |
| a = get_product_by_artnr(numbers[0], tech_info) | |
| b = get_product_by_artnr(numbers[1], tech_info) | |
| if a and b: | |
| return (f"Comparison:\n" | |
| f"- {a['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(a['ARTNR'])}): {a['DIMMABILITY']}, {a['LOCATION']}, Efficiency {a['EFFICIENCY']}\n" | |
| f"- {b['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(b['ARTNR'])}): {b['DIMMABILITY']}, {b['LOCATION']}, Efficiency {b['EFFICIENCY']}") | |
| # IP20 vs IP67 | |
| if "ip20" in q and "ip67" in q: | |
| ip20 = [v for v in tech_info.values() if "ip20" in str(v["CONVERTER DESCRIPTION"]).lower()] | |
| ip67 = [v for v in tech_info.values() if "ip67" in str(v["CONVERTER DESCRIPTION"]).lower()] | |
| return (f"IP20 converters:\n" + "\n".join([f"- {v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])})" for v in ip20]) + "\n\n" + | |
| f"IP67 converters:\n" + "\n".join([f"- {v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])})" for v in ip67])) | |
| # Size/space | |
| if "smallest" in q or "compact" in q: | |
| candidates = [v for v in tech_info.values() if "24v" in v["TYPE"].lower()] | |
| if not candidates: | |
| return "No 24V converters found." | |
| smallest = min( | |
| candidates, | |
| key=lambda x: parse_float(str(x["SIZE"].split('*')[0])) | |
| ) | |
| return f"Smallest 24V converter: {smallest['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(smallest['ARTNR'])}), size: {smallest['SIZE']}" | |
| if "under 100mm" in q or ("length" in q and "100" in q): | |
| candidates = [v for v in tech_info.values() if parse_float(str(v["SIZE"].split('*')[0])) < 100] | |
| return "\n".join([f"{v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])}), size: {v['SIZE']}" for v in candidates]) | |
| # Documentation | |
| if "datasheet" in q or "manual" in q or "pdf" in q: | |
| numbers = re.findall(r'\d+', question) | |
| if numbers: | |
| v = get_product_by_artnr(numbers[0], tech_info) | |
| if v and v["PDF_LINK"] != "N/A": | |
| return f"Datasheet/manual for {v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])}): {v['PDF_LINK']}" | |
| # Pricing | |
| if "price" in q or "affordable" in q: | |
| if "most affordable" in q: | |
| candidates = [v for v in tech_info.values() if "24v" in v["TYPE"].lower() and str(v["Listprice"]) != "N/A"] | |
| if candidates: | |
| cheapest = min(candidates, key=lambda x: float(str(x["Listprice"]).replace(',', '.'))) | |
| return f"Most affordable 24V converter: {cheapest['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(cheapest['ARTNR'])}), price: {cheapest['Listprice']}" | |
| elif "price below" in q: | |
| price_match = re.search(r'€(\d+)', question) | |
| price = float(price_match.group(1)) if price_match else 65 | |
| candidates = [ | |
| v for v in tech_info.values() | |
| if "24v" in v["TYPE"].lower() | |
| and str(v["Listprice"]) != "N/A" | |
| and parse_price(v["Listprice"]) < price | |
| ] | |
| return "\n".join([f"{v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])}), price: {v['Listprice']}" for v in candidates]) | |
| # Product info | |
| if "weight" in q: | |
| numbers = re.findall(r'\d+', question) | |
| if numbers: | |
| v = get_product_by_artnr(numbers[0], tech_info) | |
| if v and v["WEIGHT"] != "N/A": | |
| return f"Weight of {v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])}): {v['WEIGHT']} kg" | |
| if "input voltage" in q: | |
| numbers = re.findall(r'\d+', question) | |
| if numbers: | |
| v = get_product_by_artnr(numbers[0], tech_info) | |
| if v and v["INPUT VOLTAGE"] != "N/A": | |
| return f"Input voltage range of {v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])}): {v['INPUT VOLTAGE']}" | |
| # All 24V converters | |
| if "show me all 24v converters" in q: | |
| candidates = [v for v in tech_info.values() if "24v" in v["TYPE"].lower()] | |
| return "\n".join([f"{v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])})" for v in candidates]) | |
| # Lifecycle | |
| if "active" in q or "lifecycle" in q: | |
| candidates = [v for v in tech_info.values() if v.get("LifeCycle", "").upper() == "A"] | |
| return "\n".join([f"{v['CONVERTER DESCRIPTION']} (ARTNR: {normalize_artnr(v['ARTNR'])}) is active." for v in candidates]) | |
| if "output voltage for each converter" in q or "output voltage for each model" in q: | |
| result = [] | |
| for v in tech_info.values(): | |
| description = v.get("CONVERTER DESCRIPTION", "N/A").strip() | |
| output_voltage = v.get("OUTPUT VOLTAGE", "N/A") | |
| result.append(f"{description}: {output_voltage}") | |
| return "\n".join(result) | |
| if "ip rating for each converter" in q and "what does it mean" in q: | |
| ip_meaning = { | |
| "IP20": "Protected against solid foreign objects ≥12mm (e.g., fingers), no protection against water. Suitable for indoor use in protected environments like cabinets.", | |
| "IP54": "Protected against limited dust ingress and water splashes from any direction. Suitable for outdoor use in sheltered locations.", | |
| "IP65": "Dust-tight and protected against low-pressure water jets. Suitable for outdoor use.", | |
| "IP66": "Dust-tight and protected against powerful water jets. Suitable for outdoor use in harsh environments.", | |
| "IP67": "Dust-tight and protected against temporary immersion in water. Suitable for outdoor use, even in harsh environments." | |
| } | |
| result = ["IP rating for each converter and installation meaning:"] | |
| for v in tech_info.values(): | |
| description = v.get("CONVERTER DESCRIPTION", "N/A").strip() | |
| ip = v.get("IP", "N/A") | |
| normalized_ip = normalize_ip(ip) | |
| meaning = ip_meaning.get(normalized_ip, "No specific installation guidance available.") | |
| result.append(f"{description}: {normalized_ip} – {meaning}") | |
| return "\n".join(result) | |
| if "class of each converter" in q or "class (electrical safety class) of each converter" in q: | |
| result = ["Class (electrical safety class) for each converter:"] | |
| for v in tech_info.values(): | |
| description = v.get("CONVERTER DESCRIPTION", "N/A").strip() | |
| class_ = v.get("CLASS", "N/A") | |
| result.append(f"{description}: Class {class_}") | |
| return "\n".join(result) | |
| 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): | |
| result = ["Dimensions (LBH) for each converter:"] | |
| for v in tech_info.values(): | |
| description = v.get("CONVERTER DESCRIPTION", "N/A").strip() | |
| size = v.get("SIZE", "N/A") | |
| result.append(f"{description}: {size}") | |
| return "\n".join(result) | |
| if "weight of converter" in q or "weight of each converter" in q or ("gross weight" in q and "each" in q): | |
| result = ["Gross weight of each converter:"] | |
| for v in tech_info.values(): | |
| description = v.get("CONVERTER DESCRIPTION", "N/A").strip() | |
| weight = v.get("WEIGHT", v.get("Gross Weight", "N/A")) | |
| result.append(f"{description}: {weight} kg") | |
| return "\n".join(result) | |
| # Example: "What is the difference between the 24V DC and 48V LED converters?" | |
| if "difference between" in q and any( | |
| (f"{x}v" in q and f"{y}v" in q) or | |
| (f"{x}ma" in q and f"{y}ma" in q) | |
| for x, y in [(24, 48), (180, 250), (250, 260), (260, 350), (350, 500), (500, 700)] | |
| ): | |
| # Extract the two types from the question (simplified for demo) | |
| parts = q.split("between")[1].split("and") | |
| type1 = parts[0].strip().lower() | |
| type2 = parts[1].strip().lower() | |
| # Build a technical explanation based on the types | |
| if "24v" in type1 and "48v" in type2: | |
| explanation = ( | |
| "Difference between 24V DC and 48V LED converters:\n" | |
| "- **Power Delivery:** 48V converters can deliver the same power at half the current compared to 24V, reducing cable size and cost.\n" | |
| "- **Efficiency:** 48V systems are generally more efficient, especially over longer cable runs, due to lower current and less voltage drop.\n" | |
| "- **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" | |
| "- **Compatibility:** 48V converters are better for large LED systems or longer runs, while 24V is common for smaller or standard installations.\n" | |
| "- **System Design:** 48V allows for higher power LED arrays and longer cable runs without significant voltage drop or power loss[2][3][4].\n" | |
| ) | |
| 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)]): | |
| # Example for current-based converters | |
| current1 = type1.split("ma")[0].strip() | |
| current2 = type2.split("ma")[0].strip() | |
| explanation = ( | |
| f"Difference between {current1}mA and {current2}mA LED converters:\n" | |
| f"- **Current Output:** {current2}mA converters can drive more power-hungry or larger LED installations compared to {current1}mA.\n" | |
| 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" | |
| f"- **Efficiency:** Higher current converters (like {current2}mA) may require thicker cables to minimize voltage drop and power loss over distance.\n" | |
| ) | |
| else: | |
| 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)." | |
| return explanation | |
| # Example: "What is the difference between remote and in-track LED converters?" | |
| if "difference between remote and in-track" in q.lower() or "remote vs in-track" in q.lower(): | |
| explanation = ( | |
| "Difference between 'remote' and 'in-track' LED converters:\n\n" | |
| "- **Remote Converters:**\n" | |
| " - The converter (driver) is located outside the LED track or rail, often in a central location or remote enclosure.\n" | |
| " - Multiple LED tracks or fixtures can be powered from a single remote converter.\n" | |
| " - Remote converters are easier to access for maintenance or replacement.\n" | |
| " - They are typically used for larger installations or when you want to centralize power management.\n" | |
| " - Remote converters can be more efficient and reliable, as they are not limited by the space or heat constraints of the track.\n\n" | |
| "- **In-Track Converters:**\n" | |
| " - The converter is mounted directly inside or alongside the LED track or rail.\n" | |
| " - Each track usually has its own dedicated converter.\n" | |
| " - In-track converters are more compact and can be used for smaller installations or where a centralized converter is not practical.\n" | |
| " - They are less visible and can be easier to install in tight spaces.\n" | |
| " - Maintenance or replacement may require access to the track itself.\n\n" | |
| "**Summary:**\n" | |
| "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." | |
| ) | |
| return explanation | |
| if "minimum and maximum number of lamps" in q or "min and max number of lamps" in q or "min max lamps" in q: | |
| result = ["Minimum and maximum number of lamps that can be connected to each converter:"] | |
| for v in tech_info.values(): | |
| description = v.get("CONVERTER DESCRIPTION", "N/A").strip() | |
| lamps = v.get("LAMPS", {}) | |
| if not lamps: | |
| result.append(f"{description}: No lamp compatibility data available.") | |
| else: | |
| for lamp_name, lamp_data in lamps.items(): | |
| min_val = lamp_data.get("min", "N/A") | |
| max_val = lamp_data.get("max", "N/A") | |
| result.append(f"{description}: {lamp_name} – min: {min_val}, max: {max_val}") | |
| return "\n".join(result) | |
| # Default fallback | |
| return "I do not know the answer to this question." | |
| # --- LLM fallback function --- | |
| def llm_fallback(question): | |
| prompt = f"User: {question}\nAssistant:" | |
| inputs = llm_tokenizer(prompt, return_tensors="pt", truncation=True, max_length=256) | |
| outputs = llm_model.generate( | |
| input_ids=inputs["input_ids"], | |
| attention_mask=inputs["attention_mask"], | |
| max_new_tokens=64, | |
| do_sample=True, | |
| temperature=0.7, | |
| pad_token_id=llm_tokenizer.eos_token_id | |
| ) | |
| completion = llm_tokenizer.decode(outputs[0], skip_special_tokens=True) | |
| # Extract only the assistant's answer | |
| if "Assistant:" in completion: | |
| return completion.split("Assistant:")[-1].strip() | |
| else: | |
| return completion.strip() | |
| # --- Prompt and Graph --- | |
| custom_prompt = ChatPromptTemplate.from_messages([ | |
| ("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."), | |
| ("human", """Context: {context} | |
| Question: {question} | |
| Answer:""") | |
| ]) | |
| class State(TypedDict): | |
| question: str | |
| context: List[Document] | |
| answer: str | |
| def retrieve(state: State): | |
| retriever = vector_store.as_retriever(search_kwargs={"k": 3}) | |
| retrieved_docs = retriever.invoke(state["question"]) | |
| return {"context": retrieved_docs} | |
| def generate(state: State): | |
| docs_content = "\n\n".join(doc.page_content for doc in state["context"]) | |
| prompt = f""" | |
| 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. | |
| Context: {docs_content} | |
| Question: {state["question"]} | |
| Answer: | |
| """ | |
| input_ids = tokenizer.encode(prompt, truncation=True, max_length=max_length, return_tensors="pt") | |
| truncated_prompt = tokenizer.decode(input_ids[0]) | |
| response = chatbot(truncated_prompt, max_new_tokens=32, do_sample=True, temperature=0.2) | |
| answer = response[0]['generated_text'].split("Answer:", 1)[-1].strip() | |
| return {"answer": answer} | |
| graph_builder = StateGraph(State) | |
| graph_builder.add_node("retrieve", retrieve) | |
| graph_builder.add_node("generate", generate) | |
| graph_builder.add_edge(START, "retrieve") | |
| graph_builder.add_edge("retrieve", "generate") | |
| graph = graph_builder.compile() | |
| # --- Main chatbot function --- | |
| def tal_langchain_chatbot(user_message, history=None): | |
| # 1. Try to answer from database/rules | |
| answer = answer_technical_question(user_message, tech_info) | |
| # 2. If no answer, use the LLM | |
| if not answer or answer.lower() == "i do not know the answer to this question.": | |
| answer = llm_fallback(user_message) | |
| # 3. Update history and return | |
| if history is None: | |
| history = [] | |
| history.append({"role": "user", "content": user_message}) | |
| history.append({"role": "assistant", "content": answer}) | |
| return history, history, "" | |
| # --- Gradio UI --- | |
| custom_css = """ | |
| #chatbot-toggle-btn { | |
| position: fixed; | |
| bottom: 30px; | |
| right: 30px; | |
| z-index: 10001; | |
| background-color: #ED1C24; | |
| color: white; | |
| border: none; | |
| border-radius: 50%; | |
| width: 56px; | |
| height: 56px; | |
| font-size: 28px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.3s ease; | |
| } | |
| #chatbot-panel { | |
| position: fixed; | |
| bottom: 100px; | |
| right: 30px; | |
| z-index: 10000; | |
| width: 600px; /* Increased width */ | |
| height: 700px; /* Increased height */ | |
| background-color: #ffffff; | |
| border-radius: 20px; | |
| box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| font-family: 'Arial', sans-serif; | |
| } | |
| #chatbot-panel.hide { | |
| display: none !important; | |
| } | |
| #chat-header { | |
| background-color: #ED1C24; | |
| color: white; | |
| padding: 20px; | |
| font-weight: bold; | |
| font-size: 22px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| width: 100%; | |
| box-sizing: border-box; | |
| } | |
| #chat-header img { | |
| border-radius: 50%; | |
| width: 40px; | |
| height: 40px; | |
| } | |
| .gr-chatbot { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 20px; | |
| background-color: #f9f9f9; | |
| border-top: 1px solid #eee; | |
| border-bottom: 1px solid #eee; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| box-sizing: border-box; | |
| } | |
| .gr-textbox { | |
| padding: 16px 20px; | |
| background-color: #fff; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| border-top: 1px solid #eee; | |
| box-sizing: border-box; | |
| } | |
| .gr-textbox textarea { | |
| flex: 1; | |
| resize: none; | |
| padding: 12px; | |
| background-color: white; | |
| border: 1px solid #ccc; | |
| border-radius: 8px; | |
| font-family: inherit; | |
| font-size: 16px; | |
| box-sizing: border-box; | |
| height: 48px; | |
| line-height: 1.5; | |
| } | |
| .gr-textbox button { | |
| background-color: #ED1C24; | |
| border: none; | |
| color: white; | |
| border-radius: 8px; | |
| padding: 12px 20px; | |
| cursor: pointer; | |
| font-weight: bold; | |
| transition: background-color 0.3s ease; | |
| font-size: 16px; | |
| } | |
| .gr-textbox button:hover { | |
| background-color: #c4161c; | |
| } | |
| footer { | |
| display: none !important; | |
| } | |
| """ | |
| def toggle_visibility(current_state): | |
| new_state = not current_state | |
| return new_state, gr.update(visible=new_state) | |
| with gr.Blocks(css=custom_css) as demo: | |
| visibility_state = gr.State(False) | |
| history = gr.State([]) | |
| chatbot_toggle = gr.Button("💬", elem_id="chatbot-toggle-btn") | |
| with gr.Column(visible=False, elem_id="chatbot-panel") as chatbot_panel: | |
| gr.HTML(""" | |
| <div id='chat-header'> | |
| <img src="https://www.svgrepo.com/download/490283/pixar-lamp.svg" /> | |
| Lofty the TAL Bot | |
| </div> | |
| """) | |
| chat = gr.Chatbot(label="Chat", elem_id="chat-window", type="messages") | |
| msg = gr.Textbox(placeholder="Type your message here...", show_label=False) | |
| send = gr.Button("Send") | |
| send.click( | |
| fn=tal_langchain_chatbot, | |
| inputs=[msg, history], | |
| outputs=[chat, history, msg] | |
| ) | |
| msg.submit( | |
| fn=tal_langchain_chatbot, | |
| inputs=[msg, history], | |
| outputs=[chat, history, msg] | |
| ) | |
| chatbot_toggle.click( | |
| fn=toggle_visibility, | |
| inputs=visibility_state, | |
| outputs=[visibility_state, chatbot_panel] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |