#Vector Search

0 Abonnés · 22 Publications

La recherche vectorielle est une méthode utilisée dans la recherche d'informations et l'apprentissage automatique pour trouver des éléments similaires sur la base de leurs représentations mathématiques en tant que vecteurs. Dans cette approche, chaque élément est représenté sous la forme d'un vecteur à dimensions élevées, chaque dimension correspondant à un élément ou à une caractéristique de l'élément. Les algorithmes de recherche vectorielle comparent ensuite ces vecteurs pour trouver des éléments similaires, par exemple s'ils présentent des caractéristiques similaires ou s'ils sont proches les uns des autres dans l'espace vectoriel. Pour en savoir plus, cliquez ici.

Article Sylvain Guilbaud · Nov 6, 2025 5m read

Introduction

Dans mon article précédent, j'ai présenté le module IRIStool, qui intègre de manière transparente la bibliothèque pandas pour Python à la base de données IRIS. Je vais maintenant vous expliquer comment utiliser IRIStool pour exploiter InterSystems IRIS comme base pour une recherche sémantique intelligente dans les données de soins de santé au format FHIR.

Cet article décrit ce que j'ai fait pour créer une base de données pour mon autre projet, FHIR Data Explorer. Les deux projets sont candidats au concours InterSystems actuel, alors n'hésitez pas à voter pour eux si vous les trouvez utiles.

Ils sont disponibles sur Open Exchange:

Dans cet article, nous aborderons les sujets suivants:

  • Connexion à la base de données InterSystems IRIS via Python
  • Création d'un schéma de base de données compatible FHIR
  • Importation de données FHIR au moyen d'intégrations vectorielles pour la recherche sémantique

Conditions préalables

Installez IRIStool à partir de la page Github IRIStool et Data Manager.

1. Configuration de la connexion IRIS

Commencez par configurer votre connexion à l'aide des variables d'environnement dans un fichier .env:

IRIS_HOST=localhost
IRIS_PORT=9092
IRIS_NAMESPACE=USER
IRIS_USER=_SYSTEM
IRIS_PASSWORD=SYS

Connectez-vous à IRIS à l'aide du gestionnaire de contexte du module IRIStool:

from utils.iristool import IRIStool
import os
from dotenv import load_dotenv

load_dotenv()

with IRIStool( host=os.getenv('IRIS_HOST'), port=os.getenv('IRIS_PORT'), namespace=os.getenv('IRIS_NAMESPACE'), username=os.getenv('IRIS_USER'), password=os.getenv('IRIS_PASSWORD') ) as iris: # IRIStool manages the connection automatically pass

2. Création du schéma FHIR

Commencez par créer une table pour stocker les données FHIR, puis, tout en extrayant les données des paquets FHIR, créez des tables avec des capacités de recherche vectorielle pour chacune des ressources FHIR extraites (comme Patient, Osservability, etc.). 

Le module IRIStool simplifie la création de tables et d'index!

Table de référentiel FHIR

# Créer une table de référentiel principal pour les paquets FHIR brutsifnot iris.table_exists("FHIRrepository", "SQLUser"):
    iris.create_table(
        table_name="FHIRrepository",
        columns={
            "patient_id": "VARCHAR(200)",
            "fhir_bundle": "CLOB"
        }
    )
    iris.quick_create_index(
        table_name="FHIRrepository",
        column_name="patient_id"
    )

Table de patients avec support vectoriel

# Création d'une table de patients au moyen de la colonne vectorielle pour la recherche sémantiqueifnot iris.table_exists("Patient", "SQLUser"):
    iris.create_table(
        table_name="Patient",
        columns={
            "patient_row_id": "INT AUTO_INCREMENT PRIMARY KEY",
            "patient_id": "VARCHAR(200)",
            "description": "CLOB",
            "description_vector": "VECTOR(FLOAT, 384)",
            "full_name": "VARCHAR(200)",
            "gender": "VARCHAR(30)",
            "age": "INTEGER",
            "birthdate": "TIMESTAMP"
        }
    )
<span class="hljs-comment"># Création d'index standards</span>
iris.quick_create_index(table_name=<span class="hljs-string">"Patient"</span>, column_name=<span class="hljs-string">"patient_id"</span>)
iris.quick_create_index(table_name=<span class="hljs-string">"Patient"</span>, column_name=<span class="hljs-string">"age"</span>)

<span class="hljs-comment"># Création d'un index vectoriel HNSW pour la recherche par similarité</span>
iris.create_hnsw_index(
    index_name=<span class="hljs-string">"patient_vector_idx"</span>,
    table_name=<span class="hljs-string">"Patient"</span>,
    column_name=<span class="hljs-string">"description_vector"</span>,
    distance=<span class="hljs-string">"Cosine"</span>
)</code></pre>

3. Importation de données FHIR à l'aide de vecteurs

Générez facilement des intégrations vectorielles à partir des descriptions des patients FHIR et insérez-les dans IRIS:

from sentence_transformers import SentenceTransformer

# Initialisation du modèle de convertisseur model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')

# Exemple : Traitement des données du patient patient_description = "45-year-old male with hypertension and type 2 diabetes" patient_id = "patient-123"# Création d'un encodage vectoriel vector = model.encode(patient_description, normalize_embeddings=True).tolist()

# Insertion des données du patient à l'aide du vecteur iris.insert( table_name="Patient", patient_id=patient_id, description=patient_description, description_vector=str(vector), full_name="John Doe", gender="male", age=45, birthdate="1979-03-15" )

4. Recherche sémantique

Lorsque vos données sont téléchargées, vous pouvez effectuer des recherches par similarité:

# Requête de recherche
search_text = "patients with diabetes"
query_vector = model.encode(search_text, normalize_embeddings=True).tolist()

# définition de requête SQL query = f""" SELECT TOP 5 patient_id, full_name, description, VECTOR_COSINE(description_vector, TO_VECTOR(?)) as similarity FROM Patient ORDER BY similarity DESC """# définition des paramètres de requête parameters = [str(query_vector)]

# Recherche de patients similaires à l'aide de la recherche vectorielle results = iris.query(query, parameters)

# Impression des données du DataFrameifnot results.empty: print(f"{results['full_name']}: {results['similarity']:.3f}")

Conclusion

  • Le module IRIStool simplifie l'intégration d'IRIS avec des méthodes Python intuitives pour la création de tables et d'index
  • IRIS prend en charge le stockage hybride SQL + vecteur de manière native, ce qui permet de réaliser les deux requêtes traditionnelles et la recherche sémantique
  • Les intégrations vectorielles permettent une recherche intelligente dans les données de soins de santé FHIR à l'aide du langage naturel
  • Les index HNSW fournissent une recherche par similarité efficace à grande échelle

Cette approche prouve qu'InterSystems IRIS peut servir de base solide pour créer des applications de santé intelligentes avec des capacités de recherche sémantique sur les données FHIR.

0
0 8
Annonce Irène Mykhailova · Sept 29, 2025

Bonjour à la communauté,

Nous sommes ravis de vous présenter un tout nouveau tutoriel Instruqt :

🧑‍🏫 RAG avec InterSystems IRIS Vector Search

Ce laboratoire pratique vous guide dans la création d'un chatbot IA basé sur la génération augmentée de récupération (RAG) et optimisé par InterSystems IRIS Vector Search. Vous découvrirez comment la recherche vectorielle peut être exploitée pour fournir des réponses actualisées et précises, en combinant les atouts d'IRIS et de l'IA générative.

✨ Pourquoi l'essayer ?

0
0 21
Article Lorenzo Scalese · Juil 28, 2025 17m read

Découvrez comment concevoir des agents IA évolutifs et autonomes qui combinent raisonnement, recherche vectorielle et intégration d'outils à l'aide de LangGraph.

cover

C'est trop long, vous n'avez pas lu

  • Les agents IA sont des systèmes proactifs qui combinent mémoire, contexte et initiative pour automatiser des tâches dépassant le simple champ d'action des chatbots.
  • LangGraph est un framework qui nous permet de créer des workflows d'IA complexes, en utilisant des nœuds (tâches) et des arêtes (connexions) avec une gestion d'état intégrée.
  • Ce guide vous explique comment créer un agent de support client basé sur l'IA qui classe les priorités, identifie les sujets pertinents et détermine si une escalade ou une réponse automatique est nécessaire.

Alors, les agents IA, c'est quoi exactement?

Soyons réalistes, le terme "agents IA" peut faire penser à des robots qui vont envahir votre bureau. En réalité, il s'agit de vos assistants proactifs qui peuvent rationaliser des flux de travail complexes et éliminer les tâches répétitives. Considérez-les comme la prochaine étape évolutive après les chatbots: ils ne se contentent pas d'attendre des instructions, ils lancent des actions, coordonnent plusieurs étapes et s'adaptent tout au long du processus.

Autrefois, créer un système "intelligent" signifiait jongler avec différents modèles pour la compréhension du langage, la génération de code, la recherche de données, etc., puis les assembler à la va-vite. Auparavant, vous passiez la moitié de votre temps dans l'enfer de l'intégration, et l'autre moitié à corriger les erreurs.

Les agents renversent la tendance. Ils regroupent le contexte, l'initiative et l'adaptabilité en un seul flux orchestré. Il ne s'agit pas seulement d'automatisation, mais d'intelligence au service d'une mission. Et grâce à des frameworks tels que LangGraph, la création de votre propre équipe d'agents peut même devenir... oserais-je dire, amusante?

image

LangGraph, c'est quoi exactement?

LangGraph est un cadre innovant qui révolutionne la manière dont nous construisons des applications complexes impliquant des modèles linguistiques à grande échelle (LLM).

Imaginez que vous dirigez un orchestre: chaque instrument (ou "nœud") doit savoir quand entrer, à quel volume jouer et dans quel ordre. LangGraph, dans ce cas**,** est votre baguette, qui vous donne les informations suivantes:

  • Structure graphique: utilise une structure graphique avec des nœuds et des arêtes, permettant aux développeurs de concevoir des flux de travail flexibles et non linéaires qui s'adaptent aux branches et aux boucles. Elle reflète les processus décisionnels complexes qui ressemblent au fonctionnement des voies neuronales.
  • Gestion d'état: LangGraph offre des outils intégrés pour la persistance d'état et la récupération d'erreurs, simplifiant la gestion des données contextuelles à différentes étapes d'une application. Il peut basculer efficacement entre la mémoire à court terme et la mémoire à long terme, améliorant ainsi la qualité des interactions grâce à des outils tels que Zep.
  • Intégration d'outils: Avec LangGraph, les agents LLM peuvent facilement collaborer avec des services ou des bases de données externes pour récupérer des données du monde réel, améliorant ainsi la fonctionnalité et la réactivité de vos applications.
  • Human-in-the-Loop: Au-delà de l'automatisation, LangGraph s'adapte aux interventions humaines dans les flux de travail, qui sont essentielles pour les processus décisionnels nécessitant une supervision analytique ou une réflexion éthique.

Lorsque vous créez un chatbot doté d'une mémoire réelle, un moteur d'histoires interactives ou une équipe d'agents chargés de résoudre un problème complexe, LangGraph transforme les tâches fastidieuses en une machine à états simple et visuelle.

Pour commencer

Pour commencer à utiliser LangGraph, vous aurez besoin d'une configuration de base qui implique généralement l'installation de bibliothèques essentielles telles que langgraph et langchain-openai. Ensuite, vous pourrez définir les nœuds (tâches) et les bords (connexions) au sein du graphe, en mettant efficacement en œuvre des points de contrôle pour la mémoire à court terme et en utilisant Zep pour les besoins en mémoire plus persistants.

Lorsque vous utilisez LangGraph, gardez à l'esprit les remarques suivantes:

  • Flexibilité de conception: Tirez parti de la puissante structure graphique pour prendre en compte les ramifications et les interactions potentielles du flux de travail qui ne sont pas strictement linéaires.
  • Interaction prudente avec les outils: Améliorez les capacités du LLM à l'aide d'outils externes, mais ne les remplacez pas. Fournissez à chaque outil des descriptions complètes pour permettre une utilisation précise.
  • Utilisation de solutions de mémoire riches: Utilisez la mémoire de manière efficace, soyez attentif à la fenêtre contextuelle du LLM et envisagez d'intégrer des solutions externes pour la gestion automatiquedu contenu factuel.

Nous avons abordé les bases de LangGraph, passons maintenant à un exemple pratique. Pour cela, nous allons développer un agent IA spécialement conçu pour le support client.

Cet agent recevra les demandes par e-mail, analysera la description du problème dans le corps du message, puis déterminera la priorité de la demande et le sujet/la catégorie/le secteur approprié.

Alors, attachez vos ceintures et c'est parti!

buckle up

Pour commencer, nous devons définir ce qu'est un "outil". Vous pouvez le considérer comme un "assistant manager" spécialisé pour votre agent, lui permettant d'interagir avec des fonctionnalités externes.

Le décorateur @tool est ici essentiel. LangChain simplifie la création d'outils personnalisés, ce qui signifie que vous définissez d'abord une fonction Python, puis appliquez le décorateur @tool.

tools

Illustrons cela en créant notre premier outil. Cet outil aidera l'agent à classer la priorité d'un ticket d'assistance informatique en fonction du contenu de l'e-mail:

    from langchain_core.tools import tool
    
    @tool
    def classify_priority(email_body: str) -> str:
        """Classify the priority of an IT support ticket based on email content."""
        prompt = ChatPromptTemplate.from_template(
            """Analyze this IT support email and classify its priority as High, Medium, or Low.
            
            High: System outages, security breaches, critical business functions down
            Medium: Non-critical issues affecting productivity, software problems
            Low: General questions, requests, minor issues
            
            Email: {email}
            
            Respond with only: High, Medium, or Low"""
        )
        chain = prompt | llm
        response = chain.invoke({"email": email_body})
        return response.content.strip()

Excellent! Nous avons maintenant une invite qui demande à l'IA de recevoir le corps de l'e-mail, de l'analyser et de classer sa priorité comme Élevée, Moyenne ou Faible.

C'est tout! Vous venez de créer un outil accessible à votre agent!

Créons maintenant un outil similaire pour identifier le sujet principal (ou la catégorie) de la demande de support:


    @tool
    def identify_topic(email_body: str) -> str:
        """Identify the main topic/category of the IT support request."""
        prompt = ChatPromptTemplate.from_template(
            """Analyze this IT support email and identify the main topic category.
            
            Categories: password_reset, vpn, software_request, hardware, email, network, printer, other
            
            Email: {email}
            
            Respond with only the category name (lowercase with underscores)."""
        )
        chain = prompt | llm
        response = chain.invoke({"email": email_body})
        return response.content.strip()

Nous devons maintenant créer un état, et dans LangGraph, cette petite partie est plutôt importante.

Considérez-le comme le système nerveux central de votre graphe. C'est ainsi que les nœuds communiquent entre eux, en se passant des petits mots comme des surdoués à l'école.

Selon la documentation:

“Un état est une structure de données partagée qui représente l'instantané actuel de votre application.”

Et en pratique? L'état est un message structuré qui circule entre les nœuds. Il transporte le résultat d'une étape comme entrée pour la suivante. En gros, c'est le ciment qui maintient l'ensemble de votre flux de travail.

Par conséquent, avant de construire le graphique, nous devons d'abord définir la structure de notre état. Dans cet exemple, notre état sera composé des éléments suivants:

  • La demande de l'utilisateur (corps de l'email)
  • La priorité attribuée
  • Le sujet identifié (catégorie)

C'est simple et clair, vous pouvez donc vous déplacer à travers le graphe comme un pro.

    from typing import TypedDict

    # Définition de la structure d'état
    class TicketState(TypedDict):
        email_body: str
        priority: str
        topic: str
        
    
    # Initialisation d'état
    initial_state = TicketState(
        email_body=email_body,
        priority="",
        topic=""
    )

Nœuds et bords: Composants clés de LangGraph

Les éléments fondamentaux de LangGraph sont les nœuds et les bords.

  • Nœuds: Il s'agit des unités opérationnelles du graphe qui effectuent le travail proprement dit. Un nœud se compose généralement d'un code Python capable d'exécuter n'importe quelle logique, qu'il s'agisse de calculs ou d'interactions avec des modèles linguistiques (LLM) ou des intégrations externes. Les nœuds sont essentiellement similaires aux fonctions ou agents individuels de la programmation traditionnelle.
  • Bords: les bords définissent le flux d'exécution entre les nœuds et déterminent ce qui se passe ensuite. Elles agissent comme des connecteurs qui permettent à l'état de passer d'un nœud à l'autre en fonction de conditions prédéfinies. Dans le contexte de LangGraph, les bords sont essentiels pour orchestrer la séquence et le flux de décision entre les nœuds.

Pour comprendre le fonctionnement des bords, prenons l'exemple simple d'une application de messagerie:

  • Nœuds sont similaires aux utilisateurs (ou à leurs appareils) qui participent activement à une conversation.
  • Bords symbolisent les fils de discussion ou les connexions entre les utilisateurs qui facilitent la communication.

Lorsqu'un utilisateur sélectionne un fil de discussion pour envoyer un message, un bord est créée, le reliant à un autre utilisateur. Chaque interaction, qu'il s'agisse d'envoyer un message textuel, vocal ou vidéo, suit une séquence prédéfinie, comparable au schéma structuré de l'état de LangGraph. Cela garantit l'uniformité et l'interprétabilité des données transmises le long des bords.

Contrairement à la nature dynamique des applications pilotées par les événements, LangGraph utilise un schéma statique qui reste cohérent tout au long de l'exécution. Il simplifie la communication entre les nœuds, permettant aux développeurs de s'appuyer sur un format stable, garantissant ainsi une communication fluide au niveau des bords.

Conception d'un flux de travail de base

L'ingénierie des flux dans LangGraph peut être conceptualisée comme la conception d'une machine d'état. Dans ce paradigme, chaque nœud représente un état ou une étape de traitement distinct, tandis que les arêtes définissent les transitions entre ces états. Cette approche est particulièrement avantageuse pour les développeurs qui cherchent à trouver un équilibre entre les séquences de tâches déterministes et les capacités décisionnelles dynamiques de l'IA. Commençons à construire notre flux en initialisant StateGraph avec la classe TicketState que nous avons définie précédemment.

    from langgraph.graph import StateGraph, START, END
    
    workflow = StateGraph(TicketState)

