Améliorez Amazon Lex avec des LLMs et améliorez l’expérience FAQ en utilisant l’ingestion d’URL
Améliorez Amazon Lex avec des LLMs et l'expérience FAQ avec l'ingestion d'URL
Dans le monde numérique d’aujourd’hui, la plupart des consommateurs préfèrent trouver des réponses à leurs questions de service client par eux-mêmes plutôt que de prendre le temps de contacter des entreprises et/ou des prestataires de services. Cet article de blog explore une solution innovante pour construire un chatbot de questions-réponses dans Amazon Lex qui utilise les FAQ existantes de votre site web. Cet outil alimenté par l’IA peut fournir des réponses rapides et précises aux demandes du monde réel, permettant au client de résoudre rapidement et facilement des problèmes courants de manière indépendante.
Ingestion d’une seule URL
De nombreuses entreprises disposent d’un ensemble de réponses publiées pour les questions fréquemment posées (FAQ) de leurs clients, disponibles sur leur site web. Dans ce cas, nous voulons offrir aux clients un chatbot qui peut répondre à leurs questions à partir de nos FAQ publiées. Dans l’article de blog intitulé “Améliorer Amazon Lex avec des fonctionnalités de FAQ conversationnelles en utilisant LLMs”, nous avons démontré comment vous pouvez utiliser une combinaison d’Amazon Lex et de LlamaIndex pour construire un chatbot alimenté par vos sources de connaissances existantes, telles que des documents PDF ou Word. Pour prendre en charge une FAQ simple, basée sur un site web de FAQ, nous devons créer un processus d’ingestion qui peut parcourir le site web et créer des embeddings qui peuvent être utilisés par LlamaIndex pour répondre aux questions des clients. Dans ce cas, nous allons nous appuyer sur le chatbot créé dans l’article de blog précédent, qui interroge ces embeddings avec l’énoncé d’un utilisateur et renvoie la réponse des FAQ du site web.
Le diagramme suivant montre comment le processus d’ingestion et le chatbot Amazon Lex fonctionnent ensemble pour notre solution.
- Améliorez Amazon Lex avec des fonctionnalités de FAQ conversationnelle en utilisant des LLMs
- L’ajustement des symboles améliore l’apprentissage en contexte dans les modèles de langage
- Llama 2 est là – obtenez-le sur Hugging Face
Dans le flux de travail de la solution, le site web avec les FAQ est ingéré via AWS Lambda. Cette fonction Lambda parcourt le site web et stocke le texte résultant dans un compartiment Amazon Simple Storage Service (Amazon S3). Le compartiment S3 déclenche ensuite une fonction Lambda qui utilise LlamaIndex pour créer des embeddings qui sont stockés dans Amazon S3. Lorsqu’une question d’un utilisateur final arrive, comme “Quelle est votre politique de retour ?”, le chatbot Amazon Lex utilise sa fonction Lambda pour interroger les embeddings en utilisant une approche basée sur RAG avec LlamaIndex. Pour plus d’informations sur cette approche et les prérequis, consultez l’article de blog “Améliorer Amazon Lex avec des fonctionnalités de FAQ conversationnelles en utilisant LLMs”.
Une fois les prérequis de l’article de blog susmentionné terminés, la première étape consiste à ingérer les FAQ dans un référentiel de documents qui peuvent être vectorisés et indexés par LlamaIndex. Le code suivant montre comment accomplir cela :
import logging
import sys
import requests
import html2text
from llama_index.readers.schema.base import Document
from llama_index import GPTVectorStoreIndex
from typing import List
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))
class EZWebLoader:
def __init__(self, default_header: str = None):
self._html_to_text_parser = html2text()
if default_header is None:
self._default_header = {"User-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36"}
else:
self._default_header = default_header
def load_data(self, urls: List[str], headers: str = None) -> List[Document]:
if headers is None:
headers = self._default_header
documents = []
for url in urls:
response = requests.get(url, headers=headers).text
response = self._html2text.html2text(response)
documents.append(Document(response))
return documents
url = "http://www.zappos.com/general-questions"
loader = EZWebLoader()
documents = loader.load_data([url])
index = GPTVectorStoreIndex.from_documents(documents)
Dans l’exemple précédent, nous prenons une URL de site web de FAQ prédéfinie de Zappos et nous l’ingérons à l’aide de la classe EZWebLoader
. Avec cette classe, nous avons accédé à l’URL et chargé toutes les questions qui se trouvent sur la page dans un index. Nous pouvons maintenant poser une question comme “Est-ce que Zappos propose des cartes-cadeaux ?” et obtenir les réponses directement à partir de nos FAQ sur le site web. La capture d’écran suivante montre la console de test du chatbot Amazon Lex répondant à cette question à partir des FAQ.
Nous avons pu réaliser cela car nous avions parcouru l’URL lors de la première étape et créé des embeddings que LlamaIndex pourrait utiliser pour rechercher la réponse à notre question. La fonction Lambda de notre bot montre comment cette recherche est exécutée chaque fois que l’intention de secours est retournée :
import time
import json
import os
import logging
import boto3
from llama_index import StorageContext, load_index_from_storage
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
def download_docstore():
# Créez un client S3
s3 = boto3.client('s3')
# Liste tous les objets dans le seau S3 et télécharge chacun
try:
bucket_name = 'faq-bot-storage-001'
s3_response = s3.list_objects_v2(Bucket=bucket_name)
if 'Contents' in s3_response:
for item in s3_response['Contents']:
file_name = item['Key']
logger.debug("Téléchargement vers /tmp/" + file_name)
s3.download_file(bucket_name, file_name, '/tmp/' + file_name)
logger.debug('Tous les fichiers ont été téléchargés depuis S3 et écrits sur le système de fichiers local.')
except Exception as e:
logger.error(e)
raise e
# télécharge le stockage de documents localement
download_docstore()
storage_context = StorageContext.from_defaults(persist_dir="/tmp/")
# charge l'index
index = load_index_from_storage(storage_context)
query_engine = index.as_query_engine()
def lambda_handler(event, context):
"""
Routage de la demande entrante en fonction de l'intention.
Le corps JSON de la demande est fourni dans l'emplacement de l'événement.
"""
# Par défaut, considérez la demande de l'utilisateur comme provenant du fuseau horaire America/New_York.
os.environ['TZ'] = 'America/New_York'
time.tzset()
logger.debug("===== DÉBUT DE L'EXÉCUTION DE LEX ====")
logger.debug(event)
slots = {}
if "currentIntent" in event and "slots" in event["currentIntent"]:
slots = event["currentIntent"]["slots"]
intent = event["sessionState"]["intent"]
dialogaction = {"type": "Delegate"}
message = []
if str.lower(intent["name"]) == "fallbackintent":
# exécutez la requête à partir de l'entrée fournie par l'utilisateur
response = str.strip(query_engine.query(event["inputTranscript"]).response)
dialogaction["type"] = "Close"
message.append({'content': f'{response}', 'contentType': 'PlainText'})
final_response = {
"sessionState": {
"dialogAction": dialogaction,
"intent": intent
},
"messages": message
}
logger.debug(json.dumps(final_response, indent=1))
logger.debug("===== FIN DE L'EXÉCUTION DE LEX ====")
return final_response
Cette solution fonctionne bien lorsqu’une seule page web contient toutes les réponses. Cependant, la plupart des sites de FAQ ne sont pas construits sur une seule page. Par exemple, dans notre exemple avec Zappos, si nous posons la question “Avez-vous une politique de correspondance des prix ?”, nous obtenons une réponse insatisfaisante, comme le montre la capture d’écran suivante.
Dans l’interaction précédente, la réponse sur la politique de correspondance des prix n’est pas utile pour notre utilisateur. Cette réponse est courte car la FAQ référencée est un lien vers une page spécifique sur la politique de correspondance des prix et notre parcours web ne concernait que la page unique. Pour obtenir de meilleures réponses, il faudra parcourir également ces liens. La prochaine section montre comment obtenir des réponses à des questions qui nécessitent deux niveaux ou plus de profondeur de page.
Parcours à niveaux multiples
Lorsque nous parcourons une page web pour trouver des connaissances sur les FAQ, les informations que nous recherchons peuvent être contenues dans des pages liées. Par exemple, dans notre exemple avec Zappos, si nous posons la question “Avez-vous une politique de correspondance des prix ?”, la réponse est “Oui, veuillez visiter <lien> pour en savoir plus.” Si quelqu’un demande “Quelle est votre politique de correspondance des prix ?”, nous voulons donner une réponse complète avec la politique. Pour y parvenir, nous devons parcourir les liens pour obtenir les informations réelles pour notre utilisateur final. Pendant le processus d’ingestion, nous pouvons utiliser notre chargeur web pour trouver les liens d’ancre vers d’autres pages HTML, puis les parcourir. Le changement de code suivant dans notre robot d’exploration web nous permet de trouver les liens dans les pages que nous parcourons. Il inclut également une logique supplémentaire pour éviter les parcours circulaires et permettre un filtrage par préfixe.
import logging
import requests
import html2text
from llama_index.readers.schema.base import Document
from typing import List
import re
def find_http_urls_in_parentheses(s: str, prefix: str = None):
pattern = r'\((https?://[^)]+)\)'
urls = re.findall(pattern, s)
matched = []
if prefix is not None:
for url in urls:
if str(url).startswith(prefix):
matched.append(url)
else:
matched = urls
return list(set(matched)) # supprimer les doublons en convertissant en ensemble, puis reconvertir en liste
class EZWebLoader:
def __init__(self, default_header: str = None):
self._html_to_text_parser = html2text
if default_header is None:
self._default_header = {"User-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36"}
else:
self._default_header = default_header
def load_data(self,
urls: List[str],
num_levels: int = 0,
level_prefix: str = None,
headers: str = None) -> List[Document]:
logging.info(f"Nombre d'URL : {len(urls)}.")
if headers is None:
headers = self._default_header
documents = []
visited = {}
for url in urls:
q = [url]
depth = num_levels
for page in q:
if page not in visited: # éviter les cycles en vérifiant si nous avons déjà parcouru un lien
logging.info(f"Exploration de {page}")
visited[page] = True # ajouter une entrée à visited pour éviter de repasser sur les pages
response = requests.get(page, headers=headers).text
response = self._html_to_text_parser.html2text(response) # réduire le HTML en texte
documents.append(Document(response))
if depth > 0:
# parcourir les pages liées
ingest_urls = find_http_urls_in_parentheses(response, level_prefix)
logging.info(f"Trouvé {len(ingest_urls)} pages à explorer.")
q.extend(ingest_urls)
depth -= 1 # réduire le compteur de profondeur pour ne pas aller plus de num_levels en profondeur dans notre parcours
else:
logging.info(f"Passage de {page} car il a déjà été exploré")
logging.info(f"Nombre de documents : {len(documents)}.")
return documents
url = "http://www.zappos.com/general-questions"
loader = EZWebLoader()
# parcourir le site avec une profondeur de 1 niveau et un préfixe "/c/" pour la racine du service client
documents = loader.load_data([url]
num_levels=1, level_prefix="https://www.zappos.com/c/")
index = GPTVectorStoreIndex.from_documents(documents)
Dans le code précédent, nous introduisons la possibilité de parcourir en profondeur N niveaux, et nous donnons un préfixe qui nous permet de limiter le parcours uniquement aux éléments qui commencent par un certain motif d’URL. Dans notre exemple Zappos, les pages de service client sont toutes basées sur zappos.com/c
, nous incluons donc cela comme préfixe pour limiter nos parcours à un sous-ensemble plus petit et plus pertinent. Le code montre comment nous pouvons ingérer jusqu’à deux niveaux de profondeur. La logique Lambda de notre bot reste la même car rien n’a changé, sauf que le crawler ingère plus de documents.
Nous avons maintenant tous les documents indexés et nous pouvons poser une question plus détaillée. Dans la capture d’écran suivante, notre bot donne la bonne réponse à la question “Avez-vous une politique de correspondance des prix ?”
Nous avons maintenant une réponse complète à notre question sur la correspondance des prix. Au lieu de simplement nous dire “Oui, consultez notre politique”, cela nous donne les détails de la recherche de deuxième niveau.
Nettoyage
Pour éviter de futurs frais, procédez à la suppression de toutes les ressources déployées dans le cadre de cet exercice. Nous avons fourni un script pour arrêter l’endpoint Sagemaker en toute sécurité. Les détails d’utilisation se trouvent dans le fichier README. De plus, pour supprimer toutes les autres ressources, vous pouvez exécuter la commande cdk destroy
dans le même répertoire que les autres commandes cdk afin de déprovisionner toutes les ressources de votre pile.
Conclusion
La possibilité d’ingérer un ensemble de FAQ dans un chatbot permet à vos clients de trouver les réponses à leurs questions grâce à des requêtes simples et naturelles. En combinant le support intégré d’Amazon Lex pour la gestion des fallback avec une solution RAG telle qu’un LlamaIndex, nous pouvons offrir un chemin rapide à nos clients pour obtenir des réponses satisfaisantes, validées et approuvées aux FAQ. En appliquant le parcours en N niveaux dans notre solution, nous pouvons permettre des réponses qui pourraient couvrir plusieurs liens de FAQ et fournir des réponses plus approfondies aux requêtes de nos clients. En suivant ces étapes, vous pouvez intégrer de manière transparente des capacités puissantes de questions-réponses basées sur LLM et une ingestion d’URL efficace dans votre chatbot Amazon Lex. Cela se traduit par des interactions plus précises, complètes et contextualisées avec les utilisateurs.
We will continue to update IPGirl; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles
- Google fait face à une poursuite alléguant une utilisation abusive des données pour former ses LLMs
- Des acteurs affirment que les studios veulent utiliser des répliques d’IA.
- Voici comment San Jose prévoit d’utiliser l’intelligence artificielle pour arrêter les décès de piétons dans la circulation
- Les meilleurs outils d’IA pour le podcasting (2023)
- ChatGPT et Tesla Full-Self-Driving ont le même problème
- Développer un chatbot personnalisé avec OpenAI
- Segmentez tout, mais plus rapidement ! Cette approche d’IA accélère le modèle SAM