Sathvika-Alla commited on
Commit
e718856
·
verified ·
1 Parent(s): 2e4d70f

Upload folder using huggingface_hub

Browse files
.env ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ AZURE_OPENAI_API_KEY = 74fO9RE4s4f7HTSd9SM19Adw6rnECwUuBnfY593dPI7xSHa057RHJQQJ99BEACfhMk5XJ3w3AAAAACOGFVJQ
2
+
3
+ OPENAI_API_TYPE = azure
4
+
5
+ AZURE_OPENAI_API_VERSION = 2024-12-01-preview
6
+ AZURE_OPENAI_DEPLOYMENT_NAME = gpt-4o-mini
7
+ AZURE_OPENAI_ENDPOINT = https://tal-chatbot-resource2.cognitiveservices.azure.com/openai/deployments/gpt-4o-mini/chat/completions?api-version=2025-01-01-preview
8
+
9
+ OPENAI_EMBEDDINGS_MODEL_NAME = text-embedding-ada-002
10
+ OPENAI_EMBEDDINGS_MODEL_DEPLOYMENT = text-embedding-ada-002
11
+ OPENAI_API_ENDPOINT = https://tal-chatbot-resource2.cognitiveservices.azure.com/
12
+
13
+ LANGSMITH_API_KEY = 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
14
+ LANGSMITH_TRACING=true
15
+
16
+
17
+ AZURE_COSMOS_DB_ENDPOINT = https://tal-chatbot.documents.azure.com:443/
18
+ AZURE_COSMOS_DB_KEY = 6XG3CwRPJeHWAufiMNbWNS2PhBfoSMtPEP5qNGPQJFulXqgJfR9K3xO1sgegOq9vkjwSgmIDqA7hACDbWIzPVA==
19
+ AZURE_COSMOS_DB_DATABASE = tal-chatbot
README.md CHANGED
@@ -1,12 +1,6 @@
1
  ---
2
- title: TAL Chatbot
3
- emoji: 💻
4
- colorFrom: yellow
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 5.32.1
8
- app_file: app.py
9
- pinned: false
10
  ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: TAL-Chatbot
3
+ app_file: chatbot-gradio.py
 
 
4
  sdk: gradio
5
+ sdk_version: 5.31.0
 
 
6
  ---
 
 
__pycache__/cosmosConnector.cpython-311.pyc ADDED
Binary file (8.45 kB). View file
 
chatbot-gradio.py ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from semantic_kernel import Kernel
3
+ from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
4
+ from semantic_kernel.functions import kernel_function
5
+ from azure.cosmos import CosmosClient
6
+ from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import (
7
+ AzureChatPromptExecutionSettings,
8
+ )
9
+ from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
10
+ from models.converterModels import PowerConverter
11
+ from plugins.converterPlugin import ConverterPlugin
12
+ import os
13
+ import gradio as gr
14
+
15
+ from dotenv import load_dotenv
16
+ load_dotenv()
17
+
18
+ logger = logging.getLogger("kernel")
19
+ logger.setLevel(logging.DEBUG)
20
+ handler = logging.StreamHandler()
21
+ handler.setFormatter(logging.Formatter(
22
+ "[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s"
23
+ ))
24
+ logger.addHandler(handler)
25
+
26
+
27
+ # Initialize Semantic Kernel
28
+ kernel = Kernel()
29
+
30
+ # Add Azure OpenAI Chat Service
31
+ kernel.add_service(AzureChatCompletion(
32
+ service_id="chat",
33
+ deployment_name=os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"),
34
+ endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
35
+ api_key=os.getenv("AZURE_OPENAI_KEY")
36
+ ))
37
+
38
+ # SQL Generation Plugin
39
+ class NL2SQLPlugin:
40
+ @kernel_function(name="generate_sql", description="Generate Cosmos DB SQL query")
41
+ async def generate_sql(self, question: str) -> str:
42
+ sql = await self._generate_sql_helper(question)
43
+ # if ["DELETE", "UPDATE", "INSERT"] in sql:
44
+ # return ""
45
+ if "FROM converters c" in sql:
46
+ sql = sql.replace("FROM converters c", "FROM c")
47
+ if "SELECT *" not in sql and "FROM c" in sql:
48
+ sql = sql.replace("SELECT c.*,", "SELECT *")
49
+ sql = sql.replace("SELECT c.*", "SELECT *")
50
+ sql = sql.replace("SELECT c", "SELECT *")
51
+
52
+ return sql
53
+
54
+ async def _generate_sql_helper(self, question: str) -> str:
55
+ from semantic_kernel.contents import ChatHistory
56
+
57
+ chat_service = kernel.get_service("chat")
58
+ chat_history = ChatHistory()
59
+ chat_history.add_user_message(f"""Convert to Cosmos DB SQL: {question}
60
+ Collection: converters (alias 'c')
61
+ Fields:
62
+ - type (e.g., '350mA')
63
+ - artnr (numeric (int) article number e.g., 930546)
64
+ - output_voltage_v: dictionary with min/max values for output voltage
65
+ - output_voltage_v.min (e.g., 15)
66
+ - output_voltage_v.max (e.g., 40)
67
+ - nom_input_voltage_v: dictionary with min/max values for input voltage
68
+ - nom_input_voltage_v.min (e.g., 198)
69
+ - nom_input_voltage_v.max (e.g., 264)
70
+ - lamps: dictionary with min/max values for lamp types for this converter
71
+ - lamps["lamp_name"].min (e.g., 1)
72
+ - lamps["lamp_name"].max (e.g., 10)
73
+ - class (safety class)
74
+ - dimmability (e.g. if not dimmable 'NOT DIMMABLE'. if supports dimming, 'DALI/TOUCHDIM','MAINS DIM LC' etc)
75
+ - listprice (e.g., 58)
76
+ - lifecycle (e.g., 'Active')
77
+ - size (e.g., '150x30x30')
78
+ - dimlist_type (e.g., 'DALI')
79
+ - pdf_link (link to product PDF)
80
+ - converter_description (e.g., 'POWERLED CONVERTER REMOTE 180mA 8W IP20 1-10V')
81
+ - ip (Ingress Protection, integer values e.g., 20,67)
82
+ - efficiency_full_load (e.g., 0.9)
83
+ - name (e.g., 'Power Converter 350mA')
84
+ - unit (e.g., 'PC')
85
+ - strain_relief (e.g., "NO", "YES")
86
+ Return ONLY SQL without explanations""")
87
+
88
+ response = await chat_service.get_chat_message_content(
89
+ chat_history=chat_history,
90
+ settings=AzureChatPromptExecutionSettings()
91
+ )
92
+
93
+ return str(response)
94
+
95
+
96
+ # Register plugins
97
+ kernel.add_plugin(ConverterPlugin(logger=logger), "CosmosDBPlugin")
98
+ kernel.add_plugin(NL2SQLPlugin(), "NL2SQLPlugin")
99
+
100
+ # Updated query handler using function calling
101
+ async def handle_query(user_input: str):
102
+
103
+ settings = AzureChatPromptExecutionSettings(
104
+ function_choice_behavior=FunctionChoiceBehavior.Auto(auto_invoke=True)
105
+ )
106
+
107
+ prompt = f"""
108
+ You are a converter database expert. Process this user query:
109
+ {user_input}
110
+
111
+ Available functions:
112
+ - generate_sql: Creates SQL queries (use only for complex queries or schema keywords)
113
+ - query_converters: Executes SQL queries
114
+ - get_compatible_lamps: Simple artnr-based lamp queries
115
+ - get_converters_by_lamp_type: Simple lamp type searches
116
+ - get_lamp_limits: Simple artnr+lamp combinations
117
+
118
+ Decision Flow:
119
+ 1. Use simple functions if query matches these patterns:
120
+ - "lamps for [artnr]" → get_compatible_lamps
121
+ - "converters for [lamp type]" → get_converters_by_lamp_type
122
+ - "min/max [lamp] for [artnr]" → get_lamp_limits
123
+
124
+ 2. Use SQL generation ONLY when:
125
+ - Query contains schema keywords: voltage, price, type, ip, efficiency, size, class, dimmability
126
+ - Combining multiple conditions (AND/OR/NOT)
127
+ - Needs complex filtering/sorting
128
+ - Requesting technical specifications
129
+
130
+ SQL Guidelines (if needed):
131
+ 1. Always use SELECT * instead of field lists
132
+ 2. For exact matches use: WHERE c.[field] = value
133
+ 3. For ranges use: WHERE c.[field].min >= X AND c.[field].max <= Y
134
+ 4. Do not use AS and cast key names
135
+
136
+ Examples:
137
+ User: "Show IP67 converters under €100" → generate_sql
138
+ User: "What lamps are compatible with 930560?" → get_compatible_lamps
139
+ User: "What converters are compatible with haloled lamps?" → get_converters_by_lamp_type
140
+ User: "Voltage range for 930562" → generate_sql
141
+ """
142
+
143
+ result = await kernel.invoke_prompt(
144
+ prompt=prompt,
145
+ settings=settings
146
+ )
147
+
148
+ return str(result)
149
+
150
+ # Example usage
151
+ async def main():
152
+
153
+ while True:
154
+ try:
155
+ query = input("User: ")
156
+ if query.lower() in ["exit", "quit"]:
157
+ break
158
+
159
+ response = await handle_query(query)
160
+ print(response)
161
+
162
+ except KeyboardInterrupt:
163
+ break
164
+
165
+
166
+
167
+ # --- Gradio UI ---
168
+
169
+ custom_css = """
170
+ #chatbot-toggle-btn {
171
+ position: fixed;
172
+ bottom: 30px;
173
+ right: 30px;
174
+ z-index: 10001;
175
+ background-color: #ED1C24;
176
+ color: white;
177
+ border: none;
178
+ border-radius: 50%;
179
+ width: 56px;
180
+ height: 56px;
181
+ font-size: 28px;
182
+ font-weight: bold;
183
+ cursor: pointer;
184
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ transition: all 0.3s ease;
189
+ }
190
+
191
+ #chatbot-panel {
192
+ position: fixed;
193
+ bottom: 100px;
194
+ right: 30px;
195
+ z-index: 10000;
196
+ width: 600px;
197
+ height: 700px;
198
+ background-color: #ffffff;
199
+ border-radius: 20px;
200
+ box-shadow: 0 4px 24px rgba(0,0,0,0.25);
201
+ overflow: hidden;
202
+ display: flex;
203
+ flex-direction: column;
204
+ justify-content: space-between; /* keep input box pinned at the bottom */
205
+ font-family: 'Arial', sans-serif;
206
+ }
207
+
208
+ #chatbot-panel.hide {
209
+ display: none !important;
210
+ }
211
+
212
+ #chat-header {
213
+ background-color: #ED1C24;
214
+ color: white;
215
+ padding: 16px;
216
+ font-weight: bold;
217
+ font-size: 16px;
218
+ display: flex;
219
+ align-items: center;
220
+ gap: 12px;
221
+ }
222
+
223
+ #chat-header img {
224
+ border-radius: 50%;
225
+ width: 32px;
226
+ height: 32px;
227
+ }
228
+
229
+ .gr-chatbot {
230
+ flex: 1;
231
+ overflow-y: auto;
232
+ padding: 12px;
233
+ background-color: #f8f8f8;
234
+ border: none;
235
+ }
236
+
237
+ .gr-textbox {
238
+ border-top: 1px solid #eee;
239
+ padding: 10px;
240
+ background-color: #fff;
241
+ display: flex;
242
+ align-items: center;
243
+ justify-content: space-between;
244
+ gap: 10px;
245
+ }
246
+
247
+ .gr-textbox textarea {
248
+ flex: 1;
249
+ resize: none;
250
+ padding: 10px;
251
+ background-color: white;
252
+ border: 1px solid #ccc;
253
+ border-radius: 8px;
254
+ font-family: inherit;
255
+ font-size: 14px;
256
+ }
257
+
258
+ footer {
259
+ display: none !important;
260
+ }
261
+ """
262
+ panel_visible = False
263
+
264
+ def toggle_panel():
265
+ global panel_visible
266
+ panel_visible = not panel_visible
267
+ return gr.Column(visible=panel_visible)
268
+
269
+ with gr.Blocks(css=custom_css) as demo:
270
+ # Toggle button (floating action button)
271
+ toggle_btn = gr.Button("💬", elem_id="chatbot-toggle-btn")
272
+
273
+ # Chat panel (initially hidden)
274
+ chat_panel = gr.Column(visible=panel_visible, elem_id="chatbot-panel")
275
+ with chat_panel:
276
+ # Chat header
277
+ with gr.Row(elem_id="chat-header"):
278
+ gr.HTML("""
279
+ <div id='chat-header'>
280
+ <img src="https://www.svgrepo.com/download/490283/pixar-lamp.svg" />
281
+ Lofty the TAL Bot
282
+ </div>
283
+ """)
284
+ # Chatbot and input
285
+ chatbot = gr.Chatbot(elem_id="gr-chatbot", type="messages")
286
+ msg = gr.Textbox(placeholder="Type your question here...", elem_id="gr-textbox")
287
+ clear = gr.ClearButton([msg, chatbot])
288
+
289
+
290
+ # Function to handle messages
291
+ async def respond(message, chat_history):
292
+ response = await handle_query(message)
293
+ # Convert existing history to OpenAI format if it's in tuples
294
+
295
+ # Add new messages
296
+ chat_history.append({"role": "user", "content": message})
297
+ chat_history.append({"role": "assistant", "content": response})
298
+ return "", chat_history
299
+
300
+ msg.submit(respond, [msg, chatbot], [msg, chatbot])
301
+ toggle_btn.click(toggle_panel, outputs=chat_panel)
302
+
303
+ demo.launch(share=True)
chatbot.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from semantic_kernel import Kernel
3
+ from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
4
+ from semantic_kernel.functions import kernel_function
5
+ from azure.cosmos import CosmosClient
6
+ from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import (
7
+ AzureChatPromptExecutionSettings,
8
+ )
9
+ from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
10
+ from models.converterModels import PowerConverter
11
+ from plugins.converterPlugin import ConverterPlugin
12
+ import os
13
+ from dotenv import load_dotenv
14
+ load_dotenv()
15
+
16
+ logger = logging.getLogger("kernel")
17
+ logger.setLevel(logging.DEBUG)
18
+ handler = logging.StreamHandler()
19
+ handler.setFormatter(logging.Formatter(
20
+ "[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s"
21
+ ))
22
+ logger.addHandler(handler)
23
+
24
+
25
+ # Initialize Semantic Kernel
26
+ kernel = Kernel()
27
+
28
+ # Add Azure OpenAI Chat Service
29
+ kernel.add_service(AzureChatCompletion(
30
+ service_id="chat",
31
+ deployment_name=os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"),
32
+ endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
33
+ api_key=os.getenv("AZURE_OPENAI_KEY")
34
+ ))
35
+
36
+ # SQL Generation Plugin
37
+ class NL2SQLPlugin:
38
+ @kernel_function(name="generate_sql", description="Generate Cosmos DB SQL query")
39
+ async def generate_sql(self, question: str) -> str:
40
+ sql = await self._generate_sql_helper(question)
41
+ # if ["DELETE", "UPDATE", "INSERT"] in sql:
42
+ # return ""
43
+ if "FROM converters c" in sql:
44
+ sql = sql.replace("FROM converters c", "FROM c")
45
+ if "SELECT *" not in sql and "FROM c" in sql:
46
+ sql = sql.replace("SELECT c.*,", "SELECT *")
47
+ sql = sql.replace("SELECT c.*", "SELECT *")
48
+ sql = sql.replace("SELECT c", "SELECT *")
49
+
50
+ return sql
51
+
52
+ async def _generate_sql_helper(self, question: str) -> str:
53
+ from semantic_kernel.contents import ChatHistory
54
+
55
+ chat_service = kernel.get_service("chat")
56
+ chat_history = ChatHistory()
57
+ chat_history.add_user_message(f"""Convert to Cosmos DB SQL: {question}
58
+ Collection: converters (alias 'c')
59
+ Fields:
60
+ - type (e.g., '350mA')
61
+ - artnr (numeric (int) article number e.g., 930546)
62
+ - output_voltage_v: dictionary with min/max values for output voltage
63
+ - output_voltage_v.min (e.g., 15)
64
+ - output_voltage_v.max (e.g., 40)
65
+ - nom_input_voltage_v: dictionary with min/max values for input voltage
66
+ - nom_input_voltage_v.min (e.g., 198)
67
+ - nom_input_voltage_v.max (e.g., 264)
68
+ - lamps: dictionary with min/max values for lamp types for this converter
69
+ - lamps["lamp_name"].min (e.g., 1)
70
+ - lamps["lamp_name"].max (e.g., 10)
71
+ - class (safety class)
72
+ - dimmability (e.g. if not dimmable 'NOT DIMMABLE'. if supports dimming, 'DALI/TOUCHDIM','MAINS DIM LC' etc)
73
+ - listprice (e.g., 58)
74
+ - lifecycle (e.g., 'Active')
75
+ - size (e.g., '150x30x30')
76
+ - dimlist_type (e.g., 'DALI')
77
+ - pdf_link (link to product PDF)
78
+ - converter_description (e.g., 'POWERLED CONVERTER REMOTE 180mA 8W IP20 1-10V')
79
+ - ip (Ingress Protection, integer values e.g., 20,67)
80
+ - efficiency_full_load (e.g., 0.9)
81
+ - name (e.g., 'Power Converter 350mA')
82
+ - unit (e.g., 'PC')
83
+ - strain_relief (e.g., "NO", "YES")
84
+ Return ONLY SQL without explanations""")
85
+
86
+ response = await chat_service.get_chat_message_content(
87
+ chat_history=chat_history,
88
+ settings=AzureChatPromptExecutionSettings()
89
+ )
90
+
91
+ return str(response)
92
+
93
+
94
+ # Register plugins
95
+ kernel.add_plugin(ConverterPlugin(logger=logger), "CosmosDBPlugin")
96
+ kernel.add_plugin(NL2SQLPlugin(), "NL2SQLPlugin")
97
+
98
+ # Updated query handler using function calling
99
+ async def handle_query(user_input: str):
100
+
101
+ settings = AzureChatPromptExecutionSettings(
102
+ function_choice_behavior=FunctionChoiceBehavior.Auto(auto_invoke=True)
103
+ )
104
+
105
+ prompt = f"""
106
+ You are a converter database expert. Process this user query:
107
+ {user_input}
108
+
109
+ Available functions:
110
+ - generate_sql: Creates SQL queries (use only for complex queries or schema keywords)
111
+ - query_converters: Executes SQL queries
112
+ - get_compatible_lamps: Simple artnr-based lamp queries
113
+ - get_converters_by_lamp_type: Simple lamp type searches
114
+ - get_lamp_limits: Simple artnr+lamp combinations
115
+
116
+ Decision Flow:
117
+ 1. Use simple functions if query matches these patterns:
118
+ - "lamps for [artnr]" → get_compatible_lamps
119
+ - "converters for [lamp type]" → get_converters_by_lamp_type
120
+ - "min/max [lamp] for [artnr]" → get_lamp_limits
121
+
122
+ 2. Use SQL generation ONLY when:
123
+ - Query contains schema keywords: voltage, price, type, ip, efficiency, size, class, dimmability
124
+ - Combining multiple conditions (AND/OR/NOT)
125
+ - Needs complex filtering/sorting
126
+ - Requesting technical specifications
127
+
128
+ SQL Guidelines (if needed):
129
+ 1. Always use SELECT * instead of field lists
130
+ 2. For exact matches use: WHERE c.[field] = value
131
+ 3. For EXACT ranges ALWAYS use: SELECT * FROM c WHERE c.[field].min = X AND c.[field].max = Y and NEVER >= <=
132
+ 4. Limit results with SELECT TOP 10
133
+
134
+ Examples:
135
+ User: "Show IP67 converters under €100" → generate_sql
136
+ User: "What lamps are compatible with 930560?" → get_compatible_lamps
137
+ User: "What converters are compatible with haloled lamps?" → get_converters_by_lamp_type
138
+ User: "Voltage range for 930562" → generate_sql
139
+ """
140
+
141
+ result = await kernel.invoke_prompt(
142
+ prompt=prompt,
143
+ settings=settings
144
+ )
145
+
146
+ return str(result)
147
+
148
+ # Example usage
149
+ async def main():
150
+
151
+ while True:
152
+ try:
153
+ query = input("User: ")
154
+ if query.lower() in ["exit", "quit"]:
155
+ break
156
+
157
+ response = await handle_query(query)
158
+ print(response)
159
+
160
+ except KeyboardInterrupt:
161
+ break
162
+
163
+ if __name__ == "__main__":
164
+ import asyncio
165
+ asyncio.run(main())
cosmosConnector.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # cosmosConnector.py
2
+ from jsonschema import ValidationError
3
+ from models.converterModels import PowerConverter
4
+ import os
5
+ from azure.cosmos import CosmosClient
6
+ from typing import List, Optional, Dict
7
+ from langchain_openai import AzureOpenAIEmbeddings
8
+ from rapidfuzz import fuzz
9
+ import logging
10
+ import os
11
+ from dotenv import load_dotenv
12
+
13
+ load_dotenv()
14
+ # Initialize logging
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class CosmosLampHandler:
18
+
19
+ def __init__(self, logger: Optional[logging.Logger] = None):
20
+ self.client = CosmosClient(
21
+ os.getenv("AZURE_COSMOS_DB_ENDPOINT"),
22
+ os.getenv("AZURE_COSMOS_DB_KEY")
23
+ )
24
+ self.database = self.client.get_database_client("TAL_DB")
25
+ self.container = self.database.get_container_client("Converters")
26
+ self.logger = logger
27
+
28
+ async def get_compatible_lamps(self, artnr: int) -> List[str]:
29
+ """Get compatible lamps for a converter with fuzzy matching"""
30
+ try:
31
+ parameters = [{"name": "@artnr", "value": artnr}]
32
+ query = "SELECT * FROM c WHERE c.artnr = @artnr"
33
+
34
+ # Collect results properly
35
+ results = [item for item in list(self.container.query_items(
36
+ query=query,
37
+ parameters=parameters
38
+ ))]
39
+
40
+ if not results:
41
+ return []
42
+
43
+ return list(results[0]["lamps"].keys())
44
+
45
+ except Exception as e:
46
+ logger.error(f"Failed to get compatible lamps: {str(e)}")
47
+ return []
48
+
49
+ async def get_converters_by_lamp_type(self, lamp_type: str, threshold: int = 75) -> List[PowerConverter]:
50
+ """Get converters with fuzzy-matched lamp types"""
51
+ try:
52
+ # Case-insensitive search with fuzzy matching
53
+ query = """
54
+ SELECT
55
+ *
56
+ FROM c WHERE IS_DEFINED(c.lamps)"""
57
+ converters = []
58
+ results = list(self.container.query_items(
59
+ query=query,
60
+ enable_cross_partition_query=True))
61
+ for item in results:
62
+ lamp_keys = item.get("lamps", {}).keys()
63
+ matches = [key for key in lamp_keys
64
+ if fuzz.ratio(key.lower(), lamp_type.lower()) >= threshold]
65
+
66
+ if matches:
67
+ converters.append(PowerConverter(**item))
68
+
69
+ return converters
70
+
71
+ except Exception as e:
72
+ logger.error(f"Lamp type search failed: {str(e)}")
73
+ return []
74
+
75
+
76
+ async def get_lamp_limits(self, artnr: int, lamp_type: str) -> Dict[str, int]:
77
+ """Get lamp limits with typo tolerance"""
78
+ try:
79
+ parameters = [{"name": "@artnr", "value": artnr}]
80
+ query = """
81
+ SELECT c.lamps FROM c
82
+ WHERE c.artnr = @artnr
83
+ """
84
+ results_iter = list(self.container.query_items(
85
+ query=query,
86
+ parameters=parameters
87
+ ))
88
+
89
+ results = [item for item in results_iter] # Collect results asynchronously
90
+
91
+ if not results:
92
+ return {}
93
+
94
+ lamps = results[0]["lamps"]
95
+
96
+ # Fuzzy match lamp type
97
+ best_match = max(
98
+ lamps.keys(),
99
+ key=lambda x: fuzz.ratio(x.lower(), lamp_type.lower())
100
+ )
101
+
102
+ if fuzz.ratio(best_match.lower(), lamp_type.lower()) < 65:
103
+ raise ValueError("No matching lamp type found")
104
+
105
+ return {
106
+ "min": int(lamps[best_match]["min"]),
107
+ "max": int(lamps[best_match]["max"])
108
+ }
109
+
110
+ except Exception as e:
111
+ logger.error(f"Failed to get lamp limits: {str(e)}")
112
+ raise
113
+
114
+
115
+
116
+ async def query_converters(self, query: str) -> str:
117
+ try:
118
+ print(f"Executing query: {query}")
119
+ items = list(self.container.query_items(
120
+ query=query,
121
+ enable_cross_partition_query=True
122
+ ))
123
+ print(f"Query returned {len(items)} items")
124
+ items = items[:10]
125
+ # self.logger.debug(f"Raw items: {items}")
126
+
127
+ items = [PowerConverter(**item) for item in items] if items else []
128
+
129
+ self.logger.info(f"Query returned {len(items)} items after conversion")
130
+
131
+ return str(items)
132
+ except Exception as e:
133
+ self.logger.info(f"Query failed: {str(e)}")
134
+ return f"Query failed: {str(e)}"
models/__pycache__/converterModels.cpython-311.pyc ADDED
Binary file (4.33 kB). View file
 
models/converterModels.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # models/converterModels.py
2
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, validator
3
+ from typing import Dict, Optional
4
+
5
+ class LampConnections(BaseModel):
6
+ min: float
7
+ max: float
8
+
9
+ @field_validator('min', 'max', mode='before')
10
+ def convert_values(cls, v):
11
+ v_str = str(v)
12
+ # Handle comma decimals
13
+ v_str = v_str.replace(',', '.')
14
+ return float(v_str)
15
+
16
+
17
+ class VoltageRange(BaseModel):
18
+ min: float
19
+ max: float
20
+
21
+ @field_validator('min', 'max', mode='before')
22
+ def convert_values(cls, v):
23
+ v_str = str(v)
24
+ # Handle comma decimals
25
+ v_str = v_str.replace(',', '.')
26
+ return float(v_str)
27
+
28
+
29
+ class PowerConverter(BaseModel):
30
+ doc_id: Optional[str] = Field(None, alias="id")
31
+ artnr: Optional[int] = Field(None, alias="artnr")
32
+ ip_rating: Optional[int] = Field(None, alias="ip")
33
+ lamps: Optional[Dict[str, LampConnections]] = Field(default_factory=dict, alias="lamps")
34
+
35
+ type: Optional[str] = Field(None, alias="type")
36
+ name: Optional[str] = Field(None, alias="name")
37
+ efficiency: Optional[float] = Field(None, alias="efficiency_full_load")
38
+ pdf_link: Optional[str] = Field(None, alias="pdf_link")
39
+ converter_description: Optional[str] = Field(None, alias="converter_description")
40
+
41
+ nom_input_voltage: Optional[VoltageRange] = Field(None, alias="nom_input_voltage_v")
42
+ output_voltage:Optional[VoltageRange]= Field(None, alias="output_voltage_v")
43
+
44
+ unit: Optional[str] = Field(None, alias="unit")
45
+ price: Optional[float] = Field(None, alias="listprice")
46
+ life_cycle: Optional[str] = Field(None, alias="lifecycle")
47
+ size: Optional[str] = Field(None, alias="size")
48
+ ccr_amplitude: Optional[str] = Field(None, alias="ccr_amplitude")
49
+ dimmability: Optional[str] = Field(None, alias="dimmability")
50
+ dim_list_type: Optional[str] = Field(None, alias="dimlist_type")
51
+
52
+ strain_relief: Optional[str] = Field(None, alias="strain_relief")
53
+ gross_weight: Optional[float] = Field(None, alias="gross_weight")
54
+
55
+ similarity_score: Optional[float] = Field(None, alias="SimilarityScore") # For hybrid search results
56
+ model_config = ConfigDict(
57
+ populate_by_name=True, # Critical fix
58
+
59
+ extra="ignore"
60
+ )
plugins/__pycache__/converterPlugin.cpython-311.pyc ADDED
Binary file (5.46 kB). View file
 
plugins/__pycache__/sqlGenerationPlugin.cpython-311.pyc ADDED
Binary file (3.78 kB). View file
 
plugins/converterPlugin.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #converterPlugin.py
2
+ from typing import Annotated, Optional
3
+ from cosmosConnector import CosmosLampHandler
4
+ from semantic_kernel.functions import kernel_function
5
+
6
+ class ConverterPlugin:
7
+ def __init__(self, logger):
8
+ self.logger = logger
9
+ self.db = CosmosLampHandler(logger=logger)
10
+
11
+
12
+ @kernel_function(
13
+ name="query_converters",
14
+ description="Execute SQL query against Cosmos DB converters collection"
15
+ )
16
+ async def query_converters(self, query: str) -> str:
17
+ try:
18
+ items = await self.db.query_converters(query)
19
+ self.logger.info(f"Executed query: {query}")
20
+ if not items:
21
+ return "No items found for the given query."
22
+ return str(items)
23
+ except Exception as e:
24
+ return f"Query failed: {str(e)}"
25
+
26
+
27
+ @kernel_function
28
+ async def get_compatible_lamps(
29
+ self,
30
+ artnr: Annotated[int, "Converter ARTNR (partition key)"]
31
+ ) -> str:
32
+ """Get compatible lamps for a converter by ARTNR"""
33
+ try:
34
+ lamps = await self.db.get_compatible_lamps(artnr)
35
+ self.logger.info(f"Used get_compatible_lamps with ARTNR: {artnr}")
36
+ return f"Compatible lamps: {', '.join(lamps)}" if lamps else "No lamps found"
37
+ # return "\n".join([f"{c.model_dump()})" for c in lamps]) if lamps else "No lamps found"
38
+ except Exception as e:
39
+ return f"Error retrieving compatible lamps: {str(e)}"
40
+
41
+
42
+ @kernel_function(
43
+ name="get_converters_by_lamp_type",
44
+ description="Find converters compatible with a specific lamp type"
45
+ )
46
+ async def get_converters_by_lamp_type(
47
+ self,
48
+ lamp_type: Annotated[str, "Lamp model (e.g., Haloled, B4)"]
49
+ ) -> str:
50
+ """Find converters compatible with specific lamp type"""
51
+ try:
52
+ converters = await self.db.get_converters_by_lamp_type(lamp_type)
53
+ self.logger.info(f"Used get_converters_by_lamp_type with lamp_type: {lamp_type}")
54
+ if not converters:
55
+ return "No compatible converters found"
56
+ # return "\n".join([f"{c.name} (ARTNR: {c.artnr})\nTYPE: {c.type}\nMANUAL: {c.pdf_link}" for c in converters])
57
+ return "\n".join([f"{c.model_dump()})" for c in converters]) if converters else "No converters found"
58
+ except Exception as e:
59
+ return f"Error retrieving converters: {str(e)}"
60
+
61
+
62
+ @kernel_function(
63
+ name="get_lamp_limits",
64
+ description="Get min/max lamps for a converter by ARTNR and lamp type"
65
+ )
66
+ async def get_lamp_limits(
67
+ self,
68
+ artnr: Annotated[int, "Converter ARTNR"],
69
+ lamp_type: Annotated[str, "Lamp model (e.g., Haloled)"]
70
+ ) -> str:
71
+ """Get min/max lamps for a converter"""
72
+ try:
73
+ limits = await self.db.get_lamp_limits(artnr, lamp_type)
74
+ self.logger.info(f"Used get_lamp_limits with ARTNR: {artnr} and lamp_type: {lamp_type}")
75
+ return f"{lamp_type}: Min {limits['min']} - Max {limits['max']} lamps"
76
+ except Exception as e:
77
+ return f"Error retrieving lamp limits: {str(e)}"
questions.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ what lamps can i use with 930599
2
+ how many thinksmall halosphere double lamps can i use with 930599
3
+ cheapest converter that supports Beaufort lamps
4
+ what is the converter that supports the most number of single led xpe lamps
5
+ what is the output voltage range of converter 930560
6
+ give me converters that are not dimmable and also support Haloled lamps
7
+ which converters support boa wc lamps and have an efficiency greater than 0.7
8
+ what is the input voltage range of converter 930544
9
+ give me converters with an input voltage range of 230-240
10
+ which converters are of type 500mA and support dimming
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ semantic-kernel
sql-genonly-chatbot.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from semantic_kernel import Kernel
3
+ from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
4
+ from semantic_kernel.functions import kernel_function
5
+ from azure.cosmos import CosmosClient
6
+ from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import (
7
+ AzureChatPromptExecutionSettings,
8
+ )
9
+ from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
10
+ from models.converterModels import PowerConverter
11
+
12
+ import os
13
+ from dotenv import load_dotenv
14
+ load_dotenv()
15
+
16
+ logger = logging.getLogger("kernel")
17
+ logger.setLevel(logging.DEBUG)
18
+ handler = logging.StreamHandler()
19
+ handler.setFormatter(logging.Formatter(
20
+ "[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s"
21
+ ))
22
+ logger.addHandler(handler)
23
+
24
+
25
+ # Initialize Semantic Kernel
26
+ kernel = Kernel()
27
+
28
+ # Add Azure OpenAI Chat Service
29
+ kernel.add_service(AzureChatCompletion(
30
+ service_id="chat",
31
+ deployment_name=os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"),
32
+ endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
33
+ api_key=os.getenv("AZURE_OPENAI_KEY")
34
+ ))
35
+
36
+ # Database Plugin
37
+ class CosmosDBPlugin:
38
+ def __init__(self):
39
+ self.client = CosmosClient(
40
+ os.getenv("AZURE_COSMOS_DB_ENDPOINT"),
41
+ os.getenv("AZURE_COSMOS_DB_KEY")
42
+ )
43
+ self.database = self.client.get_database_client("TAL_DB")
44
+ self.container = self.database.get_container_client("Converters")
45
+ self.logger = logger
46
+
47
+ @kernel_function(
48
+ name="query_converters",
49
+ description="Execute SQL query against Cosmos DB converters collection"
50
+ )
51
+ async def query_converters(self, query: str) -> str:
52
+ try:
53
+ print(f"Executing query: {query}")
54
+ items = list(self.container.query_items(
55
+ query=query,
56
+ enable_cross_partition_query=True
57
+ ))
58
+ print(f"Query returned {len(items)} items")
59
+ items = items[:10]
60
+ self.logger.debug(f"Raw items: {items}")
61
+
62
+ items = [PowerConverter(**item) for item in items] if items else []
63
+
64
+ self.logger.info(f"Query returned {len(items)} items after conversion")
65
+ self.logger.debug(f"Items: {items}")
66
+
67
+
68
+
69
+ return str(items)
70
+ except Exception as e:
71
+ self.logger.info(f"Query failed: {str(e)}")
72
+ return f"Query failed: {str(e)}"
73
+
74
+ # SQL Generation Plugin
75
+ class NL2SQLPlugin:
76
+ @kernel_function(name="generate_sql", description="Generate Cosmos DB SQL query")
77
+ async def generate_sql(self, question: str) -> str:
78
+ sql = await self._generate_sql_helper(question)
79
+ if "SELECT *" not in sql and "FROM c" in sql:
80
+ sql = sql.replace("SELECT c.*", "SELECT *")
81
+ sql = sql.replace("SELECT c", "SELECT *")
82
+ return sql
83
+
84
+ async def _generate_sql_helper(self, question: str) -> str:
85
+ from semantic_kernel.contents import ChatHistory
86
+
87
+ chat_service = kernel.get_service("chat")
88
+ chat_history = ChatHistory()
89
+ chat_history.add_user_message(f"""Convert to Cosmos DB SQL: {question}
90
+ Collection: converters (alias 'c')
91
+ Fields:
92
+ - type (e.g., '350mA')
93
+ - artnr (numeric (int) article number e.g., 930546)
94
+ - output_voltage_v: dictionary with min/max values for output voltage
95
+ - output_voltage_v.min (e.g., 15)
96
+ - output_voltage_v.max (e.g., 40)
97
+ - input_voltage_v: dictionary with min/max values for input voltage
98
+ - input_voltage_v.min (e.g., 198)
99
+ - input_voltage_v.max (e.g., 264)
100
+ - lamps: dictionary with min/max values for lamp types for this converter
101
+ - lamps["lamp_name"].min (e.g., 1)
102
+ - lamps["lamp_name"].max (e.g., 10)
103
+ - nom_input_voltage (e.g, '198-264V')
104
+ - class (safety class)
105
+ - dimmability (e.g., 'MAINS DIM LC')
106
+ - listprice (e.g., 58)
107
+ - lifecycle (e.g., 'Active')
108
+ - size (e.g., '150x30x30')
109
+ - dimlist_type (e.g., 'DALI')
110
+ - pdf_link (link to product PDF)
111
+ - converter_description (e.g., 'POWERLED CONVERTER REMOTE 180mA 8W IP20 1-10V')
112
+ - ip (Ingress Protection, integer values e.g., 20,67)
113
+ - efficiency_full_load (e.g., 0.9)
114
+ - name (e.g., 'Power Converter 350mA')
115
+ - unit (e.g., 'PC')
116
+ Return ONLY SQL without explanations""")
117
+
118
+ response = await chat_service.get_chat_message_content(
119
+ chat_history=chat_history,
120
+ settings=AzureChatPromptExecutionSettings()
121
+ )
122
+
123
+ return str(response)
124
+
125
+
126
+ # Register plugins
127
+ kernel.add_plugin(CosmosDBPlugin(), "CosmosDBPlugin")
128
+ kernel.add_plugin(NL2SQLPlugin(), "NL2SQLPlugin")
129
+
130
+ # Updated query handler using function calling
131
+ async def handle_query(user_input: str):
132
+
133
+ settings = AzureChatPromptExecutionSettings(
134
+ function_choice_behavior=FunctionChoiceBehavior.Auto(auto_invoke=True)
135
+ )
136
+
137
+ prompt = f"""
138
+ You are a converter database expert. Process this user query:
139
+ {user_input}
140
+
141
+ Available functions:
142
+ - generate_sql: Creates SQL queries from natural language
143
+ - query_converters: Executes SQL queries against the database
144
+
145
+ Follow these steps:
146
+ 1. Generate SQL using generate_sql
147
+ 2. Execute query with query_converters
148
+ 3. Format results into natural language response
149
+
150
+ Query Guidelines:
151
+ 1. When performing SELECT ALL queries always use SELECT *. Never use SELECT c.* or SELECT c
152
+ 2. For questions about lamp compatibility, ALWAYS use SELECT * FROM c WHERE IS_DEFINED(c.lamps["lamp_name"])
153
+ 3. For questions about lamps that can be used with a converter, ALWAYS use SELECT c.lamps FROM c WHERE c.artnr = @artnr
154
+ 5. For questions about lamp limits, query for the lamps dictionary and return min/max values
155
+
156
+ """
157
+
158
+ result = await kernel.invoke_prompt(
159
+ prompt=prompt,
160
+ settings=settings
161
+ )
162
+
163
+ return str(result)
164
+
165
+ # Example usage
166
+ async def main():
167
+
168
+ while True:
169
+ try:
170
+ query = input("User: ")
171
+ if query.lower() in ["exit", "quit"]:
172
+ break
173
+
174
+ response = await handle_query(query)
175
+ print(response)
176
+
177
+ except KeyboardInterrupt:
178
+ break
179
+
180
+ if __name__ == "__main__":
181
+ import asyncio
182
+ asyncio.run(main())