Ajout de nœuds: Les nœuds sont des éléments fondamentaux, définis pour exécuter des tâches spécifiques telles que la classification de la priorité d'un ticket ou l'identification de son sujet.

Chaque fonction de nœud reçoit l'état actuel, effectue son opération et renvoie un dictionnaire permettant de mettre à jour l'état:

   def classify_priority_node(state: TicketState) -> TicketState:
        """Node to classify ticket priority."""
        priority = classify_priority.invoke({"email_body": state["email_body"]})
        return {"priority": priority}

    def identify_topic_node(state: TicketState) -> TicketState:
        """Node to identify ticket topic."""
        topic = identify_topic.invoke({"email_body": state["email_body"]})
        return {"topic": topic}
        
        
    workflow.add_node("classify_priority", classify_priority_node)
    workflow.add_node("identify_topic", identify_topic_node)

Les méthodes classify_priority_node et identify_topic_node modifieront le TicketState et enverront la saisie du paramètre.

Création des bords: Définissez les bords pour connecter les nœuds:


    workflow.add_edge(START, "classify_priority")
    workflow.add_edge("classify_priority", "identify_topic")
    workflow.add_edge("identify_topic", END)

The classify_priority establishes the start, whereas the identify_topic determines the end of our workflow so far.

Compilation et exécution: Une fois les nœuds et les bords configurés, compilez le flux de travail et exécutez-le.


    graph = workflow.compile()
    result = graph.invoke(initial_state)

Très bien! Vous pouvez également générer une représentation visuelle de notre flux LangGraph.

graph.get_graph().draw_mermaid_png(output_file_path="graph.png")

Si vous exécutez le code jusqu'à ce point, vous obtiendrez un graphe similaire au suivant:

first_graph.png

Cette illustration visualise une exécution séquentielle: démarrage, suivi du classement des priorités, puis identification du sujet et enfin terminaison.

L'un des aspects les plus puissants de LangGraph est sa flexibilité, qui nous permet de créer des flux et des applications plus complexes. Par exemple, nous pouvons modifier le flux de travail pour ajouter des bords depuis START vers les deux nœuds avec la ligne suivante:

    workflow.add_edge(START, "classify_priority")
    workflow.add_edge(START, "identify_topic")

Cette modification aura pour conséquence que l'agent exécutera simultanément classify_priority et identify_topic.

Une autre fonctionnalité très utile de LangGraph est la possibilité d'utiliser des bords conditionnels. Ils permettent au flux de travail de se ramifier en fonction de l'évaluation de l'état actuel, ce qui permet un routage dynamique des tâches.

Améliorons notre flux de travail. Nous allons créer un nouvel outil qui analyse le contenu, la priorité et le sujet de la demande afin de déterminer s'il s'agit d'un problème hautement prioritaire nécessitant une escalade (c'est-à-dire l'ouverture d'un ticket pour être traité par une équipe humaine). Si ce n'est pas le cas, une réponse automatisée sera générée pour l'utilisateur.


    @tool
    def make_escalation_decision(email_body: str, priority: str, topic: str) -> str:
        """Decide whether to auto-respond or escalate to IT team."""
        prompt = ChatPromptTemplate.from_template(
            """Based on this IT support ticket, decide whether to:
            - "auto_respond": Send an automated response for simple/common or medium priority issues
            - "escalate": Escalate to the IT team for complex/urgent issues
            
            Email: {email}
            Priority: {priority}
            Topic: {topic}
            
            Consider: High priority items usually require escalation, while complex technical issues necessitate human review.
            
            Respond with only: auto_respond or escalate"""
        )
        chain = prompt | llm
        response = chain.invoke({
            "email": email_body,
            "priority": priority,
            "topic": topic
        })
        return response.content.strip()
        

De plus, si la demande est jugée de priorité faible ou moyenne (ce qui entraîne une décision "auto_respond"), nous effectuerons une recherche vectorielle pour récupérer les réponses historiques. Ces informations seront ensuite utilisées pour générer une réponse automatisée appropriée. Cependant, cela nécessitera deux outils supplémentaires:


    @tool
    def retrieve_examples(email_body: str) -> str:
        """Retrieve relevant examples from past responses based on email_body."""
        try:
            examples = iris.cls(__name__).Retrieve(email_body)
            return examples if examples else "No relevant examples found."
        except:
            return "No relevant examples found."

    @tool
    def generate_reply(email_body: str, topic: str, examples: str) -> str:
        """Generate a suggested reply based on the email, topic, and RAG examples."""
        prompt = ChatPromptTemplate.from_template(
            """Generate a professional IT support response based on:
            
            Original Email: {email}
            Topic Category: {topic}
            Example Response: {examples}
            
            Create a helpful, professional response that addresses the user's concern.
            Keep it concise and actionable."""
        )
        chain = prompt | llm
        response = chain.invoke({
            "email": email_body,
            "topic": topic,
            "examples": examples
        })
        return response.content.strip()

Maintenant, définissons les nœuds correspondants à ces nouveaux outils:

    
    def decision_node(state: TicketState) -> TicketState:
        """Node to decide on escalation or auto-response."""
        decision = make_escalation_decision.invoke({
            "email_body": state["email_body"],
            "priority": state["priority"],
            "topic": state["topic"]
        })
        return {"decision": decision}
        
    
    def rag_node(state: TicketState) -> TicketState:
        """Node to retrieve relevant examples using RAG."""
        examples = retrieve_examples.invoke({"email_body": state["email_body"]})
        return {"rag_examples": examples}

    def generate_reply_node(state: TicketState) -> TicketState:
        """Node to generate suggested reply."""
        reply = generate_reply.invoke({
            "email_body": state["email_body"],
            "topic": state["topic"],
            "examples": state["rag_examples"]
        })
        return {"suggested_reply": reply}
        
    
    def execute_action_node(state: TicketState) -> TicketState:
        """Node to execute final action based on decision."""
        if state["decision"] == "escalate":
            action = f"&#x1f6a8; ESCALATED TO IT TEAM\nPriority: {state['priority']}\nTopic: {state['topic']}\nTicket created in system."
            print(f"[SYSTEM] Escalating ticket to IT team - Priority: {state['priority']}, Topic: {state['topic']}")
        else:
            action = f"&#x2705; AUTO-RESPONSE SENT\nReply: {state['suggested_reply']}\nTicket logged for tracking."
            print(f"[SYSTEM] Auto-response sent to user - Topic: {state['topic']}")
        
        return {"final_action": action}
        
        
        
    workflow.add_node("make_decision", decision_node)
    workflow.add_node("rag", rag_node)
    workflow.add_node("generate_reply", generate_reply_node)
    workflow.add_node("execute_action", execute_action_node)

Le bord conditionnel utilisera alors le résultat du nœud make_decision pour diriger le flux:

    workflow.add_conditional_edges(
        "make_decision",
        lambda x: x.get("decision"),
        {
            "auto_respond": "rag",
            "escalate": "execute_action"
        }
    )

Si l'outil make_escalation_decision (via decision_node) renvoie "auto_respond", le workflow passe par le nœud rag (pour récupérer des exemples), puis par generate_reply (pour rédiger la réponse) et enfin par execute_action (pour enregistrer la réponse automatique).

En revanche, si la décision est “escalate”, le flux contournera le RAG et passera aux étapes de génération, passant directement à execute_action pour gérer l'escalade. Pour compléter le graphe en ajoutant les bords standard restants, procédez comme suit:

    workflow.add_edge("rag", "generate_reply")
    workflow.add_edge("generate_reply", "execute_action")
    workflow.add_edge("execute_action", END)

Remarque sur le jeu de données: pour ce projet, le jeu de données utilisé pour alimenter la génération augmentée par récupération (RAG) provient du jeu de données Customer Support Tickets dataset on Hugging Face. Le jeu de données a été filtré afin de n'inclure que les éléments classés dans la catégorie de support technique 'Technical Support' et limité aux saisies en anglais English. Cela a permis de garantir que le système RAG ne récupère que des exemples très pertinents et spécifiques au domaine pour les tâches de support technique.

À ce stade, notre graphique devrait ressembler au suivant:

graph.png

Lorsque vous exécutez ce graphe avec un e-mail qui entraîne une classification de priorité élevée et une décision "escalate", vous obtenez la réponse suivante:

image.png

Au même moment, une demande classée comme faible priorité et donnant lieu à une décision « auto_respond » déclenchera une réponse similaire à la suivante:

image.png

Alors... Est-ce que tout est rose?

Pas tout à fait. Il y a quelques obstacles à éviter:

  • Confidentialité des données: Soyez prudent avec les informations sensibles, ces agents nécessitent des mesures de protection.
  • Coûts informatiques: Certaines configurations avancées nécessitent des ressources importantes.
  • Hallucinations: Les LLM peuvent parfois inventer des choses (même s'ils restent plus intelligents que la plupart des stagiaires).
  • Non-déterminisme: Une même saisie peut donner des résultats différents, ce qui est excellent pour la créativité, mais problématique pour les processus rigoureux.

Cependant, la plupart de ces lacunes peuvent être comblées grâce à une bonne planification, aux bons outils et, bien sûr, à un peu de réflexion.

LangGraph transforme les agents IA, qui ne sont encore que des mots à la mode, en solutions fonctionnelles et tangibles. Que vous souhaitiez automatiser votre service client, traiter vos tickets informatiques ou créer des applications autonomes, ce framework rend tout cela possible et même agréable.

Avez-vous des questions ou des commentaires? Parlons-en. La révolution de l'IA a besoin de créateurs comme vous.

0
0 27
Article Sylvain Guilbaud · Juin 13, 2025 9m read

J'ai dû récemment rafraîchir mes connaissances sur le module EMPI de HealthShare et, comme je travaillais depuis quelque temps sur les fonctionnalités de stockage et de recherche vectorielle d'IRIS, j'ai tout naturellement fait le rapprochement.

Pour ceux d'entre vous qui ne connaissent pas bien les fonctionnalités EMPI, voici une brève introduction:

Index patient principal "Enterprise Master Patient Index"

En général, tous les EMPI fonctionnent de manière très similaire : ils ingèrent les informations, les normalisent et les comparent avec les données déjà présentes dans leur système. Dans le cas de l'EMPI de HealthShare, ce processus est connu sous le nom de NICE:

  • Normalisation: tous les textes ingérés à partir de la production interopérable sont normalisés en supprimant les caractères spéciaux.
  • Indexation: des index sont générés à partir d'une sélection de données démographiques afin d'accélérer la recherche de correspondances.
  • Comparaison: les correspondances trouvées dans les index sont comparées entre les données démographiques, puis des pondérations sont attribuées en fonction de critères correspondant au niveau de coïncidence.
  • Évaluation: la possibilité de relier les patients est évaluée à partir de la somme des pondérations obtenues.

Si vous voulez en savoir plus sur HealthShare Patient Index, vous pouvez consulter une série d'articles que j'ai écrits il y a quelque temps ici.

Quel est le défi?

Bien que le processus de configuration nécessaire pour obtenir les liens possibles ne soit pas extrêmement compliqué, je me suis demandé... Serait-il possible d'obtenir des résultats similaires en utilisant les fonctionnalités de stockage et de recherche vectorielle et en supprimant les étapes d'ajustement de la pondération? Au travail!

De quoi ai-je besoin pour mettre en œuvre mon idée?

J'aurai besoin des ingrédients suivants:

  • IRIS for Health pour mettre en œuvre la fonctionnalité et utiliser le moteur d'interopérabilité pour l'ingestion de messages HL7.
  • Bibliothèque Python pour générer les vecteurs à partir des données démographiques, dans ce cas, il s'agira de sentence-transformers.
  • Modèle pour générer les embeddings, après une recherche rapide sur Hugging Faces, j'ai choisi all-MiniLM-L6-v2.

Production d'interopérabilité

La première étape consistera à configurer la production chargée d'ingérer les messages HL7, de les transformer en messages contenant les données démographiques les plus pertinentes du patient, de générer les intégrations à partir de ces données démographiques et enfin de générer le message de réponse avec les correspondances possibles.

Jetons un coup d'œil à notre production:

Comme vous pouvez le voir, rien de plus simple. J'ai un Service métier HL7FileService qui récupère les messages HL7 à partir d'un répertoire (/shared/in), puis envoie le message au Processus métier (BP) EMPI.BP.FromHL7ToPatientRequestBPL où je vais créer le message avec les données démographiques du patient, puis nous l'envoyons à un autre BP appelé EMPI.BP.VectorizationBP où les informations démographiques seront vectorisées et où la recherche vectorielle sera effectuée, ce qui renverra un message avec tous les patients en double possibles.

Comme vous pouvez le voir, le BP FromHL7ToPatientRequesBPL est très simple:

Nous convertissons le message HL7 en un message que nous avons créé afin de stocker les données démographiques que nous avons jugées les plus pertinentes.

Messages entre les composants

Nous avons créé deux types de messages spécifiques: 

EMPI.Message.PatientRequest

Class EMPI.Message.PatientRequest Extends (Ens.Request, %XML.Adaptor)
{

Property Patient As EMPI.Object.Patient; }

EMPI.Message.PatientResponse

Class EMPI.Message.PatientResponse Extends (Ens.Response, %XML.Adaptor)
{

Property Patients As list Of EMPI.Object.Patient; }

Ce type de message contiendra une liste des patients susceptibles d'être en double.

Voyons la définition de la classe EMPI.Object.Patient :

Class EMPI.Object.Patient Extends (%SerialObject, %XML.Adaptor)
{

Property Name As%String(MAXLEN = 1000);Property Address As%String(MAXLEN = 1000);Property Contact As%String(MAXLEN = 1000);Property BirthDateAndSex As%String(MAXLEN = 100);Property SimilarityName As%Double;Property SimilarityAddress As%Double;Property SimilarityContact As%Double;Property SimilarityBirthDateAndSex As%Double; }

Nom, Address, Contact et BirthDateAndSex sont les propriétés dans lesquelles nous allons enregistrer les données démographiques les plus pertinentes sur les patients.

Voyons maintenant la magie: la génération d'intégration et la recherche vectorielle dans la production.

EMPI.BP.VectorizationBP

Intégration et recherche vectorielle

Une fois la requête PatientRequest reçue, nous allons générer les intégrations à l'aide d'une méthode Python:

Method VectorizePatient(name As%String, address As%String, contact As%String, birthDateAndSex As%String) As%String [ Language = python ]
{
    import iris
    import os
    import sentence_transformers
<span class="hljs-keyword">try</span> :
    <span class="hljs-keyword">if</span> not os.path.isdir(<span class="hljs-string">"/iris-shared/model/"</span>):
        model = sentence_transformers.SentenceTransformer(<span class="hljs-string">"sentence-transformers/all-MiniLM-L6-v2"</span>)            
        model.save('/iris-shared/model/')
    model = sentence_transformers.SentenceTransformer(<span class="hljs-string">"/iris-shared/model/"</span>)
    embeddingName = model.encode(name, normalize_embeddings=True).tolist()
    embeddingAddress = model.encode(address, normalize_embeddings=True).tolist()
    embeddingContact = model.encode(contact, normalize_embeddings=True).tolist()
    embeddingBirthDateAndSex = model.encode(birthDateAndSex, normalize_embeddings=True).tolist()

    stmt = iris.sql.prepare(<span class="hljs-string">"INSERT INTO EMPI_Object.PatientInfo (Name, Address, Contact, BirthDateAndSex, VectorizedName, VectorizedAddress, VectorizedContact, VectorizedBirthDateAndSex) VALUES (?,?,?,?, TO_VECTOR(?,DECIMAL), TO_VECTOR(?,DECIMAL), TO_VECTOR(?,DECIMAL), TO_VECTOR(?,DECIMAL))"</span>)
    rs = stmt.execute(name, address, contact, birthDateAndSex, str(embeddingName), str(embeddingAddress), str(embeddingContact), str(embeddingBirthDateAndSex))
    <span class="hljs-keyword">return</span> <span class="hljs-string">"1"</span>
except Exception <span class="hljs-keyword">as</span> err:
    iris.cls(<span class="hljs-string">"Ens.Util.Log"</span>).LogInfo(<span class="hljs-string">"EMPI.BP.VectorizationBP"</span>, <span class="hljs-string">"VectorizePatient"</span>, repr(err))
    <span class="hljs-keyword">return</span> <span class="hljs-string">"0"</span>

}

Analysons le code:

  • Avec la bibliothèque sentence-transformer, nous obtenons le modèle all-MiniLM-L6-v2 et l'enregistrons sur la machine locale (pour éviter toute connexion supplémentaire à l'Internet).
  • Une fois le modèle importé, nous pouvons générer les intégrations pour les champs démographiques à l'aide de la méthode encode.
  • À l'aide de la bibliothèque IRIS, nous exécutons la requête d'insertion pour conserver les intégrations pour le patient.

Recherche de patients en double

Nous avons maintenant la liste des patients enregistrés avec les intégrations générées à partir des données démographiques, interrogeons-la!

Method OnRequest(pInput As EMPI.Message.PatientRequest, Output pOutput As EMPI.Message.PatientResponse) As%Status
{
    try{
        set result = ..VectorizePatient(pInput.Patient.Name, pInput.Patient.Address, pInput.Patient.Contact, pInput.Patient.BirthDateAndSex)
        set pOutput = ##class(EMPI.Message.PatientResponse).%New()
        if (result = 1)
        {
            set sql = "SELECT * FROM (SELECT p1.Name, p1.Address, p1.Contact, p1.BirthDateAndSex, VECTOR_DOT_PRODUCT(p1.VectorizedName, p2.VectorizedName) as SimilarityName, VECTOR_DOT_PRODUCT(p1.VectorizedAddress, p2.VectorizedAddress) as SimilarityAddress, "_
                    "VECTOR_DOT_PRODUCT(p1.VectorizedContact, p2.VectorizedContact) as SimilarityContact, VECTOR_DOT_PRODUCT(p1.VectorizedBirthDateAndSex, p2.VectorizedBirthDateAndSex) as SimilarityBirthDateAndSex "_
                    "FROM EMPI_Object.PatientInfo p1, EMPI_Object.PatientInfo p2 WHERE p2.Name = ? AND p2.Address = ?  AND p2.Contact = ? AND p2.BirthDateAndSex = ?) "_
                    "WHERE SimilarityName > 0.8 AND SimilarityAddress > 0.8 AND SimilarityContact > 0.8 AND SimilarityBirthDateAndSex > 0.8"set statement = ##class(%SQL.Statement).%New(), statement.%ObjectSelectMode = 1set status = statement.%Prepare(sql)
            if ($$$ISOK(status)) {
                set resultSet = statement.%Execute(pInput.Patient.Name, pInput.Patient.Address, pInput.Patient.Contact, pInput.Patient.BirthDateAndSex)
                if (resultSet.%SQLCODE = 0) {
                    while (resultSet.%Next() '= 0) {
                        set patient = ##class(EMPI.Object.Patient).%New()
                        set patient.Name = resultSet.%GetData(1)
                        set patient.Address = resultSet.%GetData(2)
                        set patient.Contact = resultSet.%GetData(3)
                        set patient.BirthDateAndSex = resultSet.%GetData(4)
                        set patient.SimilarityName = resultSet.%GetData(5)
                        set patient.SimilarityAddress = resultSet.%GetData(6)
                        set patient.SimilarityContact = resultSet.%GetData(7)
                        set patient.SimilarityBirthDateAndSex = resultSet.%GetData(8)
                        do pOutput.Patients.Insert(patient)
                    }
                }
            }
        }
    }
    catch ex {
        do ex.Log()
    }
    return$$$OK
}

Voici la requête. Pour notre exemple, nous avons inclus une restriction afin d'obtenir les patients présentant une similitude supérieure à 0,8 pour toutes les données démographiques, mais nous pourrions la configurer pour affiner la requête.

Voyons l'exemple présentant le fichier messagesa28.hl7 avec des messages HL7 tels que ceux-ci:

MSH|^~\&|HIS|HULP|EMPI||20241120103314||ADT^A28|269304|P|2.5.1
EVN|A28|20241120103314|20241120103314|1
PID|||1220395631^^^SERMAS^SN~402413^^^HULP^PI||FERNÁNDEZ LÓPEZ^JOSÉ MARÍA^^^||19700611|M|||PASEO JUAN FERNÁNDEZ^1832 A^LEGANÉS^CÁDIZ^28566^SPAIN||555749170^PRN^^JOSE-MARIA.FERNANDEZ@GMAIL.COM|||||||||||||||||N|
PV1||N

MSH|^&amp;|HIS|HULP|EMPI||20241120103314||ADT^A28|570814|P|2.5.1 EVN|A28|20241120103314|20241120103314|1 PID|||1122730333^^^SERMAS^SN018565^^^HULP^PI||GONZÁLEZ GARCÍA^MARÍA^^^||19660812|F|||CALLE JOSÉ MARÍA FERNÁNDEZ^2818 IZQUIERDA^MADRID^BARCELONA^28057^SPAIN||555386663^PRN^^MARIA.GONZALEZ@GMAIL.COM|||||||||||||||||N| PV1||N DG1|1||T001^TRAUMATISMOS SUPERF AFECTAN TORAX CON ABDOMEN, REG LUMBOSACRA Y PELVIS^||20241120|||||||||||^CONTRERAS ÁLVAREZ^ENRIQUETA^^^Dr|

MSH|^&amp;|HIS|HULP|EMPI||20241120103314||ADT^A28|40613|P|2.5.1 EVN|A28|20241120103314|20241120103314|1 PID|||1007179467^^^SERMAS^SN122688^^^HULP^PI||OLIVA OLIVA^JIMENA^^^||19620222|F|||CALLE ANTONIO ÁLVAREZ^513 D^MÉRIDA^MADRID^28253^SPAIN||555638305^PRN^^JIMENA.OLIVA@VODAFONE.COM|||||||||||||||||N| PV1||N DG1|1||Q059^ESPINA BIFIDA, NO ESPECIFICADA^||20241120|||||||||||^SANZ LÓPEZ^MARIO^^^Dr|

MSH|^&amp;|HIS|HULP|EMPI||20241120103314||ADT^A28|61768|P|2.5.1 EVN|A28|20241120103314|20241120103314|1 PID|||1498973060^^^SERMAS^SN719939^^^HULP^PI||PÉREZ CABEZUELA^DIANA^^^||19820309|F|||AVENIDA JULIA ÁLVAREZ^2531 A^PERELLONET^BADAJOZ^28872^SPAIN||555705148^PRN^^DIANA.PEREZ@YAHOO.COM|||||||||||||||||N| PV1||N AL1|1|MA|^Polen de gramineas^|SV^^||20340919051523 MSH|^&amp;|HIS|HULP|EMPI||20241120103314||ADT^A28|128316|P|2.5.1 EVN|A28|20241120103314|20241120103314|1 PID|||1632386689^^^SERMAS^SN601379^^^HULP^PI||GARCÍA GARCÍA^MARIO^^^||19550603|M|||PASEO JOSÉ MARÍA TREVIÑO^1533 D^JEREZ DE LA FRONTERA^MADRID^28533^SPAIN||555231628^PRN^^MARIO.GARCIA@GMAIL.COM|||||||||||||||||N| PV1||N

Dans ce fichier, tous les patients sont différents, donc le résultat de l'opération sera du type suivant:

La seule correspondance est le patient lui-même. Introisons les messages HL7 provenant du fichier messagesa28Duplicated.hl7 avec des patients en double:

Comme vous pouvez le voir, le code a détecté le patient en double avec des différences mineures dans le nom (Maruchi est un diminutif affectueux de María et Mª est l'abréviation), bien sûr, ce cas est simplifié à l'extrême, mais vous pouvez vous faire une idée des capacités de la recherche vectorielle pour trouver des données en double, non seulement pour les patients, mais aussi pour tout autre type d'informations.

Prochaines étapes...

Pour cet exemple, j'ai utilisé un modèle commun pour générer les intégrations, mais le comportement du code pourrait être amélioré à l'aide d'un réglage fin utilisant des diminutifs, des surnoms, etc.

Merci de votre attention!

0
0 27
Article Lorenzo Scalese · Mai 28, 2025 6m read

Cet article présente une analyse du cycle de solution pour l'application Open Exchange TOOT ( application Open Exchange)

L'hypothèse

Un bouton sur une page Web permet de capturer la voix de l'utilisateur. L'intégration IRIS permet de manipuler les enregistrements afin d' extraire la signification sémantique que la recherche vectorielle d'IRIS peut ensuite proposer pour de nouveaux types de solutions d'IA.

La signification sémantique amusante choisie concernait la recherche vectorielle musicale, afin d'acquérir de nouvelles compétences et connaissances en cours de route.

À la recherche de motifs simples

La voix humaine qui parle, siffle ou fredonne a des contraintes qui se sont exprimées à travers de nombreux encodages de données historiques à faible bande passante et de supports d'enregistrement.

Quelle quantité minimale d'informations est nécessaire pour distinguer un son musical d'un autre en relation avec une entrée vocale musicale?

Considérez la séquence des lettres de l'alphabet musical:

   A, B, C, E, A

Like in programming with ASCII values of the characters:
* B est supérieur d'une unité à A
* C est supérieur d'une unité à B
* E est supérieur de deux unités à C
* A est inférieur de quatre unités à E
Représenté sous forme de progression numérique entre deux nombres:

 +1, +1, +2, -4

On a besoin d'une information de moins que le nombre initial de notes.
Si l'on teste logiquement après le premier café du matin, le fredonnement devient:

  B, C, D, F, B

Notez que la séquence numérique correspond toujours à cette hauteur élevée.
Cela démontre qu'une progression numérique de la différence de hauteur semble plus flexible pour correspondre à l'entrée utilisateur que des notes réelles.

Durée des notes

Le sifflement a une résolution inférieure à celle représentée par les manuscrits musicaux traditionnels.
Une décision a été prise de ne résoudre que DEUX types de durée de note musicale:

  • Une note longue qui dure 0,7 seconde ou plus
  • Une note courte qui dure moins de 0,7 seconde.

Vides entre les notes

Les vides ont une résolution assez faible, nous n'utilisons donc qu'un seul point d'information.
Il a été décidé de définir un vide entre deux notes comme suit:

une pause de plus d'une demi-seconde entre deux notes distinctes.

Plage de changement des notes

Une décision a été prise afin de limiter la transition maximale enregistrable entre deux notes de 45 hauteurs différentes.
Tout changement de note saisi par l'utilisateur qui dépasse cette limite est tronqué à +45 ou -45, selon que la hauteur augmente ou diminue. Contrairement aux notes réelles, cela affecte en rien l'utilité d'une séquence de recherche de mélodie continue.

   +1 0 +2 -4 +2 +1 -3

Possibilité d'entraînement pour obtenir une correspondance sémantique proche de la séquence de changements indiquée en rouge.

+1 1 +2 -4 +2 +1 -3

Saisie monocanal

Une voix ou un sifflement est un instrument simple qui ne produit qu'une seule note à la fois.
Cependant, cela doit correspondre à une musique composée généralement de:
* Plusieurs instruments qui jouent simultanément
* Des instruments qui jouent des accords (plusieurs notes à la fois)
En général, un sifflement tend à suivre UNE voix ou un instrument spécifique pour représenter une musique de référence.
Lors d'enregistrements physiques, les voix et les instruments individuels peuvent être pistes distinctes qui sont ensuite "mixés" ensemble.
De même, les utilitaires d'encodage/décodage et les formats de données peuvent également préserver et utiliser des "pistes" par voix/instrument.

Il en résulte que les entrées fredonnées/sifflées doivent être recherchées en fonction de l'impression laissée par chaque piste vocale et instrumentale.

Plusieurs modèles d'encodage "musicaux" / formats linguistiques ont été envisagés.
L'option la plus simple et la plus aboutie consistait à utiliser le traitement au format MIDI pour déterminer les encodages de référence pour la correspondance et la recherche de sifflements.

Récapitulatif du vocabulaire

Outre les séquences habituelles de symboles de début, de fin et de remplissage, les points d'information sont les suivants

  • 45 notes longues dont la durée diminue selon une amplitude spécifique
  • 45 notes courtes dont la durée diminue selon une amplitude spécifique
  • Répétition de la même note d'une durée courte
  • Répétition de la même note d'une durée longue
  • 45 notes longues dont la durée augmente selon une amplitude spécifique
  • 45 notes courtes dont la durée augmente selon une amplitude spécifique
  • Un vide entre les notes

Données synthétiques

Les globales IRIS sont très rapides et efficaces pour identifier les combinaisons et leur fréquence dans un grand ensemble de données.

Le point de départ des données synthétiques était des séquences valides.

Celles-ci ont été modifiées de différentes manières et classées en fonction de leur écart:

  • SplitLongNote - Une note longue est divisée en deux notes courtes, dont la seconde est une répétition
  • JoinLongNote - Deux notes courtes, dont la seconde est une répétition, sont fusionnées en une seule note longue.
  • VaryOneNote ( +2, +1, -1 or -2 )
  • DropSpace - Suppression de l'espace entre les notes
  • AddSpace - Ajout d'un espace entre les notes

Ensuite, ces scores sont superposés de manière efficace dans une globale pour chaque résultat.

Cela signifie que lorsqu'une autre zone de séquence de changement de note dans un flux de piste est plus proche d'une valeur mutée, le score le plus élevé est toujours sélectionné.

Flux de travail Dev

Flux de travail de recherche

Flux de travail DataLoad

Des intégrations vectorielles ont été générées pour plusieurs pistes d'instruments (22 935 enregistrements) et pour des échantillons mélodiques (6 762 enregistrements)

Flux de travail d'entraînement

Deux essais d'entraînement:

Sans supervision - 110 000 enregistrements maximum (7 heures de traitement)

Score de similarité sous supervision - 960 000 enregistrements maximum (1,3 jour de traitement)

Autres exigences à explorer

Meilleure mesure de la similarité

La version actuelle de l'implémentation trie les résultats en fonction des scores de manière trop stricte.

Il convient de réexaminer le seuil d'inclusion des séquences à faible et à forte occurrence dans l'ensemble de données.

Filtrage du bruit de fond

Au format MIDI, le volume des voix ou des instruments est important en termes d'utilisation réelle et de bruit de fond. Cela pourrait permettre de nettoyer/filtrer une source de données. On pourrait peut-être exclure les pistes qui ne seraient jamais référencées par une entrée humaine. Actuellement, la solution exclut les « percussions » par piste instrumentale, par titre et par analyse de certaines répétitions de progression.

Instruments MIDI synthétiques

L'approche actuelle pour faire correspondre la voix aux instruments consistait à essayer de contourner les incompatibilités entre les caractéristiques des instruments, afin de voir si cela donnait un résultat satisfaisant.

Une expérience intéressante serait d'ajouter certaines caractéristiques à partir des données fournies par l'utilisateur, tout en modifiant les données d'apprentissage synthétiques afin d'obtenir des caractéristiques plus humaines.

Le MIDI encode des informations supplémentaires avec des variations de hauteur afin d'obtenir une progression plus fluide entre les notes.

Ce serait une piste à explorer pour étendre et affiner la manière dont la conversion WAV vers MIDI est effectuée.

Conclusion

J'espère que vous avez trouvé cet article intéressant et que vous apprécierez l'application. Peut-être vous inspirera-t-elle de nouvelles idées.

0
0 30
Article Iryna Mykhailova · Mai 2, 2025 3m read

Qui n'a jamais développé un bel exemple avec une image IRIS Docker et vu la génération de l'image échouer dans le Dockerfile parce que la licence sous laquelle l'image a été créée ne comportait pas certains privilèges ?

Dans mon cas, je déployais dans Docker une petite application utilisant le type de données Vector. Avec la version Community, ce n'est pas un problème, car elle inclut déjà la recherche et le stockage vectoriels. Cependant, lorsque j'ai remplacé l'image IRIS par une image IRIS classique (latest-cd), j'ai constaté que la compilation de l'image, y compris des classes générées, renvoyait l'erreur suivante :

0
0 24
InterSystems officiel Adeline Icard · Mars 27, 2025

InterSystems annonce la disponibilité générale d'InterSystems IRIS, InterSystems IRIS for Health et HealthShare Health Connect 2025.1.

La version 2025.1 de la plateforme de données InterSystems IRIS®, InterSystems IRIS® for HealthTM et HealthShare® Health Connect est désormais disponible. Il s'agit d'une version en maintenance prolongée.

Points forts de la version

Cette nouvelle version propose plusieurs nouvelles fonctionnalités et améliorations, notamment :

1. Fonctionnalités avancées de recherche vectorielle

0
0 28
Annonce Irène Mykhailova · Mars 14, 2025

Bonjour à tous,

Voici le premier concours de programmation de l'année, et une surprise vous attend ! Lisez la suite !

🏆 Concours de programmation InterSystems IA : Recherche vectorielle, GenAI et agents IA 🏆

Durée : du 17 mars au 6 avril 2025

Prix : 12 000 $ + une chance d'être invité au Sommet mondial 2025 !


0
0 27
Article Rahul Singhal · Mars 3, 2025 6m read

Introduction

Pour atteindre des performances optimisées en matière d'IA, une explicabilité robuste, une adaptabilité et une efficacité dans les solutions de santé, InterSystems IRIS sert de fondation centrale pour un projet au sein du cadre multi-agent x-rAI. Cet article offre une analyse approfondie de la manière dont InterSystems IRIS permet le développement d'une plateforme d'analyse de données de santé en temps réel, permettant des analyses avancées et des informations exploitables. La solution exploite les points forts d'InterSystems IRIS, notamment le SQL dynamique, les capacités natives de recherche vectorielle, la mise en cache distribuée (ECP) et l'interopérabilité FHIR. Cette approche innovante s'aligne directement sur les thèmes du concours « Utilisation du SQL dynamique et SQL intégré », « GenAI, recherche vectorielle » et « FHIR, DME », démontrant une application pratique d'InterSystems IRIS dans un contexte critique de santé.

Architecture du système

L'Agent Santé dans x-rAI repose sur une architecture modulaire intégrant plusieurs composants :

Couche d'ingestion des données : Récupère les données de santé en temps réel à partir de dispositifs portables via l'API Terra.

Couche de stockage des données : Utilise InterSystems IRIS pour stocker et gérer les données de santé structurées.

Moteur analytique : Exploite les capacités de recherche vectorielle d'InterSystems IRIS pour l'analyse de similarité et la génération d'informations.

Couche de mise en cache : Implémente la mise en cache distribuée via le protocole Enterprise Cache Protocol (ECP) d'InterSystems IRIS pour améliorer l'évolutivité.

Couche d'interopérabilité : Utilise les normes FHIR pour s'intégrer aux systèmes externes de santé comme les DME.

Voici un diagramme d'architecture à haut niveau :

text [Dispositifs portables] --> [API Terra] --> [Ingestion des données] --> [InterSystems IRIS] --> [Moteur analytique] ------[Couche de mise en cache]------ ----[Intégration FHIR]----- Mise en œuvre technique

  1. Intégration des données en temps réel avec SQL dynamique

L'Agent Santé ingère des métriques de santé en temps réel (par exemple, fréquence cardiaque, pas effectués, heures de sommeil) depuis des dispositifs portables via l'API Terra. Ces données sont stockées dans InterSystems IRIS à l'aide du SQL dynamique pour une flexibilité dans la génération des requêtes.

Implémentation du SQL dynamique

Le SQL dynamique permet au système de construire adaptativement des requêtes basées sur les structures des données entrantes.

text def index_health_data_to_iris(data): conn = iris_connect() if conn is None: raise ConnectionError("Échec de la connexion à InterSystems IRIS.") try: with conn.cursor() as cursor: query = """ INSERT INTO HealthData (user_id, heart_rate, steps, sleep_hours) VALUES (?, ?, ?, ?) """ cursor.execute(query, ( data['user_id'], data['heart_rate'], data['steps'], data['sleep_hours'] )) conn.commit() print("Données indexées avec succès dans IRIS.") except Exception as e: print(f"Erreur lors de l'indexation des données : {e}") finally: conn.close() Avantages du SQL dynamique

Permet une construction flexible des requêtes basée sur les schémas des données entrantes.

Réduit la charge de développement en évitant les requêtes codées en dur.

Facilite l'intégration transparente de nouvelles métriques sans modifier le schéma de la base.

  1. Analytique avancée avec recherche vectorielle

Le type natif vector et les fonctions de similarité d'InterSystems IRIS ont été utilisés pour effectuer une recherche vectorielle sur les données de santé. Cela permet au système d’identifier les dossiers historiques similaires aux métriques actuelles d’un utilisateur.

Flux de travail pour la recherche vectorielle

Convertir les métriques de santé (par exemple, fréquence cardiaque, pas effectués, heures de sommeil) en une représentation vectorielle.

Stocker ces vecteurs dans une colonne dédiée dans la table HealthData.

Effectuer des recherches basées sur la similarité à l'aide de VECTOR_SIMILARITY().

Requête SQL pour la recherche vectorielle

text SELECT TOP 3 user_id, heart_rate, steps, sleep_hours, VECTOR_SIMILARITY(vec_data, ?) AS similarity FROM HealthData ORDER BY similarity DESC; Intégration Python

text def iris_vector_search(query_vector): conn = iris_connect() if conn is None: raise ConnectionError("Échec de la connexion à InterSystems IRIS.") try: with conn.cursor() as cursor: query_vector_str = ",".join(map(str, query_vector)) sql = """ SELECT TOP 3 user_id, heart_rate, steps, sleep_hours, VECTOR_SIMILARITY(vec_data, ?) AS similarity FROM HealthData ORDER BY similarity DESC; """ cursor.execute(sql, (query_vector_str,)) results = cursor.fetchall() return results except Exception as e: print(f"Erreur lors de la recherche vectorielle : {e}") return [] finally: conn.close() Avantages de la recherche vectorielle

Permet des recommandations personnalisées grâce à l’identification des tendances historiques.

Améliore l’explicabilité en reliant les métriques actuelles à des cas passés similaires.

Optimisé pour des analyses rapides grâce aux opérations SIMD (Single Instruction Multiple Data).

  1. Mise en cache distribuée pour l’évolutivité

Pour gérer efficacement le volume croissant des données, l’Agent Santé utilise le protocole Enterprise Cache Protocol (ECP) d’InterSystems IRIS. Ce mécanisme réduit la latence et améliore l’évolutivité.

Caractéristiques clés

Mise en cache locale sur les serveurs applicatifs pour minimiser les requêtes vers la base centrale.

Synchronisation automatique garantissant la cohérence entre tous les nœuds du cache.

Évolutivité horizontale permettant l’ajout dynamique de serveurs applicatifs.

Avantages du caching

Réduction des temps de réponse grâce à la mise en cache locale.

Amélioration de l’évolutivité grâce à la répartition des charges entre plusieurs nœuds.

Réduction des coûts d’infrastructure grâce à une moindre sollicitation du serveur central.

  1. Intégration FHIR pour l’interopérabilité

Le support d’InterSystems IRIS pour FHIR (Fast Healthcare Interoperability Resources) garantit une intégration fluide avec les systèmes externes comme les DME.

Flux FHIR

Les données issues des dispositifs portables sont transformées en ressources compatibles FHIR (par exemple Observation, Patient).

Ces ressources sont stockées dans InterSystems IRIS et accessibles via des API RESTful.

Les systèmes externes peuvent interroger ou mettre à jour ces ressources via des points d’accès standard FHIR.

Avantages

Assure la conformité avec les normes d’interopérabilité en santé.

Facilite un échange sécurisé des données entre systèmes.

Permet une intégration avec les flux et applications existants dans le domaine médical.

IA explicable grâce aux informations en temps réel

En combinant les capacités analytiques d’InterSystems IRIS avec le cadre multi-agent x-rAI, l’Agent Santé génère des informations exploitables et explicables. Par exemple :

« L’utilisateur 123 avait des métriques similaires (Fréquence cardiaque : 70 bpm ; Pas : 9 800 ; Sommeil : 7 h). Sur la base des tendances historiques, il est recommandé de maintenir vos niveaux actuels d’activité. »

Cette transparence renforce la confiance dans les applications IA dédiées à la santé en offrant un raisonnement clair derrière chaque recommandation.

Conclusion

L’intégration d’InterSystems IRIS dans l’Agent Santé du cadre x-rAI illustre son potentiel comme plateforme robuste pour construire des systèmes IA intelligents et explicables dans le domaine médical. En exploitant le SQL dynamique, la recherche vectorielle, la mise en cache distribuée et l’interopérabilité FHIR, ce projet fournit des informations exploitables et transparentes—ouvrant ainsi la voie à des applications IA plus fiables dans des domaines critiques comme celui de la santé.

0
0 45
Article Sylvain Guilbaud · Déc 26, 2024 3m read

Traduit du concours d'articles de la communauté espagnole.

Suite au dernier concours de programmation sur OEX j'ai eu une observation surprenante.
Il existait des applications presque exclusives basées sur l'IA en combinaison avec des modules Py précuits.
Mais en creusant plus profondément, tous les exemples utilisaient les mêmes éléments techniques d'IRIS.

Du point de vue d'IRIS, c'était à peu près la même chose que la recherche de texte
ou la recherche d'images ou d'autres motifs.  Cela s'est terminé par des méthodes presque échangeables.

1
0 40
InterSystems officiel Adeline Icard · Nov 27, 2024

InterSystems annonce la disponibilité générale d'InterSystems IRIS, InterSystems IRIS for Health et HealthShare Health Connect 2024.3

La version 2024.3 de la plateforme de données InterSystems IRIS®, InterSystems IRIS® for HealthTM et HealthShare® Health Connect est désormais généralement disponible (GA).

Points forts de la version

Dans cette version, vous pouvez vous attendre à une multitude de mises à jour intéressantes, notamment :

0
0 38
Article Guillaume Rongier · Nov 11, 2024 2m read

Bienvenue au troisième et dernier de nos articles consacrés au développement d'applications RAG basées sur des modèles LLM. Dans ce dernier article, nous examinerons comment, dans le cadre de notre petit projet d'exemple, on peut trouver le contexte le plus approprié pour la question que l'on veut envoyer à notre modèle LLM et, pour ce faire, nous utiliserons la fonctionnalité de recherche vectorielle incluse dans IRIS.

Meme Creator - Funny Context Meme Generator at MemeCreator.org!

Recherches vectorielles

Un élément clé de toute application RAG est le mécanisme de recherche vectorielle, qui permet de rechercher dans une table comportant des enregistrements de ce type les enregistrements les plus similaires au vecteur de référence. Pour ce faire, il est nécessaire de générer d'abord l'intégration de la question à transmettre au LLM. Jetons un coup d'œil à notre projet d'exemple pour voir comment nous générons cette intégration et l'utilisons pour lancer la requête dans notre base de données IRIS:

model = sentence_transformers.SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')  
question = model.encode("¿Qué medicamento puede tomar mi hijo de 2 años para bajar la fiebre?", normalize_embeddings=True)
array = np.array(question)
formatted_array = np.vectorize('{:.12f}'.format)(array)
parameterQuery = []
parameterQuery.append(str(','.join(formatted_array)))
cursorIRIS.execute("SELECT distinct(Document) FROM (SELECT VECTOR_DOT_PRODUCT(VectorizedPhrase, TO_VECTOR(?, DECIMAL)) AS similarity, Document FROM LLMRAG.DOCUMENTCHUNK) WHERE similarity > 0.6", parameterQuery)
similarityRows = cursorIRIS.fetchall()

Comme vous pouvez le voir, nous instancions le modèle générateur d'embeddings et vectorisons la question que nous allons soumettre à notre LLM. Ensuite, nous lançons une requête dans notre table de données LLMRAG.DOCUMENTCHUNK pour rechercher les vecteurs dont la similarité est supérieure à 0,6 (cette valeur est totalement basée sur les critères du développeur du produit).

Comme vous pouvez le constater, la commande utilisée pour la recherche est VECTOR_DOT_PRODUCT, mais ce n'est pas la seule option, examinons les deux options dont nous disposons pour les recherches de similarité.

Produit scalaire (VECTOR_DOT_PRODUCT)

Cette opération algébrique est simplement la somme des produits de chaque paire d'éléments occupant la même position dans leurs vecteurs respectifs, représentés comme suit:

generated description: dot product calc

InterSystems recommande l'utilisation de cette méthode lorsque les vecteurs à travailler sont unitaires, c'est-à-dire que leur module est égal à 1. Pour ceux d'entre vous qui ne sont pas familiers avec l'algèbre, le module se calcule de la manière suivante:

{\displaystyle \mid {\vec {v}}\mid ={\sqrt {v_{1}^{2}+v_{2}^{2}+v_{3}^{2}}}}

Similarité cosinus (Cosine similarity)

Ce calcul représente le produit scalaire des vecteurs divisé par le produit de la longueur des vecteurs et sa formule est la suivante:

generated description: cosine calc

Dans les deux cas, la similarité entre les vecteurs est d'autant plus grande que le résultat est proche de 1

Injection du contexte

Avec la requête précédente, nous obtiendrons les textes liés à la question que nous enverrons au LLM. Dans notre cas, nous n'allons pas transmettre le texte que nous avons vectorisé car nous trouvons qu'il est plus intéressant d'envoyer le document complet avec la notice du médicament car le modèle LLM sera capable d'assembler une réponse beaucoup plus complète avec le document complet. Jetons un coup d'œil à notre code pour voir comment nous procédons:

for similarityRow in similarityRows:
    for doc in docs_before_split:
        if similarityRow[0] == doc.metadata['source'].upper():
            context = context +"".join(doc.page_content)
prompt = hub.pull("rlm/rag-prompt")

rag_chain = ( {"context": lambda x: context, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() )

rag_chain.invoke("¿Qué medicamento puede tomar mi hijo de 2 años para bajar la fiebre?")

Empezamos con un bucle for que nos recorrerá todos los registros vectorizados que son similares a la pregunta realizada. Como en nuestro ejemplo

Nous commençons par une boucle for qui passe en revue tous les enregistrements vectorisés similaires à la question posée. Comme, dans notre exemple, les documents ont été stockés en mémoire lors de l'étape précédente de découpage et de vectorisation, nous les avons réutilisés dans la deuxième boucle pour extraire directement le texte, le mieux étant d'accéder au document que nous avons stocké dans notre système sans devoir utiliser cette deuxième boucle for.

Une fois que nous avons stocké le texte des documents qui formeront le contexte de la question dans une variable, l'étape suivante sera d'informer le LLM du contexte de la question que nous allons lui transmettre. Une fois le contexte transmis, il ne nous reste plus qu'à envoyer notre question au modèle. Dans ce cas, nous voulons savoir quels médicaments nous pouvons donner à notre fils de 2 ans pour réduire sa fièvre, voyons les réponses sans contexte et avec contexte:

Sans contexte:

La fièvre chez les jeunes enfants peut être une source d'inquiétude, mais il est important de la gérer correctement. Pour un enfant de 2 ans, les médicaments les plus couramment recommandés pour faire baisser la fièvre sont le paracétamol (acétaminophène) et l'ibuprofène.

Avec contexte:

Dalsy 40 mg/ml suspension buvable, contenant de l'ibuprofène, peut être utilisé pour soulager la fièvre chez les enfants à partir de l'âge de 3 mois. La dose recommandée pour les enfants à partir de 2 ans dépend de leur poids et doit être administrée sur ordonnance. Par exemple, pour un enfant de 10 kg, la dose recommandée est de 1,8 à 2,4 ml par prise, avec une dose journalière maximale de 7,2 ml (288 mg). Consultez toujours un médecin avant d'administrer un médicament à un enfant.

Comme vous pouvez le constater, sans contexte, la réponse est assez générique, alors qu'avec un contexte adéquat, la réponse est beaucoup plus directe et indique que le médicament doit toujours être prescrit par un médecin

Conclusions

Dans cette série d'articles, nous avons présenté les bases du développement d'applications RAG, comme vous pouvez le voir, les concepts de base sont assez simples, mais vous savez que le diable est toujours dans les détails, pour tout projet il est nécessaire de prendre les décisions suivantes:

  • Quel modèle LLM faut-il utiliser? Un service local ou un service en ligne?
  • Quel modèle d'intégration faut-il utiliser? Fonctionne-t-il correctement pour la langue que nous allons utiliser? Devons-nous lemmatiser les textes que nous allons vectoriser?
  • Comment allons-nous découper nos documents en fonction du contexte? Par paragraphe? Par longueur de texte? Avec des chevauchements?
  • Notre contexte est-il uniquement basé sur des documents non structurés ou disposons-nous de différentes sources de données?
  • Devons-nous reclasser les résultats de la recherche vectorielle par laquelle nous avons extrait le contexte ? Et si la réponse est oui, quel modèle devons-nous appliquer?
  • ...

Le développement d'applications RAG implique un effort plus important dans la validation des résultats que dans le développement technique de l'application elle-même. Vous devez être vraiment sûr que votre application ne fournit pas de réponses erronées ou inexactes, car cela pourrait avoir de graves conséquences, non seulement sur le plan juridique, mais aussi sur le plan de la confiance.

0
0 186
Article Guillaume Rongier · Nov 6, 2024 7m read

Nous poursuivons cette série d'articles sur les applications LLM et RAG et dans cet article nous traiterons de la partie encadrée en rouge du diagramme ci-dessous:

Lors de la création d'une application RAG, il est tout aussi important de choisir un modèle LLM qui réponde à vos besoins (formation dans le domaine correspondant, coût, rapidité, etc.) que de définir clairement le contexte que vous souhaitez lui fournir. Commençons par définir le terme afin d'être clair sur ce que nous entendons par contexte.

Qu'est-ce que le contexte?

Le contexte fait référence à des informations supplémentaires obtenues à partir d'une source externe, telle qu'une base de données ou un système de recherche, afin de compléter ou d'améliorer les réponses générées par un modèle linguistique. Le modèle linguistique utilise ces informations externes pertinentes pour générer des réponses plus précises et plus détaillées plutôt que de s'appuyer uniquement sur ce qu'il a appris au cours de sa formation. Le contexte permet d'actualiser les réponses et de les aligner sur le sujet spécifique de la requête.

Ce contexte peut provenir d'informations stockées dans une base de données avec un outil similaire à celui montré par notre cher membre de la communauté @José Pereira вйты суе article ou d'informations non structurées sous la forme de fichiers texte que nous alimenterons avec le LLM, ce qui sera le cas que nous allons discuter ici.

Comment générer le contexte de notre application RAG?

Tout d'abord, la chose la plus indispensable est évidemment de disposer de toutes les informations que nous considérons comme pertinentes pour les éventuelles requêtes qui seront formulées à l'encontre de notre demande. Une fois ces informations organisées de manière à être accessibles depuis notre application, nous devons être en mesure d'identifier, parmi tous les documents disponibles dans notre contexte, ceux qui se rapportent à la question spécifique posée par l'utilisateur. Dans notre exemple, nous disposons d'une série de documents PDF (notices de médicaments) que nous voulons utiliser comme contexte possible pour les questions posées par les utilisateurs de notre application.

Cet aspect est essentiel au succès d'une application RAG ; il est tout aussi mauvais pour la confiance d'un utilisateur de répondre par des généralisations et des affirmations vagues typiques d'un LLM que de répondre avec un contexte complètement erroné. C'est là qu'interviennent nos précieuses bases de données vectorielles.

Bases de données vectorielles

Vous avez probablement entendu parler des "bases de données vectorielles" comme s'il s'agissait d'un nouveau type de base de données, telles que les bases de données relationnelles ou documentaires, mais rien n'est plus faux. Les bases de données vectorielles sont des bases de données qui supportent les types de données vectorielles et les opérations qui leur sont liées. Voyons dans le projet associé à l'article comment ce type de données sera représenté:

Voyons maintenant à quoi ressemble un enregistrement:

Bases de données vectorielles... Pourquoi faire?

Comme nous l'avons expliqué dans l'article précédent sur les LLM, l'utilisation des vecteurs est essentielle dans les modèles de langage, car ils permettent de représenter les concepts et les relations entre eux dans un espace multidimensionnel. Dans notre cas, cette représentation multidimensionnelle sera la clé pour identifier les documents de notre contexte qui seront pertinents pour la question posée.

Parfait, nous avons notre base de données vectorielle et les documents qui fourniront le contexte, il ne nous reste plus qu'à enregistrer le contenu de ces documents dans notre base de données, mais.... Avec quels critères?

Modèles pour la vectorisation

Eh bien... il n'est pas nécessaire d'ennuyer notre LLM pour vectoriser nos informations contextuelles, nous pouvons utiliser des modèles de langage plus petits qui conviennent mieux à nos besoins pour cette tâche, tels que les modèles entraînés pour détecter les similarités entre les phrases. Vous pouvez en trouver une myriade dans Hugging Face,  chacun étant entraîné avec un ensemble de données spécifique qui nous permettra d'améliorer la vectorisation de nos données.

Et si cela ne vous convainc pas d'utiliser l'un de ces modèles pour la vectorisation, sachez qu'en général ce type de modèles...

Voyons dans notre exemple comment nous invoquons le modèle choisi pour ces vectorisations:

ifnot os.path.isdir('/app/data/model/'):
    model = sentence_transformers.SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')            
    model.save('/app/data/model/')

Ici, nous téléchargeons le modèle choisi pour notre cas sur notre ordinateur local. Ce mini-LM est multi-langues, nous pourrons donc vectoriser en espagnol et en anglais sans aucun problème.

Chunking

Si vous avez déjà joué avec des modèles de langue, vous avez probablement déjà été confronté au défi du découpage en morceaux - ou "chunking". Qu'est-ce que chunking ? Il s'agit tout simplement de la division d'un texte en fragments plus petits susceptibles de contenir un sens pertinent. Grâce à ce découpage de notre contexte, nous pouvons effectuer des requêtes sur notre base de données vectorielle afin d'extraire les documents de notre contexte qui peuvent être pertinents pour la question posée.

Quels sont les critères de ce découpage ? Il n'y a pas vraiment de critère magique qui nous permette de savoir quelle doit être la longueur de nos morceaux pour qu'ils soient aussi précis que possible. Dans notre exemple, nous utilisons une bibliothèque Python fournie par langchain pour effectuer ce découpage, bien que toute autre méthode ou bibliothèque puisse être utilisée:

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 700,
    chunk_overlap  = 50,
)
path = "/app/data"
loader = PyPDFDirectoryLoader(path)
docs_before_split = loader.load()
docs_after_split = text_splitter.split_documents(docs_before_split)

Comme vous pouvez le constater, la taille choisie est de 700 caractères, avec un chevauchement de 50 pour éviter de couper des mots. Ce sont ces fragments extraits de nos documents que nous allons vectoriser et insérer dans notre base de données.

Ce processus de découpage peut être optimisé à volonté au moyen de la « lemmatisation », qui permet de transformer les mots en leur lemme correspondant (sans les temps des verbes, les pluriels, le genre, etc.) et d'éliminer ainsi une partie du bruit pour la génération du vecteur, mais nous n'allons pas nous étendre sur ce point, vous pouvez voir une explication plus détaillée sur cette page .

Vectorisation des fragments

Très bien, nous avons extrait nos fragments de chacun de nos documents, il est temps de les vectoriser et de les insérer dans notre base de données, jetons un coup d'œil au code pour comprendre comment nous pourrions le faire.

for doc in docs_after_split:
    embeddings = model.encode(doc.page_content, normalize_embeddings=True)
    array = np.array(embeddings)
    formatted_array = np.vectorize('{:.12f}'.format)(array)
    parameters = []
    parameters.append(doc.metadata['source'])
    parameters.append(str(doc.page_content))
    parameters.append(str(','.join(formatted_array)))
    cursorIRIS.execute("INSERT INTO LLMRAG.DOCUMENTCHUNK (Document, Phrase, VectorizedPhrase) VALUES (?, ?, TO_VECTOR(?,DECIMAL))", parameters)
connectionIRIS.commit()

Comme vous pouvez le constater, nous allons suivre les étapes suivantes: 

  1. Nous parcourons la liste de tous les fragments obtenus à partir de tous les documents qui vont former notre contexte.
  2. Pour chaque fragment, nous vectorisons le texte (en utilisant la bibliothèque sentence_transformers). 
  3. Nous créons un tableau à l'aide de la bibliothèque numpy avec le vecteur formaté et le transformons en chaîne de caractères.
  4. Nous enregistrons les informations du document avec son vecteur associé dans notre base de données. Comme vous pouvez le voir, nous exécutons la commande TO_VECTOR qui transformera la chaîne du vecteur que nous avons passé au format approprié.

Conclusion

Dans cet article nous avons vu la nécessité d'avoir une base de données vectorielle pour la création du contexte nécessaire dans notre application RAG, nous avons aussi vu comment découper et vectoriser l'information de notre contexte pour son enregistrement dans cette base de données.

Dans le prochain article, nous verrons comment interroger notre base de données vectorielles à partir de la question que l'utilisateur envoie au modèle LLM et comment, à l'aide d'une recherche de similarité, nous assemblerons le contexte que nous transmettrons au modèle. Ne manquez pas cet article!

0
0 460
Article Lorenzo Scalese · Oct 10, 2024 30m read

Une expérience sur la manière d'utiliser le cadre LangChain, la recherche vectorielle IRIS et les LLM pour générer une base de données SQL compatible IRIS à partir des invites utilisateur.

Cet article a été rédigé à partir du carnet suivant. Vous pouvez l'utiliser dans un environnement prêt à l'emploi avec l'application suivante dans OpenExchange.

Configuration

Tout d'abord, nous devons installer les bibliothèques nécessaires:

!pip install --upgrade --quiet langchain langchain-openai langchain-iris pandas

Ensuite, nous importons les modules requis et configurons l'environnement:

import os
import datetime
import hashlib
from copy import deepcopy
from sqlalchemy import create_engine
import getpass
import pandas as pd
from langchain_core.prompts import PromptTemplate, ChatPromptTemplate
from langchain_core.example_selectors import SemanticSimilarityExampleSelector
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.docstore.document import Document
from langchain_community.document_loaders import DataFrameLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain_core.output_parsers import StrOutputParser
from langchain.globals import set_llm_cache
from langchain.cache import SQLiteCache
from langchain_iris import IRISVector

Nous utiliserons SQLiteCache pour mettre en cache les appels LLM:

# Cache pour les appels LLM
set_llm_cache(SQLiteCache(database_path=".langchain.db"))

Configurez les paramètres de connexion à la base de données IRIS:

# Paramètres de connexion à la base de données IRIS
os.environ["ISC_LOCAL_SQL_HOSTNAME"] = "localhost"
os.environ["ISC_LOCAL_SQL_PORT"] = "1972"
os.environ["ISC_LOCAL_SQL_NAMESPACE"] = "IRISAPP"
os.environ["ISC_LOCAL_SQL_USER"] = "_system"
os.environ["ISC_LOCAL_SQL_PWD"] = "SYS"

Si la clé API OpenAI n'est pas déjà configurée dans l'environnement, demandez à l'utilisateur de la saisir:

if not "OPENAI_API_KEY" in os.environ:
    os.environ["OPENAI_API_KEY"] = getpass.getpass()

Créez la chaîne de connexion pour la base de données IRIS:

# Chaîne de connexion à la base de données IRIS
args = {
    'hostname': os.getenv("ISC_LOCAL_SQL_HOSTNAME"), 
    'port': os.getenv("ISC_LOCAL_SQL_PORT"), 
    'namespace': os.getenv("ISC_LOCAL_SQL_NAMESPACE"), 
    'username': os.getenv("ISC_LOCAL_SQL_USER"), 
    'password': os.getenv("ISC_LOCAL_SQL_PWD")
}
iris_conn_str = f"iris://{args['username']}:{args['password']}@{args['hostname']}:{args['port']}/{args['namespace']}"

Etablissez la connexion avec la base de données IRIS:

# Connexion à la base de données IRIS
engine = create_engine(iris_conn_str)
cnx = engine.connect().connection

Préparez un dictionnaire contenant les renseignements contextuels pour l'invite du système:

# Dict pour les renseignements contextuels de l'invite système
context = {}
context["top_k"] = 3

Création de l'invite

Pour transformer les données de l'utilisateur en requêtes SQL compatibles avec la base de données IRIS, nous devons créer une invite efficace pour le modèle linguistique. Nous commençons par une invite initiale qui fournit des instructions de base pour générer des requêtes SQL. Ce modèle est dérivé des Invites par défaut de LangChain pour MSSQL et personnalisé pour la base de données IRIS.

# Modèle d'invite de base avec instructions SQL pour la base de données IRIS
iris_sql_template = """
Vous êtes un expert InterSystems IRIS.  Compte tenu d'une question d'entrée, créez d'abord une requête InterSystems IRIS syntaxiquement correcte pour exécuter et renvoyer la réponse à la question saisie.
Si l'utilisateur ne spécifie pas dans la question un nombre spécifique d'exemples à obtenir, demandez au maximum {top_k} résultats en utilisant la clause TOP conformément à InterSystems IRIS. Vous pouvez classer les résultats de manière à obtenir les renseignements les plus pertinents de la base de données.
Ne faites jamais de requête pour toutes les colonnes d'une table. Vous ne devez requérir que les colonnes nécessaires pour répondre à la question.. Mettez chaque nom de colonne entre guillemets simples ('') pour indiquer qu'il s'agit d'identifiants délimités.
Veillez à n'utiliser que les noms de colonnes que vous pouvez voir dans les tables ci-dessous. Veillez à ne pas requérir les colonnes qui n'existent pas. Faites également attention à ce que les colonnes se trouvent dans les différents tables.
Veillez à utiliser la fonction CAST(CURRENT_DATE as date) pour obtenir la date du jour, si la question porte sur "aujourd'hui".
Utilisez des guillemets doubles pour délimiter les identifiants des colonnes.
Renvoyez des données SQL simples ; n'appliquez aucune forme de formatage.
"""

Cette invite de base configure le modèle linguistique (LLM) pour qu'il fonctionne comme un expert SQL avec des conseils spécifiques pour la base de données IRIS. Ensuite, nous fournissons une invite auxiliaire avec des renseignements sur le schéma de la base de données pour éviter les hallucinations.

# Extension des modèles SQL pour inclure les renseignements sur le contexte des tables
tables_prompt_template = """
N'utilisez que les tables suivantes:
{table_info}
"""

Afin d'améliorer la précision des réponses du LLM, nous utilisons une technique appelée "incitation en quelques coups" ("few-shot prompting"). Il s'agit de présenter quelques exemples au LLM.

# Extension du modèle SQL pour l'inclusion de quelques exemples
prompt_sql_few_shots_template = """
Vous trouverez ci-dessous un certain nombre d'exemples de questions et de requêtes SQL correspondantes.

{examples_value}
"""

Nous définissons le modèle pour des exemples en quelques coups:

# Modèle d'invite à quelques coups
example_prompt_template = "User input: {input}\nSQL query: {query}"
example_prompt = PromptTemplate.from_template(example_prompt_template)

Nous construisons l'invite utilisateur en utilisant le modèle en quelques coups:

# Modèle d'invite utilisateur
user_prompt = "\n" + example_prompt.invoke({"input": "{input}", "query": ""}).to_string()

Enfin, nous composons toutes les invites pour créer l'invite finale:

# Modèle d'invite complet
prompt = (
    ChatPromptTemplate.from_messages([("system", iris_sql_template)])
    + ChatPromptTemplate.from_messages([("system", tables_prompt_template)])
    + ChatPromptTemplate.from_messages([("system", prompt_sql_few_shots_template)])
    + ChatPromptTemplate.from_messages([("human", user_prompt)])
)
prompt

Cette invite attend les variables examples_value, input, table_info, et top_k.

Voici comment l'invite est structurée:

ChatPromptTemplate(
    input_variables=['examples_value', 'input', 'table_info', 'top_k'], 
    messages=[
        SystemMessagePromptTemplate(
            prompt=PromptTemplate(
                input_variables=['top_k'], 
                template=iris_sql_template
            )
        ), 
        SystemMessagePromptTemplate(
            prompt=PromptTemplate(
                input_variables=['table_info'], 
                template=tables_prompt_template
            )
        ), 
        SystemMessagePromptTemplate(
            prompt=PromptTemplate(
                input_variables=['examples_value'], 
                template=prompt_sql_few_shots_template
            )
        ), 
        HumanMessagePromptTemplate(
            prompt=PromptTemplate(
                input_variables=['input'], 
                template=user_prompt
            )
        )
    ]
)

Pour visualiser la manière dont l'invite sera envoyée au LLM, nous pouvons utiliser des valeurs de remplacement pour les variables requises:

prompt_value = prompt.invoke({
    "top_k": "<top_k>",
    "table_info": "<table_info>",
    "examples_value": "<examples_value>",
    "input": "<input>"
})
print(prompt_value.to_string())
Système: 
Vous êtes un expert d'InterSystems IRIS. Compte tenu d'une question d'entrée, créez d'abord une requête InterSystems IRIS syntaxiquement correcte pour exécuter et renvoyer la réponse à la question saisie.
Si l'utilisateur ne spécifie pas dans la question un nombre spécifique d'exemples à obtenir, demandez au maximum <top_k> résultats en utilisant la clause TOP conformément à InterSystems IRIS. Vous pouvez classer les résultats de manière à obtenir les renseignements les plus pertinents de la base de données.
Ne faites jamais de requête pour toutes les colonnes d'une table. Vous ne devez requérir que les colonnes nécessaires pour répondre à la question.. Mettez chaque nom de colonne entre guillemets simples ('') pour indiquer qu'il s'agit d'identifiants délimités.
Veillez à n'utiliser que les noms de colonnes que vous pouvez voir dans les tables ci-dessous. Veillez à ne pas requérir les colonnes qui n'existent pas. Faites également attention à ce que les colonnes se trouvent dans les différents tables.
Veillez à utiliser la fonction CAST(CURRENT_DATE as date) pour obtenir la date du jour, si la question porte sur "aujourd'hui".
Utilisez des guillemets doubles pour délimiter les identifiants des colonnes.
Renvoyez des données SQL simples; n'appliquez aucune forme de formatage.

Système: 
N'utilisez que les tables suivantes:
<table_info>

Système: 
Vous trouverez ci-dessous un certain nombre d'exemples de questions et de requêtes SQL correspondantes.

<examples_value>

Human: 
User input: <input>
SQL query: 

Maintenant, nous sommes prêts à envoyer cette invite au LLM en fournissant les variables nécessaires. Passons à l'étape suivante lorsque vous êtes prêt.

Fourniture des renseignements sur la table

Pour créer des requêtes SQL précises, nous devons fournir au modèle linguistique (LLM) des renseignements détaillés sur les tables de la base de données. Sans ces renseignements, le LLM pourrait générer des requêtes qui semblent plausibles mais qui sont incorrectes en raison d'hallucinations. Par conséquent, notre première étape consiste à créer une fonction qui récupère les définitions des tables de la base de données IRIS.

Fonction de récupération des définitions de tables

La fonction suivante interroge INFORMATION_SCHEMA pour obtenir les définitions de tables pour un schéma donné. Si une table spécifique est fournie, elle récupère la définition de cette table ; sinon, elle récupère les définitions de toutes les tables du schéma.

def get_table_definitions_array(cnx, schema, table=None):
    cursor = cnx.cursor()

    # Requête de base pour obtenir les renseignements sur les colonnes
    query = """
    SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, PRIMARY_KEY, null EXTRA
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE TABLE_SCHEMA = %s
    """
    
    # Paramètres de la requête
    params = [schema]

    # Ajout de filtres optionnels
    if table:
        query += " AND TABLE_NAME = %s"
        params.append(table)
    
    # Exécution de la requête
    cursor.execute(query, params)

    # Récupération des résultats
    rows = cursor.fetchall()
    
    # Traitement des résultats pour générer la (les) définition(s) de table(s)
    table_definitions = {}
    for row in rows:
        table_schema, table_name, column_name, column_type, is_nullable, column_default, column_key, extra = row
        if table_name not in table_definitions:
            table_definitions[table_name] = []
        table_definitions[table_name].append({
            "column_name": column_name,
            "column_type": column_type,
            "is_nullable": is_nullable,
            "column_default": column_default,
            "column_key": column_key,
            "extra": extra
        })

    primary_keys = {}
    
    # Construire la chaîne de sortie
    result = []
    for table_name, columns in table_definitions.items():
        table_def = f"CREATE TABLE {schema}.{table_name} (\n"
        column_definitions = []
        for column in columns:
            column_def = f"  {column['column_name']} {column['column_type']}"
            if column['is_nullable'] == "NO":
                column_def += " NOT NULL"
            if column['column_default'] is not None:
                column_def += f" DEFAULT {column['column_default']}"
            if column['extra']:
                column_def += f" {column['extra']}"
            column_definitions.append(column_def)
        if table_name in primary_keys:
            pk_def = f"  PRIMARY KEY ({', '.join(primary_keys[table_name])})"
            column_definitions.append(pk_def)
        table_def += ",\n".join(column_definitions)
        table_def += "\n);"
        result.append(table_def)

    return result

Récupération des définitions de tables pour un schéma

Pour cet exemple, nous utilisons le schéma "Aviation", qui est disponible ici.

# Récupération des définitions de tables pour un schéma "Aviation"
tables = get_table_definitions_array(cnx, "Aviation")
print(tables)

Cette fonction renvoie les instructions CREATE TABLE (creer une table) pour toutes les tables du schéma "Aviation":

[
    'CREATE TABLE Aviation.Aircraft (\n  Event bigint NOT NULL,\n  ID varchar NOT NULL,\n  AccidentExplosion varchar,\n  AccidentFire varchar,\n  AirFrameHours varchar,\n  AirFrameHoursSince varchar,\n  AirFrameHoursSinceLastInspection varchar,\n  AircraftCategory varchar,\n  AircraftCertMaxGrossWeight integer,\n  AircraftHomeBuilt varchar,\n  AircraftKey integer NOT NULL,\n  AircraftManufacturer varchar,\n  AircraftModel varchar,\n  AircraftRegistrationClass varchar,\n  AircraftSerialNo varchar,\n  AircraftSeries varchar,\n  Damage varchar,\n  DepartureAirportId varchar,\n  DepartureCity varchar,\n  DepartureCountry varchar,\n  DepartureSameAsEvent varchar,\n  DepartureState varchar,\n  DepartureTime integer,\n  DepartureTimeZone varchar,\n  DestinationAirportId varchar,\n  DestinationCity varchar,\n  DestinationCountry varchar,\n  DestinationSameAsLocal varchar,\n  DestinationState varchar,\n  EngineCount integer,\n  EvacuationOccurred varchar,\n  EventId varchar NOT NULL,\n  FlightMedical varchar,\n  FlightMedicalType varchar,\n  FlightPhase integer,\n  FlightPlan varchar,\n  FlightPlanActivated varchar,\n  FlightSiteSeeing varchar,\n  FlightType varchar,\n  GearType varchar,\n  LastInspectionDate timestamp,\n  LastInspectionType varchar,\n  Missing varchar,\n  OperationDomestic varchar,\n  OperationScheduled varchar,\n  OperationType varchar,\n  OperatorCertificate varchar,\n  OperatorCertificateNum varchar,\n  OperatorCode varchar,\n  OperatorCountry varchar,\n  OperatorIndividual varchar,\n  OperatorName varchar,\n  OperatorState varchar,\n  Owner varchar,\n  OwnerCertified varchar,\n  OwnerCountry varchar,\n  OwnerState varchar,\n  RegistrationNumber varchar,\n  ReportedToICAO varchar,\n  SeatsCabinCrew integer,\n  SeatsFlightCrew integer,\n  SeatsPassengers integer,\n  SeatsTotal integer,\n  SecondPilot varchar,\n  childsub bigint NOT NULL DEFAULT $i(^Aviation.EventC("Aircraft"))\n);',
    'CREATE TABLE Aviation.Crew (\n  Aircraft varchar NOT NULL,\n  ID varchar NOT NULL,\n  Age integer,\n  AircraftKey integer NOT NULL,\n  Category varchar,\n  CrewNumber integer NOT NULL,\n  EventId varchar NOT NULL,\n  Injury varchar,\n  MedicalCertification varchar,\n  MedicalCertificationDate timestamp,\n  MedicalCertificationValid varchar,\n  Seat varchar,\n  SeatbeltUsed varchar,\n  Sex varchar,\n  ShoulderHarnessUsed varchar,\n  ToxicologyTestPerformed varchar,\n  childsub bigint NOT NULL DEFAULT $i(^Aviation.AircraftC("Crew"))\n);',
    'CREATE TABLE Aviation.Event (\n  ID bigint NOT NULL DEFAULT $i(^Aviation.EventD),\n  AirportDirection integer,\n  AirportDistance varchar,\n  AirportElevation integer,\n  AirportLocation varchar,\n  AirportName varchar,\n  Altimeter varchar,\n  EventDate timestamp,\n  EventId varchar NOT NULL,\n  EventTime integer,\n  FAADistrictOffice varchar,\n  InjuriesGroundFatal integer,\n  InjuriesGroundMinor integer,\n  InjuriesGroundSerious integer,\n  InjuriesHighest varchar,\n  InjuriesTotal integer,\n  InjuriesTotalFatal integer,\n  InjuriesTotalMinor integer,\n  InjuriesTotalNone integer,\n  InjuriesTotalSerious integer,\n  InvestigatingAgency varchar,\n  LightConditions varchar,\n  LocationCity varchar,\n  LocationCoordsLatitude double,\n  LocationCoordsLongitude double,\n  LocationCountry varchar,\n  LocationSiteZipCode varchar,\n  LocationState varchar,\n  MidAir varchar,\n  NTSBId varchar,\n  NarrativeCause varchar,\n  NarrativeFull varchar,\n  NarrativeSummary varchar,\n  OnGroundCollision varchar,\n  SkyConditionCeiling varchar,\n  SkyConditionCeilingHeight integer,\n  SkyConditionNonCeiling varchar,\n  SkyConditionNonCeilingHeight integer,\n  TimeZone varchar,\n  Type varchar,\n  Visibility varchar,\n  WeatherAirTemperature integer,\n  WeatherPrecipitation varchar,\n  WindDirection integer,\n  WindDirectionIndicator varchar,\n  WindGust integer,\n  WindGustIndicator varchar,\n  WindVelocity integer,\n  WindVelocityIndicator varchar\n);'
]

Avec ces définitions de tables, nous pouvons passer à l'étape suivante, qui consiste à les intégrer dans notre invite pour le LLM. Cela permet de s'assurer que le LLM a des renseignements précis et complets sur le schéma de la base de données lorsqu'il génère des requêtes SQL.

Sélection des tables les plus pertinentes

Lorsque vous travaillez avec des bases de données, en particulier les plus grandes, l'envoi du langage de définition des données (DDL) pour toutes les tables d'une invite peut s'avérer peu pratique. Si cette approche peut fonctionner pour les petites bases de données, les bases de données réelles contiennent souvent des centaines ou des milliers de tables, ce qui rend inefficace le traitement de chacune d'entre elles.

De plus, il est peu probable qu'un modèle linguistique ait besoin de connaître toutes les tables de la base de données pour générer efficacement des requêtes SQL. Pour relever ce défi, nous pouvons exploiter les capacités de recherche sémantique pour sélectionner uniquement les tables les plus pertinentes en fonction de la requête de l'utilisateu.

Approche

Nous y parvenons en utilisant la recherche sémantique avec IRIS Vector Search. Notez que cette méthode est plus efficace si les identifiants de vos éléments SQL (tels que les tables, les champs et les clés) ont des noms significatifs. Si vos identifiants sont des codes arbitraires, envisagez plutôt d'utiliser un dictionnaire de données.

Étapes

  1. Récupération des renseignements sur les tables

Commencez par extraire les définitions des tables dans d'un objet DataFrame pandas:

# Récupérer les définitions de tables dans un objet DataFrame pandas
table_def = get_table_definitions_array(cnx=cnx, schema='Aviation')
table_df = pd.DataFrame(data=table_def, columns=["col_def"])
table_df["id"] = table_df.index + 1
table_df

L'objet DataFrame (table_df) ressemblera à ceci:

col_defid
0CREATE TABLE Aviation.Aircraft (\n Event bigi...1
1CREATE TABLE Aviation.Crew (\n Aircraft varch...2
2CREATE TABLE Aviation.Event (\n ID bigint NOT...3
  1. Répartition des définitions dans des documents

Ensuite, répartissez les définitions des tables dans les documents Langchain. . Cette étape est cruciale pour gérer de gros fragment de texte et extraire des incorporations de texte:

loader = DataFrameLoader(table_df, page_content_column="col_def")
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=400, chunk_overlap=20, separator="\n")
tables_docs = text_splitter.split_documents(documents)
tables_docs

La liste tables_docs qui en résulte contient des documents fractionnés avec des métadonnées, comme suit:

[Document(metadata={'id': 1}, page_content='CREATE TABLE Aviation.Aircraft (\n  Event bigint NOT NULL,\n  ID varchar NOT NULL,\n  ...'),
 Document(metadata={'id': 2}, page_content='CREATE TABLE Aviation.Crew (\n  Aircraft varchar NOT NULL,\n  ID varchar NOT NULL,\n  ...'),
 Document(metadata={'id': 3}, page_content='CREATE TABLE Aviation.Event (\n  ID bigint NOT NULL DEFAULT $i(^Aviation.EventD),\n  ...')]
  1. Extraction des incorporations et stockage dans IRIS

Utilisez maintenant la classe IRISVector de langchain-iris pour extraire les vecteurs d'intégration et les stocker:

tables_vector_store = IRISVector.from_documents(
    embedding=OpenAIEmbeddings(), 
    documents=tables_docs,
    connection_string=iris_conn_str,
    collection_name="sql_tables",
    pre_delete_collection=True
)

Remarque : l'indicateur pre_delete_collection est fixé à True (vrai) à des fins de démonstration, afin de garantir une nouvelle collection à chaque exécution du test. Dans un environnement de production, cet indicateur doit généralement être défini sur False (faux).

  1. Recherche de documents pertinents Avec les incorporations de table stockées, vous pouvez désormais interroger les tables pertinentes en fonction des données de l'utilisateur:
input_query = "List the first 2 manufacturers"
relevant_tables_docs = tables_vector_store.similarity_search(input_query, k=3)
relevant_tables_docs

Par exemple, une requête portant sur les fabricants peut aboutir à un résultat:

[Document(metadata={'id': 1}, page_content='GearType varchar,\n  LastInspectionDate timestamp,\n  ...'),
 Document(metadata={'id': 1}, page_content='AircraftModel varchar,\n  AircraftRegistrationClass varchar,\n  ...'),
 Document(metadata={'id': 3}, page_content='LocationSiteZipCode varchar,\n  LocationState varchar,\n  ...')]

À partir des métadonnées, vous pouvez voir que seul la table ID 1 (Aviation.Avion) est pertinente, ce qui correspond à la requête.

  1. Gestion des cas limites

Bien que cette approche soit généralement efficace, elle n'est pas toujours parfaite. Par exemple, la recherche de sites d'accidents peut également renvoyer des tables moins pertinentes:

input_query = "List the top 10 most crash sites"
relevant_tables_docs = tables_vector_store.similarity_search(input_query, k=3)
relevant_tables_docs

Les résultats peuvent inclure ce qui suit:

[Document(metadata={'id': 3}, page_content='LocationSiteZipCode varchar,\n  LocationState varchar,\n  ...'),
 Document(metadata={'id': 3}, page_content='InjuriesGroundSerious integer,\n  InjuriesHighest varchar,\n  ...'),
 Document(metadata={'id': 1}, page_content='CREATE TABLE Aviation.Aircraft (\n  Event bigint NOT NULL,\n  ID varchar NOT NULL,\n  ...')]

Bien que la table Aviation.Event ait été récupérée deux fois, la table Aviation.Aircraft peut également apparaître, ce qui pourrait être amélioré par un filtrage ou un seuillage supplémentaire. Cela dépasse le cadre de cet exemple et sera laissé à l'appréciation de futures implémentations.

  1. Définition d'une fonction pour récupérer les tables pertinentes

Pour automatiser ce processus, définissez une fonction qui filtre et renvoie les tableaux pertinents en fonction des données de l'utilisateur:

def get_relevant_tables(user_input, tables_vector_store, table_df):
    relevant_tables_docs = tables_vector_store.similarity_search(user_input)
    relevant_tables_docs_indices = [x.metadata["id"] for x in relevant_tables_docs]
    indices = table_df["id"].isin(relevant_tables_docs_indices)
    relevant_tables_array = [x for x in table_df[indices]["col_def"]]
    return relevant_tables_array

Cette fonction permet d'extraire efficacement les tables pertinentes à envoyer au LLM, de réduire la longueur de l'invite et d'améliorer les performances globales de la requête.

Sélection des exemples les plus pertinents ("invitation en quelques coups")

Lorsque vous travaillez avec des modèles linguistiques (LLM), le fait de leur fournir des exemples pertinents permet d'obtenir des réponses précises et adaptées au contexte. Ces exemples, appelés "quelques exemples", guident le LLM dans la compréhension de la structure et du contexte des requêtes qu'il doit traiter.

Dans notre cas, nous devons remplir la variable examples_value avec un ensemble varié de requêtes SQL qui couvrent un large spectre de la syntaxe SQL d'IRIS et des tables disponibles dans la base de données. Cela permet d'éviter que le LLM ne génère des requêtes incorrectes ou non pertinentes.

Définition de requêtes d'exemple

Vous trouverez ci-dessous une liste d'exemples de requêtes conçues pour illustrer diverses opérations SQL:

examples = [
    {"input": "List all aircrafts.", "query": "SELECT * FROM Aviation.Aircraft"},
    {"input": "Find all incidents for the aircraft with ID 'N12345'.", "query": "SELECT * FROM Aviation.Event WHERE EventId IN (SELECT EventId FROM Aviation.Aircraft WHERE ID = 'N12345')"},
    {"input": "List all incidents in the 'Commercial' operation type.", "query": "SELECT * FROM Aviation.Event WHERE EventId IN (SELECT EventId FROM Aviation.Aircraft WHERE OperationType = 'Commercial')"},
    {"input": "Find the total number of incidents.", "query": "SELECT COUNT(*) FROM Aviation.Event"},
    {"input": "List all incidents that occurred in 'Canada'.", "query": "SELECT * FROM Aviation.Event WHERE LocationCountry = 'Canada'"},
    {"input": "How many incidents are associated with the aircraft with AircraftKey 5?", "query": "SELECT COUNT(*) FROM Aviation.Aircraft WHERE AircraftKey = 5"},
    {"input": "Find the total number of distinct aircrafts involved in incidents.", "query": "SELECT COUNT(DISTINCT AircraftKey) FROM Aviation.Aircraft"},
    {"input": "List all incidents that occurred after 5 PM.", "query": "SELECT * FROM Aviation.Event WHERE EventTime > 1700"},
    {"input": "Who are the top 5 operators by the number of incidents?", "query": "SELECT TOP 5 OperatorName, COUNT(*) AS IncidentCount FROM Aviation.Aircraft GROUP BY OperatorName ORDER BY IncidentCount DESC"},
    {"input": "Which incidents occurred in the year 2020?", "query": "SELECT * FROM Aviation.Event WHERE YEAR(EventDate) = '2020'"},
    {"input": "What was the month with most events in the year 2020?", "query": "SELECT TOP 1 MONTH(EventDate) EventMonth, COUNT(*) EventCount FROM Aviation.Event WHERE YEAR(EventDate) = '2020' GROUP BY MONTH(EventDate) ORDER BY EventCount DESC"},
    {"input": "How many crew members were involved in incidents?", "query": "SELECT COUNT(*) FROM Aviation.Crew"},
    {"input": "List all incidents with detailed aircraft information for incidents that occurred in the year 2012.", "query": "SELECT e.EventId, e.EventDate, a.AircraftManufacturer, a.AircraftModel, a.AircraftCategory FROM Aviation.Event e JOIN Aviation.Aircraft a ON e.EventId = a.EventId WHERE Year(e.EventDate) = 2012"},
    {"input": "Find all incidents where there were more than 5 injuries and include the aircraft manufacturer and model.", "query": "SELECT e.EventId, e.InjuriesTotal, a.AircraftManufacturer, a.AircraftModel FROM Aviation.Event e JOIN Aviation.Aircraft a ON e.EventId = a.EventId WHERE e.InjuriesTotal > 5"},
    {"input": "List all crew members involved in incidents with serious injuries, along with the incident date and location.", "query": "SELECT c.CrewNumber AS 'Crew Number', c.Age, c.Sex AS Gender, e.EventDate AS 'Event Date', e.LocationCity AS 'Location City', e.LocationState AS 'Location State' FROM Aviation.Crew c JOIN Aviation.Event e ON c.EventId = e.EventId WHERE c.Injury = 'Serious'"}
]

Sélection d'exemples pertinents

Compte tenu de la liste d'exemples qui ne cesse de s'allonger, il n'est pas pratique de fournir tous les exemples au LLM. Au lieu de cela, nous utilisons la recherche vectorielle d'IRIS et la classe SemanticSimilarityExampleSelector pour identifier les exemples les plus pertinents sur la base des invites de l'utilisateur.

Définition du Sélecteur d'exemples:

example_selector = SemanticSimilarityExampleSelector.from_examples(
    examples,
    OpenAIEmbeddings(),
    IRISVector,
    k=5,
    input_keys=["input"],
    connection_string=iris_conn_str,
    collection_name="sql_samples",
    pre_delete_collection=True
)

Remarque : l'indicateur pre_delete_collection est fixé ici à des fins de démonstration, afin de garantir une nouvelle collection à chaque exécution du test. Dans un environnement de production, cet indicateur doit être défini sur Faux (false) pour éviter les suppressions inutiles.

Requéte du sélecteur:

Pour trouver les exemples les plus pertinents pour une saisie donnée, utilisez le sélecteur comme suit:

input_query = "Find all events in 2010 informing the Event Id and date, location city and state, aircraft manufacturer and model."
relevant_examples = example_selector.select_examples({"input": input_query})

Les résultats pourraient ressembler à ceci:

[{'input': 'List all incidents with detailed aircraft information for incidents that occurred in the year 2012.', 'query': 'SELECT e.EventId, e.EventDate, a.AircraftManufacturer, a.AircraftModel, a.AircraftCategory FROM Aviation.Event e JOIN Aviation.Aircraft a ON e.EventId = a.EventId WHERE Year(e.EventDate) = 2012'},
 {'input': "Find all incidents for the aircraft with ID 'N12345'.", 'query': "SELECT * FROM Aviation.Event WHERE EventId IN (SELECT EventId FROM Aviation.Aircraft WHERE ID = 'N12345')"},
 {'input': 'Find all incidents where there were more than 5 injuries and include the aircraft manufacturer and model.', 'query': 'SELECT e.EventId, e.InjuriesTotal, a.AircraftManufacturer, a.AircraftModel FROM Aviation.Event e JOIN Aviation.Aircraft a ON e.EventId = a.EventId WHERE e.InjuriesTotal > 5'},
 {'input': 'List all aircrafts.', 'query': 'SELECT * FROM Aviation.Aircraft'},
 {'input': 'Find the total number of distinct aircrafts involved in incidents.', 'query': 'SELECT COUNT(DISTINCT AircraftKey) FROM Aviation.Aircraft'}]

Si vous avez spécifiquement besoin d'exemples liés aux quantités, vous pouvez interroger le sélecteur en conséquence:

input_query = "What is the number of incidents involving Boeing aircraft."
quantity_examples = example_selector.select_examples({"input": input_query})

Le résultat peut être comme suit:

[{'input': 'How many incidents are associated with the aircraft with AircraftKey 5?', 'query': 'SELECT COUNT(*) FROM Aviation.Aircraft WHERE AircraftKey = 5'},
 {'input': 'Find the total number of distinct aircrafts involved in incidents.', 'query': 'SELECT COUNT(DISTINCT AircraftKey) FROM Aviation.Aircraft'},
 {'input': 'How many crew members were involved in incidents?', 'query': 'SELECT COUNT(*) FROM Aviation.Crew'},
 {'input': 'Find all incidents where there were more than 5 injuries and include the aircraft manufacturer and model.', 'query': 'SELECT e.EventId, e.InjuriesTotal, a.AircraftManufacturer, a.AircraftModel FROM Aviation.Event e JOIN Aviation.Aircraft a ON e.EventId = a.EventId WHERE e.InjuriesTotal > 5'},
 {'input': 'List all incidents with detailed aircraft information for incidents that occurred in the year 2012.', 'query': 'SELECT e.EventId, e.EventDate, a.AircraftManufacturer, a.AircraftModel, a.AircraftCategory FROM Aviation.Event e JOIN Aviation.Aircraft a ON e.EventId = a.EventId WHERE Year(e.EventDate) = 2012'}]

Ce résultat comprend des exemples qui traitent spécifiquement du comptage et des quantités.

Considérations futures

Bien que le sélecteur SemanticSimilarityExampleSelector soit puissant, il est important de noter que tous les exemples sélectionnés ne sont pas forcément parfaits. Les améliorations futures peuvent impliquer l'ajout de filtres ou de seuils pour exclure les résultats moins pertinents, garantissant que seuls les exemples les plus appropriés sont fournis au LLM.

Test de précision

Pour évaluer les performances de l'invite et de la génération de requêtes SQL, nous devons mettre en place et exécuter une série de tests. L'objectif est d'évaluer dans quelle mesure le LLM génère des requêtes SQL basées sur les données de l'utilisateur, avec et sans l'utilisation de quelques coups basés sur des exemples.

Fonction de génération de requêtes SQL

Nous commençons par définir une fonction qui utilise le LLM pour générer des requêtes SQL en fonction du contexte fourni, de l'invite, de la saisie de l'utilisateur et d'autres paramètres:

def get_sql_from_text(context, prompt, user_input, use_few_shots, tables_vector_store, table_df, example_selector=None, example_prompt=None):
    relevant_tables = get_relevant_tables(user_input, tables_vector_store, table_df)
    context["table_info"] = "\n\n".join(relevant_tables)

    examples = example_selector.select_examples({"input": user_input}) if example_selector else []
    context["examples_value"] = "\n\n".join([
        example_prompt.invoke(x).to_string() for x in examples
    ])
    
    model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
    output_parser = StrOutputParser()
    chain_model = prompt | model | output_parser
    
    response = chain_model.invoke({
        "top_k": context["top_k"],
        "table_info": context["table_info"],
        "examples_value": context["examples_value"],
        "input": user_input
    })
    return response

Nous commençons par définir une fonction qui utilise le LLM pour générer des requêtes SQL en fonction du contexte fourni, de l'invite, de la saisie de l'utilisateur et d'autres paramètres

Testez l'invite avec et sans exemples:

# Exécution de l'invite **avec** quelques coups
input = "Find all events in 2010 informing the Event Id and date, location city and state, aircraft manufacturer and model."
response_with_few_shots = get_sql_from_text(
    context, 
    prompt, 
    user_input=input, 
    use_few_shots=True, 
    tables_vector_store=tables_vector_store, 
    table_df=table_df,
    example_selector=example_selector, 
    example_prompt=example_prompt,
)
print(response_with_few_shots)
SELECT e.EventId, e.EventDate, e.LocationCity, e.LocationState, a.AircraftManufacturer, a.AircraftModel
FROM Aviation.Event e
JOIN Aviation.Aircraft a ON e.EventId = a.EventId
WHERE Year(e.EventDate) = 2010
# Exécution de l'invite **sans** quelques coups
input = "Find all events in 2010 informing the Event Id and date, location city and state, aircraft manufacturer and model."
response_with_no_few_shots = get_sql_from_text(
    context, 
    prompt, 
    user_input=input, 
    use_few_shots=False, 
    tables_vector_store=tables_vector_store, 
    table_df=table_df,
)
print(response_with_no_few_shots)
SELECT TOP 3 "EventId", "EventDate", "LocationCity", "LocationState", "AircraftManufacturer", "AircraftModel"
FROM Aviation.Event e
JOIN Aviation.Aircraft a ON e.ID = a.Event
WHERE e.EventDate >= '2010-01-01' AND e.EventDate < '2011-01-01'
Utility Functions for Testing

Pour tester les requêtes SQL générées, nous définissons quelques fonctions utilitaires:

def execute_sql_query(cnx, query):
    try:
        cursor = cnx.cursor()
        cursor.execute(query)
        rows = cursor.fetchall()
        return rows
    except:
        print('Error running query:')
        print(query)
        print('-'*80)
    return None

def sql_result_equals(cnx, query, expected):
    rows = execute_sql_query(cnx, query)
    result = [set(row._asdict().values()) for row in rows or []]
    if result != expected and rows is not None:
        print('Result not as expected for query:')
        print(query)
        print('-'*80)
    return result == expected
# Test SQL pour l'invite **avec** quelques coups
print("SQL is OK" if not execute_sql_query(cnx, response_with_few_shots) is None else "SQL is not OK")
    SQL is OK
# Test SQL pour l'invite **sans** quelques coups
print("SQL is OK" if not execute_sql_query(cnx, response_with_no_few_shots) is None else "SQL is not OK")
    error on running query: 
    SELECT TOP 3 "EventId", "EventDate", "LocationCity", "LocationState", "AircraftManufacturer", "AircraftModel"
    FROM Aviation.Event e
    JOIN Aviation.Aircraft a ON e.ID = a.Event
    WHERE e.EventDate >= '2010-01-01' AND e.EventDate < '2011-01-01'
    --------------------------------------------------------------------------------
    SQL is not OK

Définition et exécution des tests

Définissez un ensemble de scénarios de test et les exécutez:

tests = [{
    "input": "What were the top 3 years with the most recorded events?",
    "expected": [{128, 2003}, {122, 2007}, {117, 2005}]
},{
    "input": "How many incidents involving Boeing aircraft.",
    "expected": [{5}]
},{
    "input": "How many incidents that resulted in fatalities.",
    "expected": [{237}]
},{
    "input": "List event Id and date and, crew number, age and gender for incidents that occurred in 2013.",
    "expected": [{1, datetime.datetime(2013, 3, 4, 11, 6), '20130305X71252', 59, 'M'},
                 {1, datetime.datetime(2013, 1, 1, 15, 0), '20130101X94035', 32, 'M'},
                 {2, datetime.datetime(2013, 1, 1, 15, 0), '20130101X94035', 35, 'M'},
                 {1, datetime.datetime(2013, 1, 12, 15, 0), '20130113X42535', 25, 'M'},
                 {2, datetime.datetime(2013, 1, 12, 15, 0), '20130113X42535', 34, 'M'},
                 {1, datetime.datetime(2013, 2, 1, 15, 0), '20130203X53401', 29, 'M'},
                 {1, datetime.datetime(2013, 2, 15, 15, 0), '20130218X70747', 27, 'M'},
                 {1, datetime.datetime(2013, 3, 2, 15, 0), '20130303X21011', 49, 'M'},
                 {1, datetime.datetime(2013, 3, 23, 13, 52), '20130326X85150', 'M', None}]
},{
    "input": "Find the total number of incidents that occurred in the United States.",
    "expected": [{1178}]
},{
    "input": "List all incidents latitude and longitude coordinates with more than 5 injuries that occurred in 2010.",
    "expected": [{-78.76833333333333, 43.25277777777778}]
},{
    "input": "Find all incidents in 2010 informing the Event Id and date, location city and state, aircraft manufacturer and model.",
    "expected": [
        {datetime.datetime(2010, 5, 20, 13, 43), '20100520X60222', 'CIRRUS DESIGN CORP', 'Farmingdale', 'New York', 'SR22'},
        {datetime.datetime(2010, 4, 11, 15, 0), '20100411X73253', 'CZECH AIRCRAFT WORKS SPOL SRO', 'Millbrook', 'New York', 'SPORTCRUISER'},
        {'108', datetime.datetime(2010, 1, 9, 12, 55), '20100111X41106', 'Bayport', 'New York', 'STINSON'},
        {datetime.datetime(2010, 8, 1, 14, 20), '20100801X85218', 'A185F', 'CESSNA', 'New York', 'Newfane'}
    ]
}]

Évaluation de la précision

Exécutez les tests et calculez la précision:

def execute_tests(cnx, context, prompt, use_few_shots, tables_vector_store, table_df, example_selector, example_prompt):
    tests_generated_sql = [(x, get_sql_from_text(
            context, 
            prompt, 
            user_input=x['input'], 
            use_few_shots=use_few_shots, 
            tables_vector_store=tables_vector_store, 
            table_df=table_df,
            example_selector=example_selector if use_few_shots else None, 
            example_prompt=example_prompt if use_few_shots else None,
        )) for x in deepcopy(tests)]
    
    tests_sql_executions = [(x[0], sql_result_equals(cnx, x[1], x[0]['expected'])) 
                            for x in tests_generated_sql]
    
    accuracy = sum(1 for i in tests_sql_executions if i[1] == True) / len(tests_sql_executions)
    print(f'Accuracy: {accuracy}')
    print('-'*80)

Résultats

# Tests de précision pour les invites exécutées **sans** quelques coups
use_few_shots = False
execute_tests(
    cnx,
    context, 
    prompt, 
    use_few_shots, 
    tables_vector_store, 
    table_df, 
    example_selector, 
    example_prompt
)
    error on running query: 
    SELECT "EventDate", COUNT("EventId") as "TotalEvents"
    FROM Aviation.Event
    GROUP BY "EventDate"
    ORDER BY "TotalEvents" DESC
    TOP 3;
    --------------------------------------------------------------------------------
    error on running query: 
    SELECT "EventId", "EventDate", "C"."CrewNumber", "C"."Age", "C"."Sex"
    FROM "Aviation.Event" AS "E"
    JOIN "Aviation.Crew" AS "C" ON "E"."ID" = "C"."EventId"
    WHERE "E"."EventDate" >= '2013-01-01' AND "E"."EventDate" < '2014-01-01'
    --------------------------------------------------------------------------------
    result not expected for query: 
    SELECT TOP 3 "e"."EventId", "e"."EventDate", "e"."LocationCity", "e"."LocationState", "a"."AircraftManufacturer", "a"."AircraftModel"
    FROM "Aviation"."Event" AS "e"
    JOIN "Aviation"."Aircraft" AS "a" ON "e"."ID" = "a"."Event"
    WHERE "e"."EventDate" >= '2010-01-01' AND "e"."EventDate" < '2011-01-01'
    --------------------------------------------------------------------------------
    accuracy: 0.5714285714285714
    --------------------------------------------------------------------------------
# Tests de précision pour les invites exécutées **avec** quelques coups
use_few_shots = True
execute_tests(
    cnx,
    context, 
    prompt, 
    use_few_shots, 
    tables_vector_store, 
    table_df, 
    example_selector, 
    example_prompt
)
    error on running query: 
    SELECT e.EventId, e.EventDate, e.LocationCity, e.LocationState, a.AircraftManufacturer, a.AircraftModel
    FROM Aviation.Event e
    JOIN Aviation.Aircraft a ON e.EventId = a.EventId
    WHERE Year(e.EventDate) = 2010 TOP 3
    --------------------------------------------------------------------------------
    accuracy: 0.8571428571428571
    --------------------------------------------------------------------------------

Conclusion

La précision des requêtes SQL générées avec des exemples (quelques coups) est environ 49% plus élevée que celles générées sans exemples (85% contre 57%).

Références

0
0 52
InterSystems officiel Adeline Icard · Oct 3, 2024

Nous avons récemment mis à disposition une nouvelle version d'InterSystems IRIS dans le cadre du programme d'accès anticipé à la recherche vectorielle, comprenant un nouvel index Approximate Nearest Neighbor basé sur l'algorithme d'indexation Hierarchical Navigable Small World (HNSW). Cet ajout permet des recherches de voisins les plus proches très efficaces et approximatives sur de grands ensembles de données vectorielles, améliorant considérablement les performances et l'évolutivité des requêtes.

0
0 43
Article Lorenzo Scalese · Août 22, 2024 5m read

Dans l'article précédent, nous avons présenté l'application d[IA]gnosis développée pour soutenir le codage des diagnostics CIM-10. Dans le présent article, nous verrons comment InterSystems IRIS for Health nous fournit les outils nécessaires à la génération de vecteurs à partir de la liste des codes CIM-10 au moyen d'un modèle de langage pré-entraîné, à leur stockage et à la recherche ultérieure de similitudes sur tous ces vecteurs générés.

Introduction

L'une des principales fonctionnalités apparues avec le développement des modèles d'IA est ce que nous appelons RAG (Retrieval-Augmented Generation), qui nous permet d'améliorer les résultats des modèles LLM en incorporant un contexte au modèle. Dans notre exemple, le contexte est donné par l'ensemble des diagnostics CIM-10, et pour les utiliser, nous devons d'abord les vectoriser.

Comment vectoriser notre liste de diagnostics?

SentenceTransformers et Embedded Python

Pour la génération de vecteurs, nous avons utilisé la bibliothèque Python SentenceTransformers qui facilite grandement la vectorisation de texte libre à partir de modèles pré-entraînés. Extrait de leur propre site web:

Le module "Sentence Transformers" (alias SBERT) SBERT) est un module Python intégré qui permet d'accéder, d'utiliser et d'entraîner des modèles incorporés de texte et d'image à la pointe de la technologie. Il peut être utilisé pour calculer des embeddings à l'aide de modèles Sentence Transformer (quickstart) ou pour calculer des scores de similarité à l'aide de modèles Cross-Encoder (quickstart). Cela ouvre la voie à un large éventail d'applications, notamment la  recherche sémantique, la  similarité textuelle sémantique, et l' extraction de paraphrases.

Parmi tous les modèles développés par la communauté SentenceTransformers, nous avons trouvé BioLORD-2023-M, un modèle pré-entraîné qui génère des vecteurs de 786 dimensions.

Ce modèle a été entraîné à l'aide de BioLORD, une nouvelle stratégie de pré-entraînement visant à produire des représentations significatives pour les expressions cliniques et les concepts biomédicaux.

Les méthodologies de pointe maximisent la similarité de la représentation des noms se référant au même concept et évitent l'effondrement grâce à l'apprentissage contrastif. Cependant, les noms biomédicaux n'étant pas toujours explicites, il en résulte parfois des représentations non sémantiques.

BioLORD résout ce problème en fondant ses représentations de concepts sur des définitions, ainsi que sur de courtes descriptions dérivées d'un graphe de connaissances multirelationnel composé d'ontologies biomédicales. Grâce à cette base, notre modèle produit des représentations de concepts plus sémantiques qui correspondent mieux à la structure hiérarchique des ontologies. BioLORD-2023 établit un nouvel état de l'art en matière de similarité textuelle pour les expressions cliniques (MedSTS) et les concepts biomédicaux (EHR-Rel-B).

Comme vous pouvez le voir dans sa propre définition, ce modèle est pré-entraîné avec des concepts médicaux qui seront utiles lors de la vectorisation de nos codes ICD-10 et du texte brut.

Pour notre projet, nous téléchargerons ce modèle afin d'accélérer la création des vecteurs:

if not os.path.isdir('/shared/model/'):
    model = sentence_transformers.SentenceTransformer('FremyCompany/BioLORD-2023-M')            
    model.save('/shared/model/')

Lorsque nous sommes à notre ordinateur, nous pouvons introduire les textes à vectoriser dans des listes afin d'accélérer le processus. Voyons comment vectoriser les codes CIM-10 que nous avons précédemment enregistrés dans notre classe ENCODER.Object.Codes.

st = iris.sql.prepare("SELECT TOP 50 CodeId, Description FROM ENCODER_Object.Codes WHERE VectorDescription is null ORDER BY ID ASC ")
resultSet = st.execute()
df = resultSet.dataframe()

if (df.size > 0): model = sentence_transformers.SentenceTransformer("/shared/model/") embeddings = model.encode(df['description'].tolist(), normalize_embeddings=True)

df[<span class="hljs-string">'vectordescription'</span>] = embeddings.tolist()

stmt = iris.sql.prepare(<span class="hljs-string">"UPDATE ENCODER_Object.Codes SET VectorDescription = TO_VECTOR(?,DECIMAL) WHERE CodeId = ?"</span>)
<span class="hljs-keyword">for</span> index, row <span class="hljs-keyword">in</span> df.iterrows():
    rs = stmt.execute(str(row[<span class="hljs-string">'vectordescription'</span>]), row[<span class="hljs-string">'codeid'</span>])

else: flagLoop = False

Comme vous pouvez le voir, nous extrayons d'abord les codes stockés dans notre table de codes CIM-10 que nous n'avons pas encore vectorisés mais que nous avons enregistrés dans une étape précédente après les avoir extraits du fichier CSV, puis nous extrayons la liste des descriptions à vectoriser et en utilisant la bibliothèque Python sentence_transformers nous allons récupérer notre modèle et générer les embeddings associés.

Enfin, nous mettons à jour le code CIM-10 avec la description vectorisée en exécutant la commande UPDATE. Comme vous pouvez le voir, le résultat retourné par le modèle est vectorisé par la commande TO_VECTOR de SQL dans IRIS.

Utilisation dans IRIS

Très bien, nous avons déjà notre code Python, il nous suffit donc de l'inclure dans une classe qui étend Ens.BusinessProcess et de l'inclure dans notre production, puis de le connecter au Business Service chargé de récupérer le fichier CSV et le tour est joué!

Voyons à quoi ressemblera ce code dans notre production:

Comme vous pouvez le voir, nous avons notre service d'entreprise avec l'adaptateur EnsLib.File.InboundAdapter qui nous permettra de collecter le fichier de code et de le rediriger vers notre processus d'entreprise dans lequel nous effectuerons toutes les opérations de vectorisation et de stockage, ce qui se traduira par un ensemble d'enregistrements comme le suivant:

Notre application est maintenant prête à rechercher des correspondances possibles avec les textes que nous lui transmettons!

Dans le prochain article...

Dans le prochain article, nous montrerons comment le front-end de l'application développée en Angular 17 est intégré à notre production dans IRIS for Health et comment IRIS reçoit les textes à analyser, les vectorise et recherche des similitudes dans la table des codes CIM-10.

À ne pas manquer!

0
1 48
Article Lorenzo Scalese · Août 20, 2024 8m read

Avec l'introduction des types de données vectorielles et de la fonctionnalité de recherche vectorielle dans IRIS, tout un univers de possibilités de développement d'applications s'ouvre et un exemple de ces applications est celui que j'ai récemment vu publié dans un appel d'offres public du Ministère régional de la santé de Valence demandant un outil d'aide au codage de la CIM-10 à l'aide de modèles d'IA.

Comment pourrions-nous mettre en œuvre une application similaire à celle demandée? Voyons ce dont nous aurions besoin:

  1. Liste des codes CIM-10, que nous utiliserons comme contexte de notre application RAG pour rechercher des diagnostics dans les textes bruts.
  2. Un modèle entraîné pour vectoriser les textes dans lesquels nous allons rechercher des équivalences dans les codes CIM-10.
  3. Les bibliothèques Python nécessaires à l'ingestion et à la vectorisation des codes CIM-10 et des textes.
  4. Un front-end convivial qui prend en charge les textes sur lesquels nous recherchons des diagnostics possibles.
  5. L'orchestration des requêtes reçues du front-end.

Que propose IRIS pour répondre à ces besoins?

  1. Importation CSV, soit en utilisant la fonctionnalité RecordMapper, soit directement en utilisant Embedded Python.
  2. Embedded Python nous permet d'implémenter le code Python nécessaire pour générer les vecteurs à l'aide du modèle sélectionné.
  3. Publication d'API REST à invoquer à partir de l'application front-end.
  4. Les productions d'interopérabilité qui permettent le suivi des informations au sein d'IRIS.

Il ne reste plus qu'à voir l'exemple développé:

d[IA]gnosis

Associé à cet article vous avez accès à l'application développée, les prochains articles présenteront en détail la mise en œuvre de chacune des fonctionnalités, de l'utilisation du modèle au stockage des vecteurs, en passant par l'utilisation des recherches vectorielles.

Passons en revue l'application:

Importation des codes CIM-10

L'écran de configuration indique le format que doit suivre le fichier CSV contenant les codes CIE-10 que nous allons importer. Le processus de chargement et de vectorisation consomme beaucoup de temps et de ressources, c'est pourquoi le déploiement du conteneur Docker configure non seulement la mémoire RAM utilisable par Docker mais aussi la mémoire disque au cas où les besoins dépasseraient la RAM allouée:

# iris  iris:    init:true    container_name:iris    build:      context:.      dockerfile:iris/Dockerfile    ports:      -52774:52773      -51774:1972    volumes:    -./shared:/shared    environment:    -ISC_DATA_DIRECTORY=/shared/durable    command:--check-capsfalse--ISCAgentfalse    mem_limit:30G    memswap_limit:32G

Le fichier contenant les codes ICD-10 est disponible dans le chemin du projet /shared/cie10/icd10.csv, une fois que 100% est atteint, l'application sera prête à être utilisée.

Dans notre application, nous avons défini deux fonctionnalités différentes pour le codage des diagnostics, l'une basée sur les messages HL7 reçus dans le système et l'autre basée sur des textes bruts.

Saisie des diagnostics via HL7

Le projet contient une série de messages HL7 prêts à être testés, il suffit de copier le fichier /shared/hl7/messagesa01_en.hl7 dans le dossier /shared/HL7In et la production associée en extraira le diagnostic pour l'afficher dans l'application web:

L'écran de demande de diagnostic permet de voir tous les diagnostics reçus via la messagerie HL7. Pour leur codage CIM-10, il suffit de cliquer sur la loupe pour afficher une liste des codes CIM-10 les plus proches du diagnostic reçu:

Une fois sélectionné, le diagnostic et le code CIM-10 associé apparaissent dans la liste. En cliquant sur le bouton avec l'icône de l'enveloppe, un message est généré en utilisant l'original et en incluant le nouveau code sélectionné dans le segment du diagnostic:

MSH|^~\&|HIS|HULP|EMPI||||ADT^A08|592956|P|2.5.1
EVN|A01|
PID|||1556655212^^^SERMAS^SN~922210^^^HULP^PI||GARCÍA PÉREZ^JUAN^^^||20150403|M|||PASEO PEDRO ÁLVAREZ 1951 CENTRO^^LEGANÉS^MADRID^28379^SPAIN||555283055^PRN^^JUAN.GARCIA@YAHOO.COM|||||||||||||||||N|
PV1||N
DG1|1||O10.91^Hypertension préexistante non spécifiée compliquant la grossesse^CIE10-ES|Hypertension gestationnelle||A||

Ce message se trouve dans le chemin /shared/HL7Out

Captures d'écran de diagnostic en texte brut

Dans l'option Analyseur de texte, l'utilisateur peut inclure un texte brut sur lequel un processus d'analyse sera effectué. L'application recherchera des tuples de 3 mots lemmatisés (en éliminant les articles, les pronoms et d'autres mots peu pertinents). Une fois analysé, le système affichera le texte pertinent souligné et les diagnostics possibles localisés:

Une fois l'analyse effectuée, elle peut être consultée à tout moment à partir de l'historique de l'analyse.

Historique des analyses

Toutes les analyses effectuées sont enregistrées et peuvent être consultées à tout moment, en visualisant tous les codes CIM-10 possibles:

Dans le prochain article...

Nous verrons comment, en utilisant Embedded Python, nous utilisons un modèle LLM spécifique pour la vectorisation des codes CIM-10 qui nous serviront de contexte et des textes bruts.

Si vous avez des questions ou des suggestions, n'hésitez pas à écrire un commentaire dans l'article.

Avec l'introduction des types de données vectorielles et de la fonctionnalité de recherche vectorielle dans IRIS, tout un univers de possibilités de développement d'applications s'ouvre et un exemple de ces applications est celui que j'ai récemment vu publié dans un appel d'offres public du Ministère régional de la santé de Valence demandant un outil d'aide au codage de la CIM-10 à l'aide de modèles d'IA.

Comment pourrions-nous mettre en œuvre une application similaire à celle demandée? Voyons ce dont nous aurions besoin:

  1. Liste des codes CIM-10, que nous utiliserons comme contexte de notre application RAG pour rechercher des diagnostics dans les textes bruts.
  2. Un modèle entraîné pour vectoriser les textes dans lesquels nous allons rechercher des équivalences dans les codes CIM-10.
  3. Les bibliothèques Python nécessaires à l'ingestion et à la vectorisation des codes CIM-10 et des textes.
  4. Un front-end convivial qui prend en charge les textes sur lesquels nous recherchons des diagnostics possibles.
  5. L'orchestration des requêtes reçues du front-end.

Que propose IRIS pour répondre à ces besoins?

  1. Importation CSV, soit en utilisant la fonctionnalité RecordMapper, soit directement en utilisant Embedded Python.
  2. Embedded Python nous permet d'implémenter le code Python nécessaire pour générer les vecteurs à l'aide du modèle sélectionné.
  3. Publication d'API REST à invoquer à partir de l'application front-end.
  4. Les productions d'interopérabilité qui permettent le suivi des informations au sein d'IRIS.

Il ne reste plus qu'à voir l'exemple développé:

d[IA]gnosis

Associé à cet article vous avez accès à l'application développée, les prochains articles présenteront en détail la mise en œuvre de chacune des fonctionnalités, de l'utilisation du modèle au stockage des vecteurs, en passant par l'utilisation des recherches vectorielles.

Passons en revue l'application:

Importation des codes CIM-10

L'écran de configuration indique le format que doit suivre le fichier CSV contenant les codes CIE-10 que nous allons importer. Le processus de chargement et de vectorisation consomme beaucoup de temps et de ressources, c'est pourquoi le déploiement du conteneur Docker configure non seulement la mémoire RAM utilisable par Docker mais aussi la mémoire disque au cas où les besoins dépasseraient la RAM allouée:

# iris  iris:    init:true    container_name:iris    build:      context:.      dockerfile:iris/Dockerfile    ports:      -52774:52773      -51774:1972    volumes:    -./shared:/shared    environment:    -ISC_DATA_DIRECTORY=/shared/durable    command:--check-capsfalse--ISCAgentfalse    mem_limit:30G    memswap_limit:32G

Le fichier contenant les codes ICD-10 est disponible dans le chemin du projet /shared/cie10/icd10.csv, une fois que 100% est atteint, l'application sera prête à être utilisée.

Dans notre application, nous avons défini deux fonctionnalités différentes pour le codage des diagnostics, l'une basée sur les messages HL7 reçus dans le système et l'autre basée sur des textes bruts.

Saisie des diagnostics via HL7

Le projet contient une série de messages HL7 prêts à être testés, il suffit de copier le fichier /shared/hl7/messagesa01_en.hl7 dans le dossier /shared/HL7In et la production associée en extraira le diagnostic pour l'afficher dans l'application web:

L'écran de demande de diagnostic permet de voir tous les diagnostics reçus via la messagerie HL7. Pour leur codage CIM-10, il suffit de cliquer sur la loupe pour afficher une liste des codes CIM-10 les plus proches du diagnostic reçu:

Une fois sélectionné, le diagnostic et le code CIM-10 associé apparaissent dans la liste. En cliquant sur le bouton avec l'icône de l'enveloppe, un message est généré en utilisant l'original et en incluant le nouveau code sélectionné dans le segment du diagnostic:

MSH|^~\&|HIS|HULP|EMPI||||ADT^A08|592956|P|2.5.1
EVN|A01|
PID|||1556655212^^^SERMAS^SN~922210^^^HULP^PI||GARCÍA PÉREZ^JUAN^^^||20150403|M|||PASEO PEDRO ÁLVAREZ 1951 CENTRO^^LEGANÉS^MADRID^28379^SPAIN||555283055^PRN^^JUAN.GARCIA@YAHOO.COM|||||||||||||||||N|
PV1||N
DG1|1||O10.91^Hypertension préexistante non spécifiée compliquant la grossesse^CIE10-ES|Hypertension gestationnelle||A||

Ce message se trouve dans le chemin /shared/HL7Out

Captures d'écran de diagnostic en texte brut

Dans l'option Analyseur de texte, l'utilisateur peut inclure un texte brut sur lequel un processus d'analyse sera effectué. L'application recherchera des tuples de 3 mots lemmatisés (en éliminant les articles, les pronoms et d'autres mots peu pertinents). Une fois analysé, le système affichera le texte pertinent souligné et les diagnostics possibles localisés:

Une fois l'analyse effectuée, elle peut être consultée à tout moment à partir de l'historique de l'analyse.

Historique des analyses

Toutes les analyses effectuées sont enregistrées et peuvent être consultées à tout moment, en visualisant tous les codes CIM-10 possibles:

Dans le prochain article...

Nous verrons comment, en utilisant Embedded Python, nous utilisons un modèle LLM spécifique pour la vectorisation des codes CIM-10 qui nous serviront de contexte et des textes bruts.

Si vous avez des questions ou des suggestions, n'hésitez pas à écrire un commentaire dans l'article.

0
1 40
Article Iryna Mykhailova · Août 16, 2024 1m read

Il existe de nombreux articles communautaires intéressants concernant la « recherche vectorielle sur IRIS » et des exemples dans OpenExchange. Chaque fois que je les vois, je suis ravi de savoir que tant de développeurs essaient déjà les vecteurs sur IRIS !

Mais si vous n'avez pas encore essayé la « recherche vectorielle sur IRIS », donnez-moi une minute 😄 Je crée une classe IRIS - et avec une seule classe IRIS, vous pouvez voir comment vous placez les données vectorielles dans votre base de données IRIS et comment vous les comparez dans votre application.

0
0 41
Article Guillaume Rongier · Juin 3, 2024 4m read

L'intelligence artificielle a un potentiel transformateur pour générer de la valeur et des informations à partir des données. Alors que nous nous dirigeons vers un univers où presque toutes les applications seront pilotées par l'IA, les développeurs qui créent ces applications auront besoin des outils adéquats pour créer des expériences à partir de ces applications. C'est pourquoi nous sommes heureux d'annoncer que la recherche vectorielle a été ajoutée à la plate-forme de données InterSystems IRIS.

0
0 64
Article Guillaume Rongier · Mai 22, 2024 6m read

Il me semble que c'était hier, lorsque nous avons réalisé un petit projet en Java pour tester les performances d'IRIS, PostgreSQL et MySQL (vous pouvez consulter l'article que nous avons écrit en juin à la fin de cet article). Si vous vous souvenez bien, IRIS était supérieur à PostgreSQL et dépassait considérablement MySQL en termes d'insertions, sans grande différence au niveau des requêtes.

Peu de temps après , @Dmitry Maslennikov m'a demandé "Pourquoi ne le testez-vous pas à partir d'un projet Python ?". Voici donc la version Python des tests que nous avons effectués précédemment en utilisant les connexions JDBC.

Tout d'abord, sachez que je ne suis pas un expert en Python, donc si vous remarquez des choses qui pourraient être améliorées, n'hésitez pas à me contacter.

Pour cet exemple, j'ai utilisé Jupyter Notebook, qui simplifie considérablement le développement en Python et nous permet de voir étape par étape ce que nous faisons. En annexe de cet article vous trouverez l'application qui vous permettra d'effectuer vos propres tests.

Avertissement pour les utilisateurs de Windows

Si vous clonez le projet GitHub dans Visual Studio Code, pour déployer correctement les conteneurs, il vous faudra peut-être changer la configuration de fin de ligne par défaut de CRLF à LF:

Si vous souhaitez reproduire les tests sur vos ordinateurs, vous devez tenir compte des éléments suivants : Docker Desktop demandera des autorisations d'accès aux dossiers de vos ordinateurs dont il a besoin pour déployer le projet. Si vous n'avez pas configuré l'autorisation d'accès à ces dossiers avant de lancer les conteneurs Docker, la création initiale de la table de test dans PostgreSQL échouera, donc avant de lancer le projet, vous devez configurer l'accès partagé aux dossiers du projet dans votre DockerDesktop.

Pour ce faire, vous devez accéder à Paramètres -> Ressources -> Partage de fichiers ("Settings -> Resources -> File sharing") et ajouter à la liste le dossier dans lequel vous avez cloné le projet:

Vous êtes prévenus!

 

Test de performance

Pour ces tests, nous utiliserons une table relativement simple contenant les informations les plus basiques possibles sur un patient. Ici vous pouvez voir la commande pour créer la table en SQL:

CREATETABLE Test.Patient (
    NameVARCHAR(225),
    Lastname VARCHAR(225),
    Photo VARCHAR(5000),
    Phone VARCHAR(14),
    Address VARCHAR(225)    
)

Comme vous pouvez le voir, nous avons défini la photo du patient comme un VARCHAR(5000), puisque nous allons inclure (théoriquement) les informations vectorisées de la photo. Il y a quelques mois, j'ai publié un article expliquant comment utiliser Embedded Python pour implémenter IRIS, un système de reconnaissance faciale (ici) où vous pouvez voir comment les images sont transformées en vecteurs pour une comparaison ultérieure. La question de la vectorisation vient du fait que ce format vectoriel est la norme dans de nombreux modèles d'apprentissage automatique et qu'il est toujours utile de tester avec quelque chose de similaire à la réalité (juste quelque chose)./p>

 

Paramètres de Jupyter Notebook

Pour simplifier au maximum le développement du projet en Python, j'ai utilisé un outil magnifique, le Jupyter Notebook, qui permet de développer pas à pas chacune des fonctionnalités dont nous aurons besoin.

Voici un aperçu de notre Jupyter:

Jetons un coup d'œil aux points les plus intéressants de celui-ci:

Importation de bibliothèques:

import iris
import names
import numpy as np
from datetime import datetime
import psycopg2
import mysql.connector
import matplotlib.pyplot as plt
import random_address
from phone_gen import PhoneNumber

Connexion aux bases de données:

IRIS:

connection_string = "iris:1972/TEST"
username = "superuser"
password = "SYS"
connectionIRIS = iris.connect(connection_string, username, password)
cursorIRIS = connectionIRIS.cursor()
print("Connected")

PostgreSQL:

connectionPostgres = psycopg2.connect(database="testuser",
                        host="postgres",
                        user="testuser",
                        password="testpassword",
                        port="5432")
cursorPostgres = connectionPostgres.cursor()
print("Connected")

MySQL:

connectionMySQL = mysql.connector.connect(
  host="mysql",
  user="testuser",
  password="testpassword"
)
cursorMySQL = connectionMySQL.cursor()
print("Connected")

Génération des valeurs à insérer

phone_number = PhoneNumber("USA")
resultsIRIS = []
resultsPostgres = []
resultsMySQL = []
parameters =  []
for x in range(1000):
    rng = np.random.default_rng()
    parameter = []
    parameter.append(names.get_first_name())
    parameter.append(names.get_last_name())
    parameter.append(str(rng.standard_normal(50)))
    parameter.append(phone_number.get_number())
    parameter.append(random_address.real_random_address_by_state('CA')['address1'])
    parameters.append(parameter)

print("Parameters built")

Insertion dans IRIS

date_before = datetime.now()

cursorIRIS.executemany("INSERT INTO Test.Patient (Name, Lastname, Photo, Phone, Address) VALUES (?, ?, ?, ?, ?)", parameters) connectionIRIS.commit() difference = datetime.now() - date_before print(difference.total_seconds()) resultsIRIS.append(difference.total_seconds())

Insertion dans PostgreSQL

date_before = datetime.now()

cursorPostgres.executemany("INSERT INTO test.patient (name, lastname, photo, phone, address) VALUES (%s,%s,%s,%s,%s)", parameters) connectionPostgres.commit() difference = datetime.now() - date_before print(difference.total_seconds()) resultsPostgres.append(difference.total_seconds())

Insertion dans MySQL

date_before = datetime.now()

cursorMySQL.executemany("INSERT INTO test.patient (name, lastname, photo, phone, address) VALUES (%s,%s,%s,%s,%s)", parameters) connectionMySQL.commit() difference = datetime.now() - date_before print(difference.total_seconds()) resultsMySQL.append(difference.total_seconds())

Pour notre test, j'ai décidé d'insérer les valeurs suivantes dans chaque base de données:

  • 1 insertion de 1000 patients.
  • 1 insertion de 5000 patients.
  • 1 insertion de 20 000 patients.
  • 1 insertion de 50 000 patients.

Gardez à l'esprit, lors de l'exécution des tests, que le processus le plus long est la création des valeurs à insérer par Python. Pour se rapprocher de la réalité, j'ai lancé plusieurs tests à l'avance afin que les bases de données disposent déjà d'un ensemble significatif d'enregistrements (environ 200 000 enregistrements)./p>

Résultats des tests

Insertion de 1000 patients:

  • InterSystems IRIS: 0,037949 seconde.
  • PostgreSQL: 0,106508 seconde.
  • MySQL: 0,053338 seconde.

Insertion de 5,000 patients:

  • InterSystems IRIS: 0,162791 seconde.
  • PostgreSQL: 0,432642 seconde.
  • MySQL: 0,18925 seconde.

Insertion de 20,000 patients:

  • InterSystems IRIS: 0,601944 seconde.
  • PostgreSQL: 1,803113 seconde.
  • MySQL: 0,594396 seconde.

Insertion de 50,000 patients:

  • InterSystems IRIS: 1.482824 seconde.
  • PostgreSQL: 4,581251 secondes.
  • MySQL: 2,162996 secondes.

Bien qu'il s'agisse d'un test assez simple, il est très significatif car il nous permet de voir la tendance de chaque base de données en ce qui concerne les performances d'insertion.

Conclusions

Si nous comparons les performances des tests effectués avec le projet Java et le projet actuel en Python, nous constatons qu'à cette occasion, PostgreSQL est clairement inférieur au projet Python, étant 4 fois plus lent qu'InterSystems IRIS, tandis que MySQL s'est amélioré par rapport à la version Java.

InterSystems IRIS reste incontestablement le meilleur des trois, présentant un comportement plus linéaire et une meilleure performance d'insertion, quelle que soit la technologie utilisée.

Caractéristiques techniques de l'ordinateur portable utilisé pour les tests:

  • Système d'exploitation: Microsoft Windows 11 Pro.
  • Processeur: 13th Gen Intel(R) Core(TM) i9-13900H, 2600 Mhz.
  • Mémoire RAM: 64 Go.
0
0 79