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() | |