StefanoDUrso commited on
Commit
11dc279
·
2 Parent(s): 48bc96d9187a60

Merge branch 'main' of https://github.com/paisleypark3121/ibn

Browse files
.gitignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ .*
3
+ .cer
4
+ data
5
+ __pycache__
6
+
7
+ # Except .gitignore itself
8
+ !.gitignore
9
+
10
+ # Except .gitkeep files
11
+ !.gitkeep
app.py ADDED
@@ -0,0 +1,530 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ import os
3
+ import json
4
+ import inspect
5
+
6
+ from langchain_openai import ChatOpenAI
7
+
8
+ #from langchain.schema import AIMessage, HumanMessage, SystemMessage
9
+ from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, ToolMessage
10
+
11
+ from utilities.qdrant.QdrantLangchainManager import *
12
+ from langchain_core.tools import tool
13
+ import gradio as gr
14
+
15
+ load_dotenv()
16
+
17
+ credentials = {}
18
+ credentials[os.getenv("USERNAME")] = os.getenv("PASSWORD")
19
+ i = 1
20
+ while os.getenv(f"USERNAME_{i}") and os.getenv(f"PASSWORD_{i}"):
21
+ credentials[os.getenv(f"USERNAME_{i}")] = os.getenv(f"PASSWORD_{i}")
22
+ i += 1
23
+
24
+ #print(credentials)
25
+
26
+ system_message = """
27
+ You are a specialized assistant that helps system managers configure 5G networks.
28
+
29
+ 🔹 **Your Main Role**:
30
+ - Assisting in **5G SA modem configuration**.
31
+ - Setting up modems for **Mixed 5G/4G Networks**.
32
+ - Deploying **industrial 5G SA networks** with custom requirements.
33
+ - Providing expert knowledge on **5G technology, network components, frequency bands, and optimization**.
34
+
35
+ 📌 **Context Handling**:
36
+ - If relevant [context] is available, use it to provide **precise and accurate answers**.
37
+ - If [context] is **not provided or does not apply**, rely on your **general knowledge** of 5G.
38
+ - Always **distinguish between official reference information and general knowledge**, prioritizing accuracy.
39
+
40
+ 🔹 **How to Respond**:
41
+ - **Prioritize clarity**: Keep answers direct and solution-oriented.
42
+ - **Guide the user step by step**, ensuring only essential parameters are asked first.
43
+ - **Acknowledge the source**: If using [context], clarify that the information comes from official reference material.
44
+
45
+ [context]
46
+ {context}
47
+ """
48
+
49
+ MAX_HISTORY = 20
50
+ messages = [SystemMessage(content=system_message)]
51
+
52
+ def _truncate_messages(messages, MAX_HISTORY=20):
53
+ """Keeps the first system message and only retains the last MAX_HISTORY user/AI messages."""
54
+
55
+ # ✅ Ensure system_message is always the first message
56
+ system_msg = messages[0]
57
+ truncated_messages = messages[1:][-MAX_HISTORY:] # Keep only the last MAX_HISTORY messages (excluding system message)
58
+
59
+ # ✅ Rebuild messages list with system_message at the beginning
60
+ messages = [system_msg] + truncated_messages
61
+
62
+ return messages
63
+
64
+
65
+ tool_system_message = """You are an assistant that generates a Bash script based on the given [parameters] and [context].
66
+ If sufficient context is not available, return an ERROR message.
67
+
68
+ Below is the [function_description] that defines the tool you are using. Use it to correctly interpret the parameters.
69
+
70
+ [function_description]
71
+ {function_description}
72
+
73
+ [parameters]
74
+ {parameters}
75
+
76
+ [context]
77
+ {context}
78
+ """
79
+
80
+ collection_name = "5g reference"
81
+
82
+ manager = QdrantLangchainManager(
83
+ qdrant_url=os.getenv("QDRANT_URL"),
84
+ qdrant_api_key=os.getenv("QDRANT_API_KEY"),
85
+ llm=ChatOpenAI(model="gpt-4o-mini", streaming=True),
86
+ crossencoder="cross-encoder/ms-marco-MiniLM-L-6-v2"
87
+ )
88
+
89
+ def _get_tool_query(tool_name: str) -> str:
90
+ """Generates a query to prompt the LLM for a tool-based response."""
91
+
92
+ if tool_name == "configure_5g_sa_modem":
93
+ return "I need a Bash script to configure a modem to connect to a Standalone (SA) 5G network with a specific APN."
94
+
95
+ elif tool_name == "configure_mixed_5g_4g_modem":
96
+ return "I need a Bash script to configure a modem for a mixed 5G/4G network."
97
+
98
+ elif tool_name == "configure_industrial_5g_sa_modem":
99
+ return "I need a Bash script to configure a modem for an industrial 5G SA network."
100
+
101
+ return None
102
+
103
+ def _tool_helper(tool_name: str, tool_args: dict) -> str:
104
+ """Helper function to generate a Bash script using the LLM based on tool parameters and retrieved context."""
105
+
106
+ manager.get_collection(collection_name)
107
+ query = _get_tool_query(tool_name)
108
+ if not query:
109
+ return "ERROR: Invalid tool name."
110
+
111
+ context = manager.search_qdrant(query)
112
+ if not context:
113
+ return "ERROR: No relevant reference found in the vector store."
114
+
115
+ # Get the function object from globals() based on the tool name
116
+ function_obj = globals().get(tool_name)
117
+ # Extract the function description directly
118
+ function_description = getattr(function_obj, "description", "No documentation available.")
119
+
120
+ # Format parameters into a readable string
121
+ parameters_str = "\n".join([f"- {key}: {value}" for key, value in tool_args.items()])
122
+
123
+ # **Include function documentation in the system message**
124
+ system_message = tool_system_message.format(
125
+ parameters=parameters_str,
126
+ context=context,
127
+ function_description=function_description
128
+ )
129
+
130
+ # print("*****")
131
+ # print(system_message)
132
+ # print("*****")
133
+
134
+ messages = [
135
+ SystemMessage(content=system_message),
136
+ #HumanMessage(content=query)
137
+ ]
138
+
139
+ response = manager.llm.invoke(messages)
140
+ return response.content
141
+
142
+ @tool
143
+ def configure_5g_sa_modem(
144
+ modem_device: str,
145
+ apn: str,
146
+ pdp_type: str,
147
+ bearer: str,
148
+ bands: list,
149
+ ip_config: str,
150
+ dns: str,
151
+ enable_roaming: bool
152
+ ) -> str:
153
+ """Configures a modem to connect to a 5G SA (Standalone) network.
154
+
155
+ Parameters:
156
+ - modem_device (str): Serial port of the modem (e.g., '/dev/ttyUSB0').
157
+ - apn (str): APN name to configure.
158
+ - pdp_type (str): Data connection type (IP, IPV6, IPV4V6).
159
+ - bearer (str): Physical connection type (NR5G, LTE, AUTO).
160
+ - bands (list): List of 5G bands to enable (e.g., ['n1', 'n3', 'n78']).
161
+ - ip_config (str): IP configuration (Static, Dynamic).
162
+ - dns (str): DNS address to use.
163
+ - enable_roaming (bool): Enables or disables roaming.
164
+
165
+ Returns:
166
+ - str: Confirmation message with the applied configuration details.
167
+ """
168
+
169
+ function_name = inspect.currentframe().f_code.co_name
170
+ return _tool_helper(tool_name=function_name, tool_args=locals())
171
+
172
+ @tool
173
+ def configure_mixed_5g_4g_modem(
174
+ modem_device: str,
175
+ lte_bands: list,
176
+ nr5g_bands: list,
177
+ network_priority: str,
178
+ connection_timeout: int
179
+ ) -> str:
180
+ """Configures a modem for a mixed 5G/4G network.
181
+
182
+ Parameters:
183
+ - modem_device (str): Serial port of the modem (e.g., '/dev/ttyUSB0').
184
+ - lte_bands (list): List of LTE bands to activate (e.g., ['1', '3', '7']).
185
+ - nr5g_bands (list): List of 5G NR bands to activate (e.g., ['n78', 'n79']).
186
+ - network_priority (str): Preference between 5G, 4G, or automatic mode ('5G', '4G', 'AUTO').
187
+ - connection_timeout (int): Maximum connection timeout in seconds.
188
+
189
+ Returns:
190
+ - str: Confirmation message with the applied configuration details.
191
+ """
192
+
193
+ function_name = inspect.currentframe().f_code.co_name
194
+ return _tool_helper(tool_name=function_name, tool_args=locals())
195
+
196
+ @tool
197
+ def configure_industrial_5g_sa_modem(
198
+ modem_device: str,
199
+ industrial_apn: str,
200
+ lte_bands: list,
201
+ nr5g_bands: list,
202
+ disable_wcdma: bool,
203
+ verify_registration: bool
204
+ ) -> str:
205
+ """Configures a modem to connect to an industrial 5G SA network.
206
+
207
+ Parameters:
208
+ - modem_device (str): Serial port of the modem (e.g., '/dev/ttyUSB2').
209
+ - industrial_apn (str): APN name specific to the industrial network.
210
+ - lte_bands (list): List of LTE bands to activate.
211
+ - nr5g_bands (list): List of 5G NR bands to activate.
212
+ - disable_wcdma (bool): If True, disables WCDMA bands.
213
+ - verify_registration (bool): If True, verifies registration status.
214
+
215
+ Returns:
216
+ - str: Confirmation message with the applied configuration details.
217
+ """
218
+
219
+ function_name = inspect.currentframe().f_code.co_name
220
+ return _tool_helper(tool_name=function_name, tool_args=locals())
221
+
222
+ tools = [
223
+ configure_5g_sa_modem,
224
+ configure_mixed_5g_4g_modem,
225
+ configure_industrial_5g_sa_modem
226
+ ]
227
+
228
+ llm = ChatOpenAI(
229
+ model="gpt-4o-mini",
230
+ streaming=True,
231
+ )
232
+ llm_with_tools = llm.bind_tools(tools)
233
+
234
+ tool_name = ""
235
+ tool_args = ""
236
+
237
+ tool_mapping = {
238
+ "configure_5g_sa_modem": configure_5g_sa_modem,
239
+ "configure_mixed_5g_4g_modem": configure_mixed_5g_4g_modem,
240
+ "configure_industrial_5g_sa_modem": configure_industrial_5g_sa_modem
241
+ }
242
+
243
+ label_buttons = ["Confirm", "Cancel"]
244
+
245
+ def _check_response(ai_msg):
246
+
247
+ # if "persona fisica" in ai_msg.lower() and "società" in ai_msg.lower():
248
+
249
+ # context_analysis_prompt = f"""
250
+ # Questa è la risposta dell'LLM a un utente che vuole creare un cliente:
251
+
252
+ # \"{ai_msg}\"
253
+
254
+ # Devi solo rispondere con "create_client" se la risposta chiede all'utente di selezionare tra "Persona Fisica" e "Società",
255
+ # oppure "SPIEGAZIONE" se si tratta solo di una descrizione generale dei tipi di clienti senza richiedere un'azione.
256
+ # """
257
+ # analysis_response = llm.invoke(context_analysis_prompt).content.strip()
258
+ # print(analysis_response)
259
+
260
+ # if analysis_response == "create_client":
261
+ # return True, analysis_response
262
+
263
+ # elif "partita iva" in ai_msg.lower() and "codice fiscale" in ai_msg.lower():
264
+
265
+ # context_analysis_prompt = f"""
266
+ # Questa è la risposta dell'LLM a un utente che deve scegliere la modalità con cui vuole inserire i dati societari
267
+
268
+ # \"{ai_msg}\"
269
+
270
+ # Devi solo rispondere con "select_mode" se la risposta chiede all'utente di selezionare tra "Partita IVA" e "Codice Fiscale",
271
+ # oppure "SPIEGAZIONE" se si tratta solo di una descrizione generale senza richiedere un'azione.
272
+ # """
273
+ # analysis_response = llm.invoke(context_analysis_prompt).content.strip()
274
+ # print(analysis_response)
275
+
276
+ # if analysis_response == "select_mode":
277
+ # return True, analysis_response
278
+
279
+ return False, ""
280
+
281
+ def _get_confirmation_message(tool_name: str, tool_args: dict) -> str:
282
+ """Generate a descriptive message to confirm the action that is about to be performed."""
283
+
284
+ if tool_name == "configure_5g_sa_modem":
285
+ return f"**ACTION** You are about to configure 5G SA modem." # {tool_args.get('first_name', 'N/D')} {tool_args.get('last_name', 'N/D')}."
286
+
287
+ elif tool_name == "configure_mixed_5g_4g_modem":
288
+ return f"**ACTION** You are about to configure a mixed 5G/4G modem."
289
+
290
+ elif tool_name == "configure_industrial_5g_sa_modem":
291
+ return f"**ACTION** You are about to configure an industrial 5G SA modem."
292
+
293
+ return "**ACTION** Unknown action."
294
+
295
+ def _format_tool_args(tool_name: str, tool_args: dict) -> str:
296
+ """Transforms technical parameters into a user-friendly description, handling optional values."""
297
+
298
+ translations = {
299
+ "modem_device": "Modem Device",
300
+ "apn": "APN",
301
+ "pdp_type": "PDP Type",
302
+ "bearer": "Bearer",
303
+ "bands": "5G Bands",
304
+ "ip_config": "IP Configuration",
305
+ "dns": "DNS",
306
+ "enable_roaming": "Roaming Enabled",
307
+ "lte_bands": "LTE Bands",
308
+ "nr5g_bands": "5G NR Bands",
309
+ "network_priority": "Network Priority",
310
+ "connection_timeout": "Connection Timeout",
311
+ "industrial_apn": "Industrial APN",
312
+ "disable_wcdma": "Disable WCDMA",
313
+ "verify_registration": "Verify Registration",
314
+ }
315
+
316
+ _formatted_args = "\n".join([
317
+ f"- {translations.get(key, key)}: {', '.join(value) if isinstance(value, list) else value if value else 'Not specified'}"
318
+ for key, value in tool_args.items()
319
+ ])
320
+
321
+ return _formatted_args
322
+
323
+ def chatbot_response(message, history):
324
+
325
+ global messages, tool_name, tool_args
326
+
327
+ history = history or []
328
+
329
+ if message.startswith("**OPERATION CANCELLED**"):
330
+ history.append({"role": "user", "content": "**OPERATION CANCELLED**"})
331
+ else:
332
+ history.append({"role": "user", "content": message})
333
+
334
+ #messages = [SystemMessage(content=system_message)]
335
+ #truncated_history = history[-MAX_HISTORY:] if len(history) > MAX_HISTORY else history
336
+
337
+ #print(f"Truncated history: {truncated_history}")
338
+
339
+ # for entry in truncated_history:
340
+ # if entry["role"] == "user":
341
+ # messages.append(HumanMessage(content=entry["content"]))
342
+ # elif entry["role"] == "assistant":
343
+ # messages.append(AIMessage(content=entry["content"]))
344
+
345
+ if message is not None:
346
+ messages.append(HumanMessage(content=message))
347
+ messages=_truncate_messages(messages,MAX_HISTORY)
348
+
349
+ #_response=llm_with_tools.stream(history_langchain_format)
350
+ ai_msg=llm_with_tools.invoke(messages)
351
+ #print(ai_msg)
352
+
353
+ choice, analysis_response = _check_response(ai_msg.content)
354
+
355
+ # if choice and analysis_response == "create_client":
356
+ # history.append({"role": "assistant", "content": f"**CREAZIONE CLIENTE**\n{ai_msg.content}"})
357
+
358
+ # elif choice and analysis_response == "select_mode":
359
+ # history.append({"role": "assistant", "content": f"**MODALITA' INSERIMENTO DATI SOCIETARI**\n{ai_msg.content}"})
360
+
361
+ # elif hasattr(ai_msg, "tool_calls") and ai_msg.tool_calls:
362
+ if hasattr(ai_msg, "tool_calls") and ai_msg.tool_calls:
363
+
364
+ for tool_call in ai_msg.tool_calls:
365
+ tool_name = tool_call["name"].lower()
366
+ tool_args = tool_call["args"]
367
+ selected_tool = tool_mapping.get(tool_name)
368
+
369
+ if selected_tool:
370
+ # if selected_tool==create_company_vat_number:
371
+ # tool_args=_get_company_info(tool_args.get('vat_number', 'N/D'))
372
+ # if tool_args is None:
373
+ # history.append({"role": "user", "content": "Errore: Dati societari non trovati."})
374
+ # return history
375
+
376
+ confirmation_message = _get_confirmation_message(tool_name, tool_args)
377
+
378
+ formatted_args = _format_tool_args(tool_name, tool_args)
379
+ ai_msg= f"{confirmation_message}\n\n**Here are the details entered:**\n{formatted_args}"
380
+ history.append({"role": "assistant", "content": ai_msg})
381
+ messages.append(AIMessage(content=ai_msg))
382
+
383
+ print("---")
384
+ print("Tool name: ", tool_name)
385
+ print("Tool args: ", tool_args)
386
+ print("---")
387
+
388
+ else:
389
+ history.append({"role": "assistant", "content": ai_msg.content})
390
+ messages.append(AIMessage(content=ai_msg.content))
391
+
392
+ return history
393
+
394
+ def reset_textbox():
395
+ """Clears the textbox after sending a message."""
396
+ return gr.update(value="")
397
+
398
+ def hide_buttons():
399
+ """Hides buttons and shows the textbox after clicking a button."""
400
+ return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
401
+
402
+ def show_or_hide_buttons(history):
403
+ global label_buttons
404
+
405
+ if history and history[-1]["content"].startswith("**ACTION**"):
406
+ label_buttons = ["Confirm", "Cancel"]
407
+ return (
408
+ gr.update(visible=False),
409
+ gr.update(visible=True, value=label_buttons[0]),
410
+ gr.update(visible=True, value=label_buttons[1]),
411
+ gr.update(visible=False)
412
+ )
413
+ return (
414
+ gr.update(visible=True),
415
+ gr.update(visible=False),
416
+ gr.update(visible=False),
417
+ gr.update(visible=True)
418
+ )
419
+
420
+ def button_clicked(option, history):
421
+ """Handles button clicks and updates chat history."""
422
+
423
+ global messages, tool_name, tool_args
424
+
425
+ print(f"Button clicked: {option}")
426
+
427
+ if (option == "Confirm"):
428
+ history.append({"role": "user", "content": option})
429
+ print("***")
430
+ print(f"Tool name: {tool_name}")
431
+ print(f"Tool args: {tool_args}")
432
+ print("***")
433
+ if tool_name and tool_args:
434
+ selected_tool = tool_mapping.get(tool_name)
435
+ #print(f"Selected tool: {selected_tool}")
436
+ if selected_tool:
437
+ tool_output = selected_tool.invoke(tool_args)
438
+ history.append({"role": "assistant", "content": tool_output})
439
+ else:
440
+ history.append({"role": "user", "content": "Operazione annullata"})
441
+ elif option == "Cancel":
442
+ history.append({"role": "user", "content": option})
443
+ llm_message = (
444
+ "**OPERATION CANCELLED**\n"
445
+ )
446
+ history = chatbot_response(llm_message, history)
447
+
448
+ messages = [messages[0]]
449
+ tool_name = ""
450
+ tool_args = ""
451
+
452
+ return history
453
+
454
+ def authenticate(username, password):
455
+ if username in credentials and credentials[username] == password:
456
+ print("🔑 Login successful!")
457
+ return gr.update(visible=False), gr.update(visible=True), gr.update(value="", visible=False) # Hide login, show chatbot, clear error
458
+ else:
459
+ print("❌ Incorrect username or password")
460
+ return gr.update(visible=True), gr.update(visible=False), gr.update(value="❌ Incorrect username or password", visible=True) # Show error
461
+
462
+ def disable_inputs():
463
+ return gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False)
464
+
465
+ def enable_inputs():
466
+ return gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True)
467
+
468
+ with gr.Blocks() as demo:
469
+
470
+ # 🔒 Login Section (Initially Visible)
471
+ with gr.Column(visible=True) as login_section:
472
+ gr.Markdown("### 🔒 Login Required")
473
+ username_input = gr.Textbox(label="Username")
474
+ password_input = gr.Textbox(label="Password", type="password")
475
+ login_button = gr.Button("Login")
476
+ error_message = gr.Text("", visible=False)
477
+
478
+ # 🧠 Chatbot Section (Initially Hidden)
479
+ with gr.Column(visible=False) as chatbot_section:
480
+
481
+ chatbot = gr.Chatbot(
482
+ label="System Manager Chatbot",
483
+ type="messages"
484
+ )
485
+
486
+ user_input = gr.Textbox(label="User",placeholder="What would you like to ask your assistant?")
487
+
488
+ with gr.Row():
489
+ btn1 = gr.Button("", visible=False)
490
+ btn2 = gr.Button("", visible=False)
491
+
492
+ send_btn = gr.Button("Send")
493
+
494
+ # When user submits text
495
+ user_input.submit(disable_inputs, None, [user_input, send_btn, btn1, btn2]) \
496
+ .then(chatbot_response, [user_input, chatbot], chatbot) \
497
+ .then(reset_textbox, None, user_input) \
498
+ .then(show_or_hide_buttons, chatbot, [user_input, btn1, btn2, send_btn]) \
499
+ .then(enable_inputs, None, [user_input, send_btn, btn1, btn2])
500
+
501
+ # When user clicks send
502
+ send_btn.click(disable_inputs, None, [user_input, send_btn, btn1, btn2]) \
503
+ .then(chatbot_response, [user_input, chatbot], chatbot) \
504
+ .then(reset_textbox, None, user_input) \
505
+ .then(show_or_hide_buttons, chatbot, [user_input, btn1, btn2, send_btn]) \
506
+ .then(enable_inputs, None, [user_input, send_btn, btn1, btn2])
507
+
508
+ # Button clicks: Show textbox, hide buttons
509
+ btn1.click(disable_inputs, None, [user_input, send_btn, btn1, btn2]) \
510
+ .then(lambda h: button_clicked(label_buttons[0], h), chatbot, chatbot) \
511
+ .then(show_or_hide_buttons, chatbot, [user_input, btn1, btn2, send_btn]) \
512
+ .then(enable_inputs, None, [user_input, send_btn, btn1, btn2])
513
+
514
+ btn2.click(disable_inputs, None, [user_input, send_btn, btn1, btn2]) \
515
+ .then(lambda h: button_clicked(label_buttons[1], h), chatbot, chatbot) \
516
+ .then(show_or_hide_buttons, chatbot, [user_input, btn1, btn2, send_btn]) \
517
+ .then(enable_inputs, None, [user_input, send_btn, btn1, btn2])
518
+
519
+ # 🔑 Login Button Action
520
+ login_button.click(
521
+ authenticate,
522
+ [username_input, password_input],
523
+ [login_section, chatbot_section, error_message]
524
+ )
525
+
526
+
527
+ demo.launch(
528
+ debug=True,
529
+ #share=True
530
+ )
old_app.py ADDED
@@ -0,0 +1,609 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ import os
3
+ import json
4
+
5
+ from langchain_openai import ChatOpenAI
6
+
7
+ #from langchain.schema import AIMessage, HumanMessage, SystemMessage
8
+ from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, ToolMessage
9
+
10
+ from langchain_core.tools import tool
11
+ import gradio as gr
12
+
13
+ load_dotenv()
14
+
15
+ credentials = {}
16
+ credentials[os.getenv("USERNAME")] = os.getenv("PASSWORD")
17
+ i = 1
18
+ while os.getenv(f"USERNAME_{i}") and os.getenv(f"PASSWORD_{i}"):
19
+ credentials[os.getenv(f"USERNAME_{i}")] = os.getenv(f"PASSWORD_{i}")
20
+ i += 1
21
+
22
+ #print(credentials)
23
+
24
+ MAX_HISTORY = 20
25
+
26
+ system_message = """
27
+ Sei l'assistente virtuale di Forfè, un software di fatturazione e gestione fiscale
28
+ specifico per il Regime Forfettario, la Gestione Separata INPS e la Cassa Artigiani e Commercianti.
29
+ Il tuo compito è aiutare gli utenti nella gestione della loro attività, fornendo supporto nella creazione
30
+ di fatture, gestione clienti e prodotti, e altre operazioni contabili.
31
+
32
+ 🔹 **Le tue funzionalità principali includono:**
33
+ - Creazione di nuovi clienti, sia persone fisiche che società.
34
+ - Creazione di nuovi prodotti o servizi da fatturare.
35
+ - Generazione di fatture con i dati dei clienti e dei prodotti registrati.
36
+
37
+ 📌 **Regole di interazione:**
38
+ - Chiedi all'utente solo i dati strettamente necessari per l'operazione richiesta.
39
+ - Mantieni sempre un linguaggio chiaro, professionale e amichevole.
40
+ - Se il numero di informazioni da richiedere all'utente sono troppe, dividi l'operazione in più passaggi, chiedendo dapprima i parametri obbligatori e poi quelli opzionali.
41
+ - Rispondi in italiano e guida l'utente passo dopo passo nel processo.
42
+
43
+ 🚀 **Obiettivo:** Aiutare i professionisti e le piccole imprese a gestire la loro attività in modo semplice ed efficace con Forfè.
44
+ """
45
+
46
+ @tool
47
+ def create_product(
48
+ name: str,
49
+ price: float
50
+ ) -> str:
51
+ """Crea un nuovo prodotto o servizio e restituisce una conferma.
52
+
53
+ Parametri:
54
+ - name (str): Nome del prodotto o del servizio offerto.
55
+ - price (float): Importo in euro (€) del prodotto o servizio.
56
+
57
+ Ritorna:
58
+ - str: Un messaggio di conferma con i dettagli del prodotto o servizio creato.
59
+ """
60
+
61
+ print("\n--- Nuovo Prodotto/Servizio Creato ---")
62
+ print(f"Nome: {name}")
63
+ print(f"Prezzo: {price:.2f} EUR")
64
+
65
+ return f"Il nuovo prodotto o servizio '{name}' con un importo di {price:.2f}€ è stato generato con successo!"
66
+
67
+ @tool
68
+ def create_customer_type(customer_type: str) -> str:
69
+ """Seleziona il tipo di cliente da creare.
70
+
71
+ Parametri:
72
+ - customer_type (str): Il tipo di cliente da creare. Deve essere 'individual' (persona fisica) o 'company' (società).
73
+
74
+ Ritorna:
75
+ - str: Un messaggio di conferma con il tipo di cliente selezionato.
76
+ """
77
+
78
+ if customer_type not in ["individual", "company"]:
79
+ return "Errore: Il tipo di cliente deve essere 'individual' (persona fisica) o 'company' (società)."
80
+
81
+ return f"Hai selezionato {customer_type}. Ora puoi procedere con l'inserimento dei dati."
82
+
83
+ @tool
84
+ def create_individual(
85
+ first_name: str,
86
+ last_name: str,
87
+ tax_code: str,
88
+ address: str,
89
+ street_number: str,
90
+ postal_code: str,
91
+ city: str,
92
+ province: str,
93
+ country: str,
94
+ vat_number: str = None, # Opzionale
95
+ email: str = None,
96
+ pec: str = None,
97
+ phone: str = None,
98
+ recipient_code: str = None
99
+ ) -> str:
100
+ """Crea un cliente di tipo persona fisica.
101
+
102
+ Parametri:
103
+ - first_name (str): Nome del cliente.
104
+ - last_name (str): Cognome del cliente.
105
+ - tax_code (str): Codice fiscale.
106
+ - address (str): Indirizzo di residenza.
107
+ - street_number (str): Numero civico.
108
+ - postal_code (str): CAP.
109
+ - city (str): Città.
110
+ - province (str): Provincia.
111
+ - country (str): Nazione.
112
+ - vat_number (str, opzionale): Partita IVA, se presente.
113
+ - email (str, opzionale): Indirizzo email.
114
+ - pec (str, opzionale): Indirizzo PEC.
115
+ - phone (str, opzionale): Numero di telefono.
116
+ - recipient_code (str, opzionale): Codice destinatario.
117
+
118
+ Ritorna:
119
+ - str: Un messaggio di conferma con i dati della persona fisica creata.
120
+ """
121
+
122
+ print("\n--- Nuova Persona Fisica Creata ---")
123
+ print(f"Nome: {first_name} {last_name}")
124
+ print(f"Codice Fiscale: {tax_code}")
125
+ print(f"Indirizzo: {address}, {street_number}, {postal_code}, {city}, {province}, {country}")
126
+
127
+ if vat_number:
128
+ print(f"Partita IVA: {vat_number}")
129
+ if email:
130
+ print(f"Email: {email}")
131
+ if pec:
132
+ print(f"PEC: {pec}")
133
+ if phone:
134
+ print(f"Telefono: {phone}")
135
+ if recipient_code:
136
+ print(f"Codice Destinatario: {recipient_code}")
137
+
138
+ return f"La persona fisica {first_name} {last_name} è stata creata con successo!"
139
+
140
+ @tool
141
+ def create_company_mode(company_type: str) -> str:
142
+ """Permette di scegliere se inserire i dati societari tramite Partiva IVA o Codice Fiscale.
143
+
144
+ Parametri:
145
+ - company_type (str): Modalità di inserimento delle informazioni societarie. Deve essere 'vat_number' (Partiva IVA) o 'tax_code' (Codice Fiscale).
146
+
147
+ Ritorna:
148
+ - str: Un messaggio di conferma con il tipo di selezionato.
149
+ """
150
+
151
+ if customer_type not in ["vat_number", "tax_code"]:
152
+ return "Errore: Il tipo di modalità deve essere 'vat_number' (Partiva IVA) o 'tax_code' (Codice Fiscale)."
153
+
154
+ return f"Hai selezionato {company_type}. Ora puoi procedere con l'inserimento dei dati."
155
+
156
+ @tool
157
+ def create_company_tax_code(
158
+ company_name: str,
159
+ tax_code: str,
160
+ address: str,
161
+ street_number: str,
162
+ postal_code: str,
163
+ city: str,
164
+ province: str,
165
+ country: str,
166
+ vat_number: str = None,
167
+ email: str = None,
168
+ pec: str = None,
169
+ phone: str = None,
170
+ recipient_code: str = None
171
+ ) -> str:
172
+ """Crea un cliente di tipo società tramite Codice Fiscale.
173
+
174
+ Parametri:
175
+ - company_name (str): Nome della società (ragione sociale).
176
+ - tax_code (str): Codice fiscale della società.
177
+ - address (str): Indirizzo della sede legale.
178
+ - street_number (str): Numero civico.
179
+ - postal_code (str): CAP.
180
+ - city (str): Città.
181
+ - province (str): Provincia.
182
+ - country (str): Nazione.
183
+ - vat_number (str, opzionale): Partita IVA.
184
+ - email (str, opzionale): Indirizzo email.
185
+ - pec (str, opzionale): Indirizzo PEC.
186
+ - phone (str, opzionale): Numero di telefono.
187
+ - recipient_code (str, opzionale): Codice destinatario.
188
+
189
+ Ritorna:
190
+ - str: Un messaggio di conferma con i dati della società creata.
191
+ """
192
+
193
+ print("\n--- Nuova Società Creata ---")
194
+ print(f"Ragione Sociale: {company_name}")
195
+ print(f"Codice Fiscale: {tax_code}")
196
+ print(f"Indirizzo: {address}, {street_number}, {postal_code}, {city}, {province}, {country}")
197
+
198
+ if vat_number:
199
+ print(f"Partita IVA: {vat_number}")
200
+ if email:
201
+ print(f"Email: {email}")
202
+ if pec:
203
+ print(f"PEC: {pec}")
204
+ if phone:
205
+ print(f"Telefono: {phone}")
206
+ if recipient_code:
207
+ print(f"Codice Destinatario: {recipient_code}")
208
+
209
+ return f"La società {company_name} è stata creata con successo!"
210
+
211
+ @tool
212
+ def create_company_vat_number(
213
+ vat_number: str = None,
214
+ ) -> str:
215
+ """Crea un cliente di tipo società tramite Partita IVA, non vanno richiesti altri dati, solo confermare la Partita IVA.
216
+
217
+ Parametri:
218
+ - vat_number (str): Partita IVA.
219
+
220
+ Ritorna:
221
+ - str: Un messaggio di conferma con i dati della Partita IVA della società creata.
222
+ """
223
+
224
+ print("\n--- Nuova Società Creata ---")
225
+ print(f"Partita IVA: {vat_number}")
226
+
227
+ return f"La società con Partita IVA {vat_number} è stata creata con successo!"
228
+
229
+
230
+ tools = [
231
+ create_product,
232
+ create_customer_type,
233
+ create_individual,
234
+ create_company_mode,
235
+ create_company_tax_code,
236
+ create_company_vat_number
237
+ ]
238
+
239
+ llm = ChatOpenAI(
240
+ model="gpt-4o-mini",
241
+ streaming=True,
242
+ )
243
+ llm_with_tools = llm.bind_tools(tools)
244
+
245
+ tool_name = ""
246
+ tool_args = ""
247
+
248
+ tool_mapping = {
249
+ #"create_customer_type": create_customer_type,
250
+ "create_individual": create_individual,
251
+ "create_company_tax_code": create_company_tax_code,
252
+ "create_company_vat_number": create_company_vat_number,
253
+ "create_product": create_product
254
+ }
255
+
256
+ label_buttons = ["Procedi", "Annulla"]
257
+
258
+ def _check_response(ai_msg):
259
+
260
+ if "persona fisica" in ai_msg.lower() and "società" in ai_msg.lower():
261
+
262
+ context_analysis_prompt = f"""
263
+ Questa è la risposta dell'LLM a un utente che vuole creare un cliente:
264
+
265
+ \"{ai_msg}\"
266
+
267
+ Devi solo rispondere con "create_client" se la risposta chiede all'utente di selezionare tra "Persona Fisica" e "Società",
268
+ oppure "SPIEGAZIONE" se si tratta solo di una descrizione generale dei tipi di clienti senza richiedere un'azione.
269
+ """
270
+ analysis_response = llm.invoke(context_analysis_prompt).content.strip()
271
+ print(analysis_response)
272
+
273
+ if analysis_response == "create_client":
274
+ return True, analysis_response
275
+
276
+ elif "partita iva" in ai_msg.lower() and "codice fiscale" in ai_msg.lower():
277
+
278
+ context_analysis_prompt = f"""
279
+ Questa è la risposta dell'LLM a un utente che deve scegliere la modalità con cui vuole inserire i dati societari
280
+
281
+ \"{ai_msg}\"
282
+
283
+ Devi solo rispondere con "select_mode" se la risposta chiede all'utente di selezionare tra "Partita IVA" e "Codice Fiscale",
284
+ oppure "SPIEGAZIONE" se si tratta solo di una descrizione generale senza richiedere un'azione.
285
+ """
286
+ analysis_response = llm.invoke(context_analysis_prompt).content.strip()
287
+ print(analysis_response)
288
+
289
+ if analysis_response == "select_mode":
290
+ return True, analysis_response
291
+
292
+ return False, ""
293
+
294
+ def _get_confirmation_message(tool_name: str, tool_args: dict) -> str:
295
+ """Genera un messaggio descrittivo per confermare l'azione che sta per essere eseguita."""
296
+
297
+ if tool_name == "create_individual":
298
+ return f"**ACTION** Stai per creare un cliente di tipo Persona Fisica: {tool_args.get('first_name', 'N/D')} {tool_args.get('last_name', 'N/D')}."
299
+
300
+ elif tool_name == "create_company_tax_code":
301
+ return f"**ACTION** Stai per creare una Società: {tool_args.get('company_name', 'N/D')}."
302
+
303
+ elif tool_name == "create_company_vat_number":
304
+ return f"**ACTION** Stai per creare una Società con Partita IVA: {tool_args.get('vat_number', 'N/D')}."
305
+
306
+ elif tool_name == "create_product":
307
+ return f"**ACTION** Stai per creare un nuovo prodotto o servizio: '{tool_args.get('name', 'N/D')}' al prezzo di {tool_args.get('price', 0):.2f}€."
308
+
309
+ return "**ACTION** Stai per eseguire un'operazione sconosciuta."
310
+
311
+ def _format_tool_args(tool_name: str, tool_args: dict) -> str:
312
+ """Trasforma i parametri tecnici in una descrizione leggibile per l'utente, gestendo i valori opzionali."""
313
+
314
+ translations = {
315
+ "first_name": "Nome",
316
+ "last_name": "Cognome",
317
+ "tax_code": "Codice Fiscale",
318
+ "address": "Indirizzo",
319
+ "street_number": "Numero Civico",
320
+ "postal_code": "CAP",
321
+ "city": "Città",
322
+ "province": "Provincia",
323
+ "country": "Nazione",
324
+ "vat_number": "Partita IVA",
325
+ "email": "Email",
326
+ "pec": "PEC",
327
+ "phone": "Telefono",
328
+ "recipient_code": "Codice Destinatario",
329
+ "company_name": "Ragione Sociale",
330
+ "name": "Nome del Prodotto",
331
+ "price": "Prezzo",
332
+ "amount": "Importo"
333
+ }
334
+
335
+ _formatted_args = "\n".join([
336
+ f"- {translations.get(key, key)}: {value if value else 'Non specificato'}"
337
+ for key, value in tool_args.items()
338
+ ])
339
+
340
+ return _formatted_args
341
+
342
+ def _get_company_info(vat_number: str) -> dict:
343
+ print(f"Getting company info for vat_number: {vat_number}")
344
+
345
+ company_info = {
346
+ "company_name": "NewCo",
347
+ "tax_code": "abcdefghilmnopqr",
348
+ "vat_number": vat_number,
349
+ "address": "via Roma",
350
+ "street_number": "12",
351
+ "postal_code": "00100",
352
+ "city": "Roma",
353
+ "province": "RM",
354
+ "country": "IT",
355
+ "email": "aaa@bbb.ccc",
356
+ "pec": "aaa@bbb.ccc",
357
+ "phone": "1234567890",
358
+ "recipient_code": "abcabcabc"
359
+ }
360
+
361
+ return company_info
362
+
363
+ def chatbot_response(message, history):
364
+
365
+ global tool_name, tool_args
366
+
367
+ history = history or []
368
+
369
+ if message.startswith("**OPERAZIONE ANNULLATA**"):
370
+ history.append({"role": "user", "content": "**OPERAZIONE ANNULLATA**"})
371
+ else:
372
+ history.append({"role": "user", "content": message})
373
+
374
+ messages = [SystemMessage(content=system_message)]
375
+ truncated_history = history[-MAX_HISTORY:] if len(history) > MAX_HISTORY else history
376
+
377
+ #print(f"Truncated history: {truncated_history}")
378
+
379
+ for entry in truncated_history:
380
+ if entry["role"] == "user":
381
+ messages.append(HumanMessage(content=entry["content"]))
382
+ elif entry["role"] == "assistant":
383
+ messages.append(AIMessage(content=entry["content"]))
384
+
385
+ if message is not None:
386
+ messages.append(HumanMessage(content=message))
387
+
388
+ #_response=llm_with_tools.stream(history_langchain_format)
389
+ ai_msg=llm_with_tools.invoke(messages)
390
+ print(ai_msg)
391
+
392
+ choice, analysis_response = _check_response(ai_msg.content)
393
+
394
+ if choice and analysis_response == "create_client":
395
+ history.append({"role": "assistant", "content": f"**CREAZIONE CLIENTE**\n{ai_msg.content}"})
396
+
397
+ elif choice and analysis_response == "select_mode":
398
+ history.append({"role": "assistant", "content": f"**MODALITA' INSERIMENTO DATI SOCIETARI**\n{ai_msg.content}"})
399
+
400
+ elif hasattr(ai_msg, "tool_calls") and ai_msg.tool_calls:
401
+
402
+ for tool_call in ai_msg.tool_calls:
403
+ tool_name = tool_call["name"].lower()
404
+ tool_args = tool_call["args"]
405
+ selected_tool = tool_mapping.get(tool_name)
406
+
407
+ if selected_tool:
408
+ if selected_tool==create_company_vat_number:
409
+ tool_args=_get_company_info(tool_args.get('vat_number', 'N/D'))
410
+ if tool_args is None:
411
+ history.append({"role": "user", "content": "Errore: Dati societari non trovati."})
412
+ return history
413
+
414
+ confirmation_message = _get_confirmation_message(tool_name, tool_args)
415
+ formatted_args = _format_tool_args(tool_name, tool_args)
416
+ ai_msg= f"{confirmation_message}\n\n**Ecco i dettagli inseriti:**\n{formatted_args}"
417
+ history.append({"role": "assistant", "content": ai_msg})
418
+
419
+ else:
420
+ history.append({"role": "assistant", "content": ai_msg.content})
421
+
422
+ return history
423
+
424
+ def reset_textbox():
425
+ """Clears the textbox after sending a message."""
426
+ return gr.update(value="")
427
+
428
+ def show_buttons(history):
429
+ global label_buttons
430
+
431
+ if history and history[-1]["content"].startswith("**ACTION**"):
432
+ label_buttons = ["Procedi", "Annulla"]
433
+ return (
434
+ gr.update(visible=False),
435
+ gr.update(visible=True, value=label_buttons[0]),
436
+ gr.update(visible=True, value=label_buttons[1])
437
+ )
438
+ elif history and history[-1]["content"].startswith("**CREAZIONE CLIENTE**"):
439
+ label_buttons = ["Persona Fisica", "Società"]
440
+ return (
441
+ gr.update(visible=False),
442
+ gr.update(visible=True, value=label_buttons[0]),
443
+ gr.update(visible=True, value=label_buttons[1])
444
+ )
445
+ elif history and history[-1]["content"].startswith("**MODALITA' INSERIMENTO DATI SOCIETARI**"):
446
+ #print("SONO QUI")
447
+ label_buttons = ["Partita IVA", "Codice Fiscale"]
448
+ return (
449
+ gr.update(visible=False),
450
+ gr.update(visible=True, value=label_buttons[0]),
451
+ gr.update(visible=True, value=label_buttons[1])
452
+ )
453
+ return (
454
+ gr.update(visible=True),
455
+ gr.update(visible=False),
456
+ gr.update(visible=False)
457
+ )
458
+
459
+ def hide_buttons():
460
+ """Hides buttons and shows the textbox after clicking a button."""
461
+ return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
462
+
463
+ def show_or_hide_buttons(history):
464
+ global label_buttons
465
+
466
+ if history and history[-1]["content"].startswith("**ACTION**"):
467
+ label_buttons = ["Procedi", "Annulla"]
468
+ return (
469
+ gr.update(visible=False),
470
+ gr.update(visible=True, value=label_buttons[0]),
471
+ gr.update(visible=True, value=label_buttons[1]),
472
+ gr.update(visible=False)
473
+ )
474
+ elif history and history[-1]["content"].startswith("**CREAZIONE CLIENTE**"):
475
+ label_buttons = ["Persona Fisica", "Società"]
476
+ return (
477
+ gr.update(visible=False),
478
+ gr.update(visible=True, value=label_buttons[0]),
479
+ gr.update(visible=True, value=label_buttons[1]),
480
+ gr.update(visible=False)
481
+ )
482
+ elif history and history[-1]["content"].startswith("**MODALITA' INSERIMENTO DATI SOCIETARI**"):
483
+ #print("SONO QUI")
484
+ label_buttons = ["Partita IVA", "Codice Fiscale"]
485
+ return (
486
+ gr.update(visible=False),
487
+ gr.update(visible=True, value=label_buttons[0]),
488
+ gr.update(visible=True, value=label_buttons[1]),
489
+ gr.update(visible=False)
490
+ )
491
+ return (
492
+ gr.update(visible=True),
493
+ gr.update(visible=False),
494
+ gr.update(visible=False),
495
+ gr.update(visible=True)
496
+ )
497
+
498
+ def button_clicked(option, history):
499
+ """Handles button clicks and updates chat history."""
500
+
501
+ global tool_name, tool_args
502
+
503
+ print(f"Button clicked: {option}")
504
+
505
+ if (option == "Procedi"):
506
+ history.append({"role": "user", "content": option})
507
+ if tool_name and tool_args:
508
+ selected_tool = tool_mapping.get(tool_name)
509
+ if selected_tool:
510
+ tool_output = selected_tool.invoke(tool_args)
511
+ history.append({"role": "assistant", "content": tool_output})
512
+ else:
513
+ history.append({"role": "user", "content": "Operazione annullata"})
514
+ elif option == "Annulla":
515
+ history.append({"role": "user", "content": option})
516
+ llm_message = (
517
+ "**OPERAZIONE ANNULLATA**\n"
518
+ "Ho annullato l'operazione corrente.\n"
519
+ "Dobbiamo modificare alcuni parametri oppure passare a una nuova operazione.\n"
520
+ )
521
+ history = chatbot_response(llm_message, history)
522
+ elif option == "Persona Fisica":
523
+ llm_message = "Persona Fisica"
524
+ history = chatbot_response(llm_message, history)
525
+ elif option == "Società":
526
+ llm_message = "Società"
527
+ history = chatbot_response(llm_message, history)
528
+ elif option == "Partita IVA":
529
+ llm_message = "Partita IVA"
530
+ history = chatbot_response(llm_message, history)
531
+ elif option == "Codice Fiscale":
532
+ llm_message = "Codice Fiscale"
533
+ history = chatbot_response(llm_message, history)
534
+
535
+ tool_name = ""
536
+ tool_args = ""
537
+
538
+ return history
539
+
540
+ # Authentication function
541
+ def authenticate(username, password):
542
+ if username in credentials and credentials[username] == password:
543
+ print("🔑 Login successful!")
544
+ return gr.update(visible=False), gr.update(visible=True), gr.update(value="", visible=False) # Hide login, show chatbot, clear error
545
+ else:
546
+ print("❌ Incorrect username or password")
547
+ return gr.update(visible=True), gr.update(visible=False), gr.update(value="❌ Incorrect username or password", visible=True) # Show error
548
+
549
+
550
+ with gr.Blocks() as demo:
551
+
552
+ # 🔒 Login Section (Initially Visible)
553
+ with gr.Column(visible=True) as login_section:
554
+ gr.Markdown("### 🔒 Login Required")
555
+ username_input = gr.Textbox(label="Username")
556
+ password_input = gr.Textbox(label="Password", type="password")
557
+ login_button = gr.Button("Login")
558
+ error_message = gr.Text("", visible=False)
559
+
560
+ # 🧠 Chatbot Section (Initially Hidden)
561
+ with gr.Column(visible=False) as chatbot_section:
562
+
563
+ chatbot = gr.Chatbot(
564
+ label="Assistente Forfè",
565
+ type="messages"
566
+ )
567
+
568
+ user_input = gr.Textbox(label="Utente",placeholder="Cosa vuoi chiedere al tuo assistente Forfè?")
569
+
570
+ with gr.Row():
571
+ btn1 = gr.Button("", visible=False)
572
+ btn2 = gr.Button("", visible=False)
573
+
574
+ send_btn = gr.Button("Invia")
575
+
576
+ # When user submits text
577
+ user_input.submit(chatbot_response, [user_input, chatbot], chatbot) \
578
+ .then(reset_textbox, None, user_input) \
579
+ .then(show_or_hide_buttons, chatbot, [user_input, btn1, btn2, send_btn])
580
+ #.then(show_buttons, chatbot, [user_input, btn1, btn2])
581
+
582
+ # When user clicks send
583
+ send_btn.click(chatbot_response, [user_input, chatbot], chatbot) \
584
+ .then(reset_textbox, None, user_input) \
585
+ .then(show_or_hide_buttons, chatbot, [user_input, btn1, btn2, send_btn])
586
+ #.then(show_buttons, chatbot, [user_input, btn1, btn2])
587
+
588
+ # Button clicks: Show textbox, hide buttons
589
+ btn1.click(lambda h: button_clicked(label_buttons[0], h), chatbot, chatbot) \
590
+ .then(show_or_hide_buttons, chatbot, [user_input, btn1, btn2, send_btn])
591
+ #.then(hide_buttons, None, [user_input, btn1, btn2])
592
+
593
+ btn2.click(lambda h: button_clicked(label_buttons[1], h), chatbot, chatbot) \
594
+ .then(show_or_hide_buttons, chatbot, [user_input, btn1, btn2, send_btn])
595
+ #.then(hide_buttons, None, [user_input, btn1, btn2])
596
+
597
+ # 🔑 Login Button Action (Now updates visibility correctly)
598
+ login_button.click(
599
+ authenticate,
600
+ [username_input, password_input],
601
+ [login_section, chatbot_section, error_message]
602
+ )
603
+
604
+
605
+
606
+ demo.launch(
607
+ debug=True,
608
+ #share=True
609
+ )
readme.md ADDED
File without changes
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio==5.16.0
2
+ langchain==0.3.18
3
+ langchain_community==0.3.17
4
+ langchain_core==0.3.34
5
+ langchain_openai==0.3.5
6
+ langchain_qdrant==0.2.0
7
+ python-dotenv==1.0.1
8
+ qdrant_client==1.13.2
9
+ sentence_transformers==3.4.1
utilities/qdrant/QdrantLangchainManager.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ from qdrant_client import QdrantClient
5
+ from qdrant_client.models import Distance, VectorParams
6
+
7
+ from langchain_openai import ChatOpenAI, OpenAIEmbeddings
8
+ from langchain_qdrant import QdrantVectorStore
9
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
10
+ from langchain_community.document_loaders import TextLoader, PyPDFLoader
11
+ from langchain_core.documents import Document
12
+ from langchain_core.messages import SystemMessage, HumanMessage
13
+
14
+ from sentence_transformers import CrossEncoder
15
+
16
+ class QdrantLangchainManager:
17
+
18
+ def __init__(self, qdrant_url, qdrant_api_key,
19
+ system_message=None, llm=None, crossencoder=None,
20
+ batch_size=500, chunk_size=2000, chunk_overlap=50, vector_size=1536, re_ranking_threshold=1):
21
+
22
+ self.qdrant_url = qdrant_url
23
+ self.qdrant_api_key = qdrant_api_key
24
+ self.client = QdrantClient(url=qdrant_url, api_key=qdrant_api_key)
25
+
26
+ self.system_message = system_message or """
27
+ You are a helpful assistant that assists users in finding solutions and answering their questions.
28
+ If a question is related to our specific environment, I will provide additional [context].
29
+ You should consider this [context] when formulating your response.
30
+ However, if the provided [context] is not relevant to the question, you can proceed based on your general knowledge.
31
+ Always prioritize clarity and usefulness in your answers.
32
+
33
+ [context]
34
+ {context}
35
+ """
36
+
37
+ self.llm = llm
38
+ self.crossencoder = crossencoder if crossencoder else None
39
+
40
+ self.batch_size = batch_size
41
+ self.chunk_size = chunk_size
42
+ self.chunk_overlap = chunk_overlap
43
+ self.vector_size = vector_size
44
+ self.re_ranking_threshold = re_ranking_threshold
45
+
46
+ self.vectorstore = None
47
+
48
+ def create_collection(self, collection_name):
49
+ try:
50
+ if not self.client.collection_exists(collection_name):
51
+ self.client.create_collection(
52
+ collection_name=collection_name,
53
+ vectors_config=VectorParams(size=self.vector_size, distance=Distance.COSINE)
54
+ )
55
+ print(f"✅ Collection '{collection_name}' created successfully.")
56
+ else:
57
+ print(f"⚠️ Collection '{collection_name}' already exists.")
58
+
59
+ self.vectorstore = QdrantVectorStore(
60
+ client=self.client,
61
+ collection_name=collection_name,
62
+ embedding=OpenAIEmbeddings(),
63
+ )
64
+ return True
65
+ except Exception as e:
66
+ print(f"❌ Error creating collection '{collection_name}': {e}")
67
+ return False
68
+
69
+ def get_collection(self, collection_name):
70
+ try:
71
+ if not self.client.collection_exists(collection_name):
72
+ print(f"⚠️ Collection '{collection_name}' doesn't exist.")
73
+ return False
74
+ self.vectorstore = QdrantVectorStore(
75
+ client=self.client,
76
+ collection_name=collection_name,
77
+ embedding=OpenAIEmbeddings(),
78
+ )
79
+ return True
80
+ except Exception as e:
81
+ print(f"❌ Error getting collection '{collection_name}': {e}")
82
+ return False
83
+
84
+ def insert_documents(self, file_path):
85
+ if not self.vectorstore:
86
+ print("⚠️ No collection initialized. Please create or load a collection first.")
87
+ return False
88
+
89
+ try:
90
+ file_extension = os.path.splitext(file_path)[-1].lower()
91
+
92
+ loader = TextLoader(file_path) if file_extension == ".txt" else PyPDFLoader(file_path) if file_extension == ".pdf" else None
93
+ if not loader:
94
+ raise ValueError(f"Unsupported file type: {file_extension}")
95
+
96
+ docs = loader.load()
97
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=self.chunk_size, chunk_overlap=self.chunk_overlap)
98
+ chunks = text_splitter.split_documents(docs)
99
+
100
+ max_iterations = 3
101
+ iteration = 0
102
+ merge_performed = True
103
+ while merge_performed and iteration < max_iterations:
104
+ chunks, merge_performed = self._merge_chunks(chunks)
105
+ iteration += 1
106
+ print(f"🔄 Merge Iteration {iteration}: {len(chunks)} chunks remain.")
107
+
108
+ for i in range(0, len(chunks), self.batch_size):
109
+ batch = chunks[i:i + self.batch_size]
110
+ self.vectorstore.add_documents(batch)
111
+ print(f"✅ Inserted {len(batch)} documents (Batch {i // self.batch_size + 1})")
112
+ return True
113
+ except Exception as e:
114
+ print(f"❌ Error inserting documents: {e}")
115
+ return False
116
+
117
+ def search_qdrant(self, query, top_k=3):
118
+ if not self.vectorstore:
119
+ print("⚠️ No collection initialized. Please create or load a collection first.")
120
+ return None
121
+
122
+ if not self.llm:
123
+ print("⚠️ LLM not initialized. Cannot process response.")
124
+ return None
125
+
126
+ try:
127
+ docs_with_scores = self.vectorstore.similarity_search_with_score(query, k=top_k)
128
+
129
+ print("\n🔍 Search Results:")
130
+ context=""
131
+ for doc, score in docs_with_scores:
132
+ print(f"* [score={score:3f}] {doc.page_content[:10]}")
133
+
134
+ if self.crossencoder:
135
+ docs_with_scores = self._reranking(query, docs_with_scores)
136
+ for (doc, old_score), new_score in docs_with_scores:
137
+ print(f"🔹 Old Score: {old_score:.2f} ➝ New Score: {new_score:.2f} - {doc.page_content[:10]}...")
138
+
139
+ top_score = docs_with_scores[0][1] if docs_with_scores else 0
140
+ context = "\n\n".join([doc.page_content for (doc, _), new_score in docs_with_scores if new_score > self.re_ranking_threshold]) if top_score > 0 else ""
141
+ final_system_message = self.system_message.format(context=context) if context else self.system_message.replace("[context]\n{context}", "").strip()
142
+
143
+ messages = [SystemMessage(content=final_system_message), HumanMessage(content=query)]
144
+ ai_msg = self.llm.invoke(messages)
145
+ #print(ai_msg)
146
+ # Ensure `ai_msg` contains a valid response
147
+ if hasattr(ai_msg, "content"):
148
+ return ai_msg.content # Extract only the useful content
149
+ else:
150
+ return None
151
+ except Exception as e:
152
+ print(f"❌ Error during search: {e}")
153
+ return None
154
+
155
+ def delete_collection(self, collection_name):
156
+ try:
157
+ self.client.delete_collection(collection_name)
158
+ print(f"🚨 Collection '{collection_name}' has been deleted.")
159
+ self.vectorstore = None
160
+ return True
161
+ except Exception as e:
162
+ print(f"❌ Error deleting collection '{collection_name}': {e}")
163
+ return False
164
+
165
+ def _merge_chunks(self, chunks, min_size=500, max_size=2000):
166
+ if not chunks:
167
+ return [], False
168
+
169
+ merged_chunks = []
170
+ temp_text = ""
171
+ merge_performed = False
172
+
173
+ if len(chunks[0].page_content) < min_size and len(chunks) > 1:
174
+ chunks[1] = Document(page_content=chunks[0].page_content + " " + chunks[1].page_content)
175
+ chunks = chunks[1:]
176
+ merge_performed = True
177
+
178
+ for chunk in chunks:
179
+ text = chunk.page_content
180
+ if not temp_text:
181
+ temp_text = text
182
+ continue
183
+ if len(text) < min_size:
184
+ temp_text += " " + text
185
+ merge_performed = True
186
+ else:
187
+ while len(temp_text) > max_size:
188
+ merged_chunks.append(Document(page_content=temp_text[:max_size]))
189
+ temp_text = temp_text[max_size:]
190
+ merged_chunks.append(Document(page_content=temp_text))
191
+ temp_text = text
192
+
193
+ if temp_text:
194
+ merged_chunks.append(Document(page_content=temp_text))
195
+
196
+ return merged_chunks, merge_performed
197
+
198
+ def _reranking(self, query, docs_with_scores):
199
+ if not self.crossencoder:
200
+ print("⚠️ Crossencoder not initialized. Skipping reranking.")
201
+ return docs_with_scores
202
+
203
+ reranker = CrossEncoder(self.crossencoder)
204
+ query_pairs = [(query, doc.page_content) for doc, _ in docs_with_scores]
205
+ new_scores = reranker.predict(query_pairs)
206
+ return sorted(zip(docs_with_scores, new_scores), key=lambda x: x[1], reverse=True)
utilities/qdrant/langchain_utils.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ from qdrant_client import QdrantClient
5
+ from qdrant_client.models import Distance, VectorParams, PointStruct
6
+
7
+ from langchain_openai import ChatOpenAI
8
+ from langchain_openai import OpenAIEmbeddings
9
+ from langchain_qdrant import QdrantVectorStore
10
+
11
+ from langchain.text_splitter import RecursiveCharacterTextSplitter,CharacterTextSplitter
12
+ from langchain_community.document_loaders import TextLoader, PyPDFLoader
13
+ from langchain_core.documents import Document
14
+ from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, ToolMessage
15
+
16
+ from sentence_transformers import CrossEncoder
17
+
18
+ load_dotenv()
19
+
20
+ qdrant_url=os.getenv("QDRANT_URL")
21
+ qdrant_api_key=os.getenv("QDRANT_API_KEY")
22
+
23
+ BATCH_SIZE=500
24
+ CHUNK_SIZE=2000
25
+ CHUNK_OVERLAP=50
26
+ VECTOR_SIZE=1536
27
+ RE_RANKING_THRESHOLD=2
28
+
29
+ system_message = """
30
+ You are a helpful assistant that assists users in finding solutions and answering their questions.
31
+ If a question is related to our specific environment, I will provide additional [context].
32
+ You should consider this [context] when formulating your response.
33
+
34
+ However, if the provided [context] is not relevant to the question, you can proceed based on your general knowledge.
35
+ Always prioritize clarity and usefulness in your answers.
36
+
37
+ [context]
38
+ {context}
39
+ """
40
+
41
+
42
+ # Initialize Qdrant client
43
+ client = QdrantClient(
44
+ url=qdrant_url,
45
+ api_key=qdrant_api_key
46
+ )
47
+
48
+ llm = ChatOpenAI(
49
+ model="gpt-4o-mini",
50
+ streaming=True,
51
+ )
52
+
53
+ messages = [SystemMessage(content=system_message)]
54
+
55
+ """LANGCHAIN CRUD_OPERATIONS"""
56
+ def langchain_create_collection(collection_name: str, vector_size: int = VECTOR_SIZE):
57
+ """Create a new Qdrant collection and return a LangChain vector store."""
58
+ # Create the collection (or recreate if it exists)
59
+ if not client.collection_exists(collection_name):
60
+ client.create_collection(
61
+ collection_name=collection_name,
62
+ vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE)
63
+ )
64
+ print(f"✅ Collection '{collection_name}' created successfully.")
65
+ else:
66
+ print(f"⚠️ Collection '{collection_name}' already exists.")
67
+
68
+ # Now initialize the LangChain Qdrant vector store
69
+ vectorstore = QdrantVectorStore(
70
+ client=client,
71
+ collection_name=collection_name,
72
+ embedding=OpenAIEmbeddings(),
73
+ )
74
+
75
+ return vectorstore
76
+
77
+ def langchain_get_collection(collection_name: str, vector_size: int = VECTOR_SIZE):
78
+ """Create a new Qdrant collection and return a LangChain vector store."""
79
+ # Create the collection (or recreate if it exists)
80
+ if not client.collection_exists(collection_name):
81
+ print(f"⚠️ Collection '{collection_name}' doesn't exist.")
82
+ return None
83
+
84
+ # Now initialize the LangChain Qdrant vector store
85
+ vectorstore = QdrantVectorStore(
86
+ client=client,
87
+ collection_name=collection_name,
88
+ embedding=OpenAIEmbeddings(),
89
+ )
90
+
91
+ return vectorstore
92
+
93
+ def langchain_insert_documents(file_path: str, vectorstore):
94
+ """Load, chunk, and insert documents into Qdrant via LangChain."""
95
+
96
+ file_extension = os.path.splitext(file_path)[-1].lower()
97
+
98
+ if file_extension == ".txt":
99
+ loader = TextLoader(file_path)
100
+ elif file_extension == ".pdf":
101
+ loader = PyPDFLoader(file_path)
102
+ else:
103
+ raise ValueError(f"Unsupported file type: {file_extension}")
104
+
105
+ docs = loader.load()
106
+
107
+ text_splitter = RecursiveCharacterTextSplitter(
108
+ chunk_size=CHUNK_SIZE,
109
+ chunk_overlap=CHUNK_OVERLAP,
110
+ )
111
+ chunks = text_splitter.split_documents(docs)
112
+
113
+ # Iterative merging
114
+ max_iterations = 3 # Set max iterations
115
+ iteration = 0
116
+ merge_performed = True # Initialize to True for first iteration
117
+
118
+ while merge_performed and iteration < max_iterations:
119
+ chunks, merge_performed = _merge_chunks(chunks)
120
+ iteration += 1
121
+ print(f"🔄 Merge Iteration {iteration}: {len(chunks)} chunks remain.")
122
+
123
+ # how many chunks
124
+ print(f"📄 Loaded {len(docs)} documents from '{file_path}'.")
125
+ print(f"🔄 Final Chunks after merging: {len(chunks)} total.")
126
+ for i, chunk in enumerate(chunks, start=1):
127
+ print(f"Chunk {i}: {len(chunk.page_content)} characters - {chunk.page_content[:10]}...")
128
+
129
+ # 🔥 Insert data in small batches to prevent timeout
130
+ for i in range(0, len(chunks), BATCH_SIZE):
131
+ batch = chunks[i:i + BATCH_SIZE]
132
+ vectorstore.add_documents(batch)
133
+ print(f"✅ Inserted {len(batch)} documents (Batch {i // BATCH_SIZE + 1})")
134
+
135
+ print(f"✅ Finished inserting {len(chunks)} total documents.")
136
+
137
+ def langchain_search_qdrant(query: str, vectorstore, top_k: int = 3):
138
+ """Search Qdrant for the most relevant documents."""
139
+ # retriever = vectorstore.as_retriever(search_kwargs={"k": top_k})
140
+ # results = retriever.invoke(query)
141
+
142
+ docs_with_scores = vectorstore.similarity_search_with_score(query, k=top_k)
143
+
144
+ print("\n🔍 Search Results:")
145
+ context=""
146
+ for doc, score in docs_with_scores:
147
+ print(f"* [score={score:3f}] {doc.page_content[:10]}")
148
+
149
+ docs_re_ranked = _reranking(query, docs_with_scores)
150
+ for (doc, old_score), new_score in docs_re_ranked:
151
+ print(f"🔹 Old Score: {old_score:.2f} ➝ New Score: {new_score:.2f} - {doc.page_content[:10]}...")
152
+
153
+ top_score = docs_re_ranked[0][1] # Get highest new score
154
+ if top_score > 0:
155
+ filtered_results = [doc for (doc, _), new_score in docs_re_ranked if new_score > RE_RANKING_THRESHOLD]
156
+ context = "\n\n".join([doc.page_content for doc in filtered_results])
157
+ final_system_message = system_message.format(context=context)
158
+ else:
159
+ final_system_message = system_message.replace("[context]\n{context}", "").strip()
160
+
161
+ messages = [SystemMessage(content=final_system_message)]
162
+ messages.append(HumanMessage(content=query))
163
+
164
+ ai_msg=llm.invoke(messages)
165
+ print(ai_msg)
166
+
167
+ def langchain_update_document(vectorstore, collection_name: str, point_id: int, new_text: str):
168
+ """Update an existing document in Qdrant."""
169
+ # Delete old vector
170
+ client.delete(collection_name=collection_name, points=[point_id])
171
+
172
+ # Insert new data
173
+ new_vector = OpenAIEmbeddings().embed_query(new_text)
174
+ vectorstore.add_texts([new_text], metadatas=[{"id": point_id}], ids=[point_id])
175
+
176
+ print(f"✅ Updated document ID {point_id} in '{collection_name}'.")
177
+
178
+ def langchain_delete_point(vectorstore, collection_name: str, point_id: int):
179
+ """Delete a specific vector from Qdrant."""
180
+ client.delete(collection_name=collection_name, points=[point_id])
181
+ print(f"🗑️ Deleted document ID {point_id} from '{collection_name}'.")
182
+
183
+ def langchain_delete_collection(collection_name: str):
184
+ """Delete an entire collection from Qdrant."""
185
+ client.delete_collection(collection_name=collection_name)
186
+ print(f"🚨 Collection '{collection_name}' has been deleted.")
187
+
188
+ """PRIVATE FUNCTIONS"""
189
+ def _merge_chunks(chunks, min_size=500, max_size=2000):
190
+ """Merge small chunks into larger ones while keeping document structure intact.
191
+
192
+ Returns:
193
+ - merged_chunks: List of Document objects after merging
194
+ - merge_performed: Boolean indicating if merging happened
195
+ """
196
+ if not chunks:
197
+ return [], False
198
+
199
+ merged_chunks = []
200
+ temp_text = ""
201
+ merge_performed = False # ✅ Track if we performed a merge
202
+
203
+ # ✅ Special case: Ensure the FIRST chunk is not too small
204
+ if len(chunks[0].page_content) < min_size and len(chunks) > 1:
205
+ chunks[1] = Document(page_content=chunks[0].page_content + " " + chunks[1].page_content)
206
+ chunks = chunks[1:] # Remove the first chunk (merged into the second)
207
+ merge_performed = True # ✅ Mark that a merge was performed
208
+
209
+ for chunk in chunks:
210
+ text = chunk.page_content # Extract text content from Document
211
+
212
+ if not temp_text:
213
+ temp_text = text
214
+ continue
215
+
216
+ # Merge small chunks
217
+ if len(text) < min_size:
218
+ temp_text += " " + text # Append small chunk to previous
219
+ merge_performed = True # ✅ A merge happened
220
+ else:
221
+ # Ensure chunk does not exceed max_size
222
+ while len(temp_text) > max_size:
223
+ merged_chunks.append(Document(page_content=temp_text[:max_size])) # ✅ Convert to Document
224
+ temp_text = temp_text[max_size:]
225
+
226
+ merged_chunks.append(Document(page_content=temp_text)) # ✅ Convert to Document
227
+ temp_text = text # Start new chunk
228
+
229
+ # Add any remaining chunk
230
+ if temp_text:
231
+ merged_chunks.append(Document(page_content=temp_text)) # ✅ Convert to Document
232
+
233
+ return merged_chunks, merge_performed # ✅ Return boolean
234
+
235
+ def _reranking(query, docs_with_scores):
236
+
237
+ reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
238
+
239
+ doc_texts = [doc.page_content for doc, _ in docs_with_scores]
240
+ query_pairs = [(query, doc) for doc in doc_texts]
241
+ new_scores = reranker.predict(query_pairs)
242
+
243
+ re_ranked_results = sorted(zip(docs_with_scores, new_scores), key=lambda x: x[1], reverse=True)
244
+
245
+ # for (doc, old_score), new_score in re_ranked_results:
246
+ # print(f"🔹 Old Score: {old_score:.2f} ➝ New Score: {new_score:.2f} - {doc.page_content[:10]}...")
247
+
248
+ return re_ranked_results
utilities/qdrant/test.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ import os
3
+
4
+ from langchain_utils import *
5
+ from QdrantLangchainManager import *
6
+
7
+
8
+ load_dotenv()
9
+
10
+ # parameters
11
+ qdrant_url=os.getenv("QDRANT_URL")
12
+ qdrant_api_key=os.getenv("QDRANT_API_KEY")
13
+ collection_name="5g reference"
14
+ llm = ChatOpenAI(
15
+ model="gpt-4o-mini",
16
+ streaming=True
17
+ )
18
+ crossencoder = "cross-encoder/ms-marco-MiniLM-L-6-v2"
19
+
20
+ manager = QdrantLangchainManager(
21
+ qdrant_url=qdrant_url,
22
+ qdrant_api_key=qdrant_api_key,
23
+ llm=llm,
24
+ crossencoder=crossencoder
25
+ )
26
+
27
+ # result = manager.create_collection("my_collection")
28
+ # print("The result is: ", result)
29
+ # if result:
30
+ # result=manager.delete_collection("my_collection")
31
+ # print("The result is: ", result)
32
+
33
+ result = manager.get_collection(collection_name)
34
+ print("The result is: ", result)
35
+ #query="who is jokerbirot?"
36
+ query="Can you tell me something about 5g modem?"
37
+ result = manager.search_qdrant(query)
38
+ print(result)
39
+
40
+ # manager.insert_documents("example.pdf", vectorstore)
41
+ # manager.search_qdrant("example query", vectorstore)
42
+
43
+ # langchain_delete_collection(collection_name)
44
+ # vectorestore=langchain_create_collection(collection_name)
45
+ # langchain_insert_documents("../../data/5G reference - en.pdf", vectorestore)
46
+
47
+ # vectorestore=langchain_get_collection(collection_name)
48
+
49
+ #query="who is jokerbirot?"
50
+ #query="what is a qubit?"
51
+ #query="Configuring a 5G SA Modem"
52
+ #langchain_search_qdrant(query, vectorestore)
53
+
54
+ # vectorstore=langchain_create_collection("jokerbirot_saga")
55
+ # langchain_insert_documents("data/jokerbirot_saga.txt", vectorstore)
56
+
57
+ # query="who is jokerbirot?"
58
+ # langchain_search_qdrant(query, vectorstore)
59
+
utilities/qdrant/utils.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ from qdrant_client import QdrantClient
5
+ from qdrant_client.models import Distance, VectorParams, PointStruct
6
+
7
+ load_dotenv()
8
+
9
+ qdrant_url=os.getenv("QDRANT_URL")
10
+ qdrant_api_key=os.getenv("QDRANT_API_KEY")
11
+
12
+ BATCH_SIZE=500
13
+ CHUNK_SIZE=2000
14
+ CHUNK_OVERLAP=50
15
+ VECTOR_SIZE=1536
16
+ RE_RANKING_THRESHOLD=2
17
+
18
+ system_message = """
19
+ You are a helpful assistant that assists users in finding solutions and answering their questions.
20
+ If a question is related to our specific environment, I will provide additional [context].
21
+ You should consider this [context] when formulating your response.
22
+
23
+ However, if the provided [context] is not relevant to the question, you can proceed based on your general knowledge.
24
+ Always prioritize clarity and usefulness in your answers.
25
+
26
+ [context]
27
+ {context}
28
+ """
29
+
30
+ # Initialize Qdrant client
31
+ client = QdrantClient(
32
+ url=qdrant_url,
33
+ api_key=qdrant_api_key
34
+ )
35
+
36
+ def create_collection(collection_name: str, vector_size: int = VECTOR_SIZE):
37
+ """Create a new Qdrant collection."""
38
+ if not client.collection_exists(collection_name):
39
+ client.create_collection(
40
+ collection_name=collection_name,
41
+ vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE)
42
+ )
43
+ print(f"✅ Collection '{collection_name}' created successfully.")
44
+ else:
45
+ print(f"⚠️ Collection '{collection_name}' already exists.")
46
+
47
+ def get_collection(collection_name: str, vector_size: int = VECTOR_SIZE):
48
+ """Create a new Qdrant collection and return a LangChain vector store."""
49
+ # Create the collection (or recreate if it exists)
50
+ if not client.collection_exists(collection_name):
51
+ print(f"⚠️ Collection '{collection_name}' doesn't exist.")
52
+
53
+ def delete_collection(collection_name: str):
54
+ """Delete an entire collection."""
55
+ client.delete_collection(collection_name=collection_name)
56
+ print(f"🚨 Collection '{collection_name}' has been deleted.")
57
+
58
+ def list_collections():
59
+ """List all collections in Qdrant."""
60
+ collections = client.get_collections()
61
+ print("🔹 Available Collections:")
62
+ for collection in collections.collections:
63
+ print(f"- {collection.name}")
64
+
65
+ def insert_data(collection_name: str, point_id: int, vector: list, payload: dict):
66
+ """Insert a vector with metadata into Qdrant."""
67
+
68
+ """
69
+ Example Usage
70
+ sample_vector = np.random.rand(1536).tolist() # Fake embedding vector
71
+ insert_data("sample_collection", point_id=1, vector=sample_vector, payload={"text": "This is a sample document."})
72
+ """
73
+
74
+ client.upsert(
75
+ collection_name=collection_name,
76
+ points=[
77
+ PointStruct(id=point_id, vector=vector, payload=payload)
78
+ ]
79
+ )
80
+ print(f"✅ Data inserted into '{collection_name}' with ID {point_id}")
81
+
82
+ def delete_point(collection_name: str, point_id: int):
83
+ """Delete a specific point from a collection."""
84
+
85
+ """
86
+ Example Usage
87
+ delete_point("sample_collection", 1)
88
+ """
89
+
90
+ client.delete(collection_name=collection_name, points=[point_id])
91
+ print(f"🗑️ Deleted point ID {point_id} from '{collection_name}'.")
92
+
93
+ def search_data(collection_name: str, query_vector: list, top_k: int = 3):
94
+ """Search for the closest vectors in Qdrant."""
95
+
96
+ """
97
+ Example Usage
98
+ search_data("sample_collection", query_vector=sample_vector)
99
+ """
100
+
101
+ results = client.search(
102
+ collection_name=collection_name,
103
+ query_vector=query_vector,
104
+ limit=top_k
105
+ )
106
+ print("🔍 Search Results:")
107
+ for res in results:
108
+ print(f"- ID: {res.id}, Score: {res.score}, Payload: {res.payload}")