| | import logging |
| | from semantic_kernel import Kernel |
| | from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion |
| | from semantic_kernel.functions import kernel_function |
| | from azure.cosmos import CosmosClient |
| | from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import ( |
| | AzureChatPromptExecutionSettings, |
| | ) |
| | from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior |
| | from models.converterModels import PowerConverter |
| | from plugins.converterPlugin import ConverterPlugin |
| | import os |
| | import gradio as gr |
| |
|
| | from dotenv import load_dotenv |
| | load_dotenv() |
| |
|
| | logger = logging.getLogger("kernel") |
| | logger.setLevel(logging.DEBUG) |
| | handler = logging.StreamHandler() |
| | handler.setFormatter(logging.Formatter( |
| | "[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s" |
| | )) |
| | logger.addHandler(handler) |
| |
|
| |
|
| | |
| | kernel = Kernel() |
| |
|
| | |
| | kernel.add_service(AzureChatCompletion( |
| | service_id="chat", |
| | deployment_name=os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"), |
| | endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"), |
| | api_key=os.getenv("AZURE_OPENAI_KEY") |
| | )) |
| | allowed_fields = [ |
| | "id","artnr","type", "converter_description","strain_relief","location","dimmability","efficiency_full_load", |
| | "ip","class","nom_input_voltage_v.min","nom_input_voltage_v.max","output_voltage_v.min","output_voltage_v.max", |
| | "name","listprice","unit","lifecycle","lamps","size" |
| | ] |
| | |
| | class NL2SQLPlugin: |
| | @kernel_function(name="generate_sql", description="Generate Cosmos DB SQL query") |
| | async def generate_sql(self, question: str) -> str: |
| | sql = await self._generate_sql_helper(question) |
| | |
| | if any(command in sql.upper() for command in ["DELETE", "UPDATE", "INSERT"]): |
| | return "" |
| | |
| | |
| | |
| | |
| | |
| | if "FROM converters c" in sql: |
| | sql = sql.replace("FROM converters c", "FROM c") |
| | if "SELECT *" not in sql and "FROM c" in sql: |
| | sql = sql.replace("SELECT c.*,", "SELECT *") |
| | sql = sql.replace("SELECT c.*", "SELECT *") |
| | sql = sql.replace("SELECT c", "SELECT *") |
| | |
| | logger.info(f"finalSQL {sql}") |
| |
|
| | return sql |
| | |
| | async def _generate_sql_helper(self, question: str) -> str: |
| | from semantic_kernel.contents import ChatHistory |
| | |
| | chat_service = kernel.get_service("chat") |
| | chat_history = ChatHistory() |
| | chat_history.add_user_message(f"""Convert to Cosmos DB SQL: {question} |
| | Collection: converters (alias 'c') |
| | Fields: |
| | - type (e.g., '350mA') |
| | - artnr (numeric (int) article number e.g., 930546) |
| | - output_voltage_v: dictionary with min/max values for output voltage |
| | - output_voltage_v.min (e.g., 15) |
| | - output_voltage_v.max (e.g., 40) |
| | - nom_input_voltage_v: dictionary with min/max values for input voltage |
| | - nom_input_voltage_v.min (e.g., 198) |
| | - nom_input_voltage_v.max (e.g., 264) |
| | - lamps: dictionary with min/max values for lamp types for this converter |
| | - lamps["lamp_name"].min (e.g., 1) |
| | - lamps["lamp_name"].max (e.g., 10) |
| | - class (safety class) |
| | - dimmability (e.g. if not dimmable 'NOT DIMMABLE'. if supports dimming, 'DALI/TOUCHDIM','MAINS DIM LC' etc) |
| | - listprice (e.g., 58) |
| | - lifecycle (e.g., 'Active') |
| | - size (e.g., '150x30x30') |
| | - dimlist_type (e.g., 'DALI') |
| | - pdf_link (link to product PDF) |
| | - converter_description (e.g., 'POWERLED CONVERTER REMOTE 180mA 8W IP20 1-10V') |
| | - ip (Ingress Protection, integer values e.g., 20,67) |
| | - efficiency_full_load (e.g., 0.9) |
| | - name (e.g., 'Power Converter 350mA') |
| | - unit (e.g., 'PC') |
| | - strain_relief (e.g., "NO", "YES") |
| | SQL Guidelines (if needed): |
| | - Always use SELECT * and never individual fields |
| | - Always refer to fields in WHERE clause using c.<field_name> |
| | - For exact matches use: WHERE c.[field] = value |
| | - For ranges use: WHERE c.[field].min = X AND c.[field].max = Y |
| | - Check for dimmability support by using either !="NOT DIMMABLE" or ="NOT DIMMABLE" |
| | - Do not use AS and cast key names |
| | Return ONLY SQL without explanations""") |
| | |
| | response = await chat_service.get_chat_message_content( |
| | chat_history=chat_history, |
| | settings=AzureChatPromptExecutionSettings() |
| | ) |
| | logger.info(f"Response dB schema{response}") |
| | |
| | return str(response) |
| | |
| |
|
| | |
| | kernel.add_plugin(ConverterPlugin(logger=logger), "CosmosDBPlugin") |
| | kernel.add_plugin(NL2SQLPlugin(), "NL2SQLPlugin") |
| |
|
| | |
| | async def handle_query(user_input: str): |
| | |
| | settings = AzureChatPromptExecutionSettings( |
| | function_choice_behavior=FunctionChoiceBehavior.Auto(auto_invoke=True) |
| | ) |
| | |
| | prompt = f""" |
| | You are a converter database expert. Process this user query: |
| | {user_input} |
| | |
| | Available functions: |
| | - generate_sql: Creates SQL queries (use only for complex queries or schema keywords) |
| | - query_converters: Executes SQL queries |
| | - get_compatible_lamps: Simple artnr-based lamp queries |
| | - get_converters_by_lamp_type: Simple lamp type searches |
| | - get_lamp_limits: Simple artnr+lamp combinations |
| | - RAG_search: questions about dimmability or current (if query contains mains c, dali, 350mA, dali drivers, dimming) |
| | |
| | Decision Flow: |
| | 1. Identify synonyms : |
| | output voltage = voltage forward = forward voltage = Vf |
| | Driver = ledconverter = converter = power supply = gear |
| | lamps = luminares |
| | |
| | 2. Use simple functions if query matches these patterns: |
| | - "lamps for [artnr]" → get_compatible_lamps |
| | - "converters for [lamp type]" → get_converters_by_lamp_type |
| | - "min/max [lamp] for [artnr]" → get_lamp_limits |
| | |
| | 3. Use SQL generation ONLY when: |
| | - Query contains schema keywords: voltage, price, type, ip, efficiency, size, class |
| | - Combining multiple conditions (AND/OR/NOT) |
| | - Needs complex filtering/sorting |
| | - Requesting technical specifications |
| | |
| | Examples: |
| | User: "Show IP67 converters under €100" → generate_sql |
| | User: "What lamps are compatible with 930560?" → get_compatible_lamps |
| | User: "What converters are compatible with haloled lamps?" → get_converters_by_lamp_type |
| | User: "Voltage range for 930562" → generate_sql |
| | """ |
| | |
| | result = await kernel.invoke_prompt( |
| | prompt=prompt, |
| | settings=settings |
| | ) |
| | |
| | return str(result) |
| |
|
| | |
| | async def main(): |
| |
|
| | while True: |
| | try: |
| | query = input("User: ") |
| | if query.lower() in ["exit", "quit"]: |
| | break |
| |
|
| | response = await handle_query(query) |
| | print(response) |
| | |
| | except KeyboardInterrupt: |
| | break |
| |
|
| |
|
| |
|
| | |
| |
|
| | 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; |
| | height: 700px; |
| | background-color: #ffffff; |
| | border-radius: 20px; |
| | box-shadow: 0 4px 24px rgba(0,0,0,0.25); |
| | overflow: hidden; |
| | display: flex; |
| | flex-direction: column; |
| | justify-content: space-between; /* keep input box pinned at the bottom */ |
| | font-family: 'Arial', sans-serif; |
| | } |
| | |
| | #chatbot-panel.hide { |
| | display: none !important; |
| | } |
| | |
| | #chat-header { |
| | background-color: #ED1C24; |
| | color: white; |
| | padding: 16px; |
| | font-weight: bold; |
| | font-size: 16px; |
| | display: flex; |
| | align-items: center; |
| | gap: 12px; |
| | } |
| | |
| | #chat-header img { |
| | border-radius: 50%; |
| | width: 32px; |
| | height: 32px; |
| | } |
| | |
| | .gr-chatbot { |
| | flex: 1; |
| | overflow-y: auto; |
| | padding: 12px; |
| | background-color: #f8f8f8; |
| | border: none; |
| | } |
| | |
| | .gr-textbox { |
| | border-top: 1px solid #eee; |
| | padding: 10px; |
| | background-color: #fff; |
| | display: flex; |
| | align-items: center; |
| | justify-content: space-between; |
| | gap: 10px; |
| | } |
| | |
| | .gr-textbox textarea { |
| | flex: 1; |
| | resize: none; |
| | padding: 10px; |
| | background-color: white; |
| | border: 1px solid #ccc; |
| | border-radius: 8px; |
| | font-family: inherit; |
| | font-size: 14px; |
| | } |
| | |
| | footer { |
| | display: none !important; |
| | } |
| | """ |
| | panel_visible = False |
| |
|
| | def toggle_panel(): |
| | global panel_visible |
| | panel_visible = not panel_visible |
| | return gr.Column(visible=panel_visible) |
| |
|
| | with gr.Blocks(css=custom_css) as demo: |
| | |
| | toggle_btn = gr.Button("💬", elem_id="chatbot-toggle-btn") |
| |
|
| | |
| | chat_panel = gr.Column(visible=panel_visible, elem_id="chatbot-panel") |
| | with chat_panel: |
| | |
| | with gr.Row(elem_id="chat-header"): |
| | gr.HTML(""" |
| | <div id='chat-header'> |
| | <img src="https://www.svgrepo.com/download/490283/pixar-lamp.svg" /> |
| | Lofty the TAL Bot |
| | </div> |
| | """) |
| | |
| | chatbot = gr.Chatbot(elem_id="gr-chatbot", type="messages") |
| | msg = gr.Textbox(placeholder="Type your question here...", elem_id="gr-textbox") |
| | clear = gr.ClearButton([msg, chatbot]) |
| |
|
| |
|
| | |
| | async def respond(message, chat_history): |
| | response = await handle_query(message) |
| | |
| | |
| | |
| | chat_history.append({"role": "user", "content": message}) |
| | chat_history.append({"role": "assistant", "content": response}) |
| | return "", chat_history |
| |
|
| | msg.submit(respond, [msg, chatbot], [msg, chatbot]) |
| | toggle_btn.click(toggle_panel, outputs=chat_panel) |
| |
|
| | demo.launch(share=True) |