Spaces:
Build error
Build error
Merge branch 'main' of https://github.com/paisleypark3121/ibn
Browse files- .gitignore +11 -0
- app.py +530 -0
- old_app.py +609 -0
- readme.md +0 -0
- requirements.txt +9 -0
- utilities/qdrant/QdrantLangchainManager.py +206 -0
- utilities/qdrant/langchain_utils.py +248 -0
- utilities/qdrant/test.py +59 -0
- utilities/qdrant/utils.py +108 -0
.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}")
|