Article Lorenzo Scalese · Oct 28, 2025 10m read

Le déploiement de nouvelles instances IRIS peut être une tâche fastidieuse, en particulier lors de la mise en place de plusieurs environnements avec des configurations en miroir.

J'ai fait face à ce problème très souvent et je souhaite partager mon expérience et mes recommandations concernant l'utilisation d'Ansible pour rationaliser le processus d'installation d'IRIS. Mon approche inclut également la gestion des tâches supplémentaires généralement effectuées avant et après l'installation d'IRIS.

Ce manuel suppose que vous disposez d'une compréhension de base du fonctionnement d'Ansible, je ne détaillerai donc pas ses principes fondamentaux. Toutefois, si vous avez des questions sur les points abordés ici, n'hésitez pas à les poser dans les commentaires ci-dessous.

Les exemples fournis dans ce manuel ont été testés à l'aide d' Ansible 3.6 sur un serveur Red Hat 8, avec IRIS 2023.1.1 et Red Hat 8 comme environnement client. D'autres versions d'Ansible, de Red Hat (ou d'autres variantes d'UNIX) et d'IRIS peuvent également fonctionner, mais les résultats peuvent varier.

Installation d'Ansible

Le serveur Ansible nécessite une distribution Linux. Nous utilisons Red Hat 8 dans cet article, mais d'autres distributions et versions Linux devraient également fonctionner.

Pour installer les paquets Ansible, il faut d'abord installer EPEL:

[ansible@auto01 ansible]$ yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm

Ensuite, il faut installer Ansible:

[ansible@auto01 ansible]$ yum install ansible

En plus des paquets, Ansible nécessite un accès SSH aux serveurs distants. Je recommande de créer une paire de clés SSH, ce qui est plus sûr que l'utilisation de mots de passe traditionnels. De plus, l'utilisateur servant à se connecter aux serveurs distants doit disposer de privilèges administratifs (c'est-à-dire faire partie du groupe wheel).

Fichiers et dossiers

Pour préserver une structure organisée, je recommande les fichiers et dossiers suivants dans le répertoire ansible:

[ansible@auto01 ansible]$ ls -l
total 4
-rw-r--r--. 1 ansible ansible 247 Dec  500:57 ansible.cfg
drwxrwxr-x. 2 ansible ansible   6 Dec  500:56 files
drwxrwxr-x. 2 ansible ansible   6 Dec  500:56 inventory
drwxrwxr-x. 2 ansible ansible   6 Dec  500:56 library
drwxrwxr-x. 2 ansible ansible   6 Dec  500:56 playbooks
drwxrwxr-x. 2 ansible ansible   6 Dec  500:56 templates
drwxrwxr-x. 2 ansible ansible   6 Dec  500:56 vars
drwxrwxr-x. 2 ansible ansible   6 Dec  500:56 vault
Fichier/DossierDescription
ansible.cfgFichier de configuration d'Ansible. Contient des directives sur le comportement d'Ansible.
filesContient les fichiers supplémentaires nécessaires aux playbooks, tels que le fichier tar.gz d'installation d'IRIS.
inventoryContient les fichiers d'inventaire de l'hôte. Vous pouvez avoir un seul fichier d'inventaire volumineux ou plusieurs fichiers de moindre taille. Le fractionnement de l'inventaire nécessite davantage d'efforts lorsque vous exécutez des playbooks sur plusieurs hôtes.
libraryContient des fichiers de bibliothèque supplémentaires d'Ansible. Non requis pour ces exemples, mais utile pour de futures extensions.
playbooksContient tous les playbooks développés, y compris le playbook d'installation IRIS décrit ci-dessous.
templatesContient les fichiers modèles utilisés par les playbooks. Ceux-ci sont transférés vers les serveurs et instanciés avec les paramètres corrects.
varsContient les variables disponibles pour tous les playbooks.
vaultContient des variables sensibles accessibles uniquement via la commande ansible-vault. Utile pour gérer les mots de passe.

 

Après avoir configuré cette structure de dossiers, copiez le programme d'installation IRIS et la clé de licence IRIS dans le dossier files. Le résultat devrait apparaître comme suit:

[ansible@auto01 ansible]$ ls -l files/
total 759976
-rw-rw-r--. 1 ansible ansible 778207913 Dec  514:32 IRISHealth-2023.1.1.380.0.22870-lnxrh8x64.tar.gz
-rw-rw-r--. 1 ansible ansible      1160 Sep  519:13 iris.key

 

Inventaire

Pour exécuter des playbooks dans Ansible, il est nécessaire de définir l'inventaire des serveurs. Il existe plusieurs méthodes pour ce faire, et chacune présente ses propres avantages. Dans cet article, nous utiliserons un seul fichier pour définir tous les serveurs.

Le ficher servers.yml contiendra l'inventaire complet, répertoriant chaque serveur ainsi que les variables requises pour l'installation d'IRIS. Voici un exemple:

[ansible@auto01ansible]$catinventory/servers.yml 
---all:  hosts:test01.mydomain:      iris_user:irisusr      iris_group:irisgrp      mgr_user:irisown      mgr_group:irismgr      platform:lnxrh8x64      iris_cmd:iris      iris_instances:        - name:TEST01          superserver_port:51773          webserver_port:52773          binary_file:IRISHealth-2023.1.1.380.0.22870-lnxrh8x64          key_file:iris.key          install_dir:/test/iris          jrnpri_dir:/test/jrnpri          jrnsec_dir:/test/jrnsec          config_globals:16384          config_errlog:10000          config_routines:"0,128,0,128,0,1024"          config_gmheap:1048576          config_locksiz:128057344

 

Fichier coffre-fort

Pour sécuriser les mots de passe, créez un fichier coffre-fort contenant les mots de passe des comptes IRIS SuperUser et CSPSystem.

Pour modifier le fichier coffre-fort par défaut, utilisez la commande suivante:

[ansible@auto01ansible]$ansible-vaulteditvault/defaults.yml---# Default passwordsiris_user_passwd:"Ch4ngeTh!s"

 

Playbook

Pour effectuer une installation IRIS, il est nécessaire d'exécuter plusieurs tâches sur le serveur cible. Ces tâches sont regroupées et classées dans un fichier appelé playbook.
Un playbook consiste essentiellement en une liste de tâches qui sont exécutées de manière séquentielle sur les hôtes distants.

Vous trouverez ci-dessous le playbook que j'ai développé pour installer IRIS:

[ansible@auto01ansible]$catplaybooks/install_iris.yml## Playbook to install Iris#- hosts:all  become:yes  gather_facts:no  tasks:  - name:"Load default passwords"    include_vars:"../vault/defaults.yml"### PRE-INSTALL TASKS:  - name:"Install required packets"    yum:      name:"{{ item }}"      state:latest    loop:      -"httpd"      -"java-1.8.0-openjdk"      -"mod_auth_mellon"      -"mod_ssl"  - name:"Create iris group"    group:      name:"{{ iris_group }}"      gid:5005  - name:"Create iris mgr group"    group:      name:"{{ mgr_group }}"      gid:5006  - name:"Create iris owner user"    user:      name:"{{ mgr_user }}"      uid:5006      group:"{{ iris_group }}"      groups:"{{ mgr_group }}"  - name:"Create iris user"    user:      name:"{{ iris_user }}"      uid:5005      group:"{{ iris_group }}"  - name:"Create mgr folder"    file:      path:"{{ item.install_dir }}/mgr"      state:directory      owner:"{{ iris_user }}"      group:"{{ iris_group }}"      mode:0775    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"  - name:"Copy license key"    copy:      src:"../files/{{ item.key_file }}"      dest:"{{ item.install_dir }}/mgr/iris.key"      owner:"{{ iris_user }}"      group:"{{ iris_group }}"      backup:yes    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"  - name:"Create /install folder"    file:      path:"/install"      state:directory      mode:0777  - name:"Create Instances install folders"    file:      path:"/install/{{ item.name }}"      state:directory      mode:0777    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"  - name:"Copy IRIS installer"    copy:      src:"../files/{{ item.binary_file }}.tar.gz"      dest:"/install/{{ item.name }}/"    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"  - name:"Untar IRIS installer"    command:      cmd:"tar -xzf /install/{{ item.name }}/{{ item.binary_file }}.tar.gz"      chdir:"/install/{{ item.name }}/"    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"### IRIS INSTALL:  - name:"Install Iris"    command:      cmd:"./irisinstall_silent"      chdir:"/install/{{ item.name }}/{{ item.binary_file }}"    environment:      ISC_PACKAGE_INSTANCENAME:"{{ item.name }}"      ISC_PACKAGE_INSTALLDIR:"{{ item.install_dir }}"      ISC_PACKAGE_PLATFORM:"{{ platform }}"      ISC_PACKAGE_UNICODE:"Y"      ISC_PACKAGE_INITIAL_SECURITY:"Normal"      ISC_PACKAGE_MGRUSER:"{{ mgr_user }}"      ISC_PACKAGE_MGRGROUP:"{{ mgr_group }}"      ISC_PACKAGE_USER_PASSWORD:"{{ iris_user_passwd }}"      ISC_PACKAGE_CSPSYSTEM_PASSWORD:"{{ iris_user_passwd }}"      ISC_PACKAGE_IRISUSER:"{{ iris_user }}"      ISC_PACKAGE_IRISGROUP:"{{ iris_group }}"      ISC_PACKAGE_SUPERSERVER_PORT:"{{ item.superserver_port }}"      ISC_PACKAGE_WEBSERVER_PORT:"{{ item.webserver_port }}"      ISC_PACKAGE_CLIENT_COMPONENTS:"standard_install"      ISC_PACKAGE_STARTIRIS:"N"    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"  - name:"Remove installers"    file:      path:"/install/{{ item.name }}"      state:absent    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"### IRIS CUSTOMIZATIONS:  - name:"Change iris.cpf"    lineinfile:      path:"{{ item[0].install_dir }}/iris.cpf"      regexp:"{{ item[1].from }}"      line:"{{ item[1].to }}"      backup:yes    with_nested:      -"{{ iris_instances }}"      -[{from:"^TerminalPrompt=.*",to:"TerminalPrompt=8,3,2"},{from:"^FreezeOnError=0",to:"FreezeOnError=1"},{from:"^AutoParallel=.*",to:"AutoParallel=0"},{from:"^FastDistinct=.*",to:"FastDistinct=0"},{from:"^LockThreshold=.*",to:"LockThreshold=10000"},{from:"^EnsembleAutoStart=.*",to:"EnsembleAutoStart=1"},{from:"^MaxIRISTempSizeAtStart=.*",to:"MaxIRISTempSizeAtStart=300"}]    loop_control:      label:"{{ item[0].name }}: {{ item[1].to }}"  - name:"Change Journal Current Dir"    lineinfile:      path:"{{ item.install_dir }}/iris.cpf"      regexp:"^CurrentDirectory=.*"      line:"CurrentDirectory={{ item.jrnpri_dir }}"      backup:yes    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"  - name:"Change Journal Alternate Dir"    lineinfile:      path:"{{ item.install_dir }}/iris.cpf"      regexp:"^AlternateDirectory=.*"      line:"AlternateDirectory={{ item.jrnsec_dir }}"      backup:yes    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"  - name:"Change Journal Prefix name"    lineinfile:      path:"{{ item.install_dir }}/iris.cpf"      regexp:"^JournalFilePrefix=.*"      line:"JournalFilePrefix={{ item.name }}_"      backup:yes    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"  - name:"Change Globals memory"    lineinfile:      path:"{{ item.install_dir }}/iris.cpf"      regexp:"^globals=.*"      line:"globals=0,0,{{ item.config_globals }},0,0,0"      backup:yes    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"  - name:"Change errlog memory"    lineinfile:      path:"{{ item.install_dir }}/iris.cpf"      regexp:"^errlog=.*"      line:"errlog={{ item.config_errlog }}"      backup:yes    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"  - name:"Change routines memory"    lineinfile:      path:"{{ item.install_dir }}/iris.cpf"      regexp:"^routines=.*"      line:"routines={{ item.config_routines }}"      backup:yes    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"  - name:"Change gmheap memory"    lineinfile:      path:"{{ item.install_dir }}/iris.cpf"      regexp:"^gmheap=.*"      line:"gmheap={{ item.config_gmheap }}"      backup:yes    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"  - name:"Change locksiz memory"    lineinfile:      path:"{{ item.install_dir }}/iris.cpf"      regexp:"^locksiz=.*"      line:"locksiz={{ item.config_locksiz }}"      backup:yes    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"### START IRIS:  - name:"Start Iris"    command:"iris start {{ item.name }}"    loop:"{{ iris_instances }}"    loop_control:      label:"{{ item.name }}"...

Comme vous pouvez le constater, ce playbook comporte plusieurs tâches, dont la plupart sont explicites d'après leur nom. Les commentaires indiquent les tâches à effectuer avant l'installation, l'installation proprement dite et les personnalisations à effectuer après l'installation. Après avoir exécuté ce playbook, vous disposerez d'une nouvelle instance IRIS installée sur le système cible, dont la mémoire et d'autres paramètres auront été personnalisés.

Lancez l'installation!

Après avoir configuré l'inventaire, le ficheir coffre-fort et les playbooks, vous êtes prêt à exécuter l'installation IRIS à l'aide d'Ansible.
Pour ce faire, exécutez la commande suivante:

[ansible@auto01 ansible]$ ansible-playbook -K --ask-vault-pass -i inventory/servers.yml playbooks/install_iris.yml
BECOME password: 
Vault password: 

PLAY [all] ************************************************************************************************************************************************** . . .

Lorsque l'exécution du playbook est terminée, vous recevez un résumé de statuts des tâches qui vous permet de vérifier que tout a été exécuté avec succès.

Et voilà, vous venez d'installer IRIS à l'aide d'Ansible! 😁

0
0 14
Article Lorenzo Scalese · Oct 6, 2025 5m read

Commençons par une question simple et motivante : au cours des 14 derniers jours, quelles sont les erreurs les plus courantes dans le Journal des erreurs d'application?

Répondre à cette question via le portail de gestion ou le terminal est un processus manuel fastidieux. Nous devrions pouvoir simplement utiliser SQL. Heureusement, quelques requêtes de classe sont disponibles  pour vous aider dans la classe SYS.ApplicationError de l'espace de noms %SYS. Vous pouvez répondre à cette question pour une seule date à l'aide d'une commande telle que:

select"Error message",count(*)
from SYS.ApplicationError_ErrorList('CCR','12/16/2024')
groupby"Error message"orderby2desc

Malheureusement, la structure des requêtes de classe est soumise aux mêmes contraintes structurelles générales que les pages du portail de gestion ; la requête ErrorList nécessite un espace de noms et une date. Il existe sûrement une meilleure approche que de faire 14 appels conjoints à cette requête de classe pour différentes dates, n'est-ce pas ? D'une certaine manière, c'est un véritable problème. S'il existe une bonne façon de procéder avec du SQL classique et que je l'ai simplement manquée, merci de me le faire savoir!

Logiquement, il convient de rédiger notre propre requête de classe personnalisée. Cela implique d'ajouter un membre de classe Query (par exemple <QueryName>) et d'implémenter des méthodes nommées <QueryName>Execute, <QueryName>Fetch et <QueryName>Close. De manière générale, la méthode Execute configure le contexte de la requête de classe et effectue toutes les tâches initiales, en conservant l'état dans qHandle. La méthode Fetch récupère une seule ligne et indique si toutes les lignes ont été trouvées ou non. Enfin, la méthode Close effectue le nettoyage final. Par exemple, si l'implémentation des méthodes Execute/Fetch utilise une variable globale privée au processus, la méthode Close peut la supprimer.

N'oubliez pas d'ajouter un indicateur [ SqlProc ] magique au membre Query afin qu'il puisse être appelé en tant que TVF (fonction table) à partir d'autres requêtes SQL!

Ci-dessous, vous trouverez un exemple complet fonctionnel:

/// Requêtes utilitaires pour aider à accéder au journal des erreurs de l'application à partir de SQLClass AppS.Util.ApplicationErrorLog
{

/// Renvoi de toutes les erreurs d'application (toutes dates confondues) à partir du journal des erreurs d'application Query All() As%Query(ROWSPEC = "Date:%Date,ErrorNumber:%Integer,ErrorMessage:%String,Username:%String") [ SqlProc ] { }

/// Récupèration d'une liste de dates comportant des erreurs et la stocke dans qHandleClassMethod AllExecute(ByRef qHandle As%Binary) As%Status { Set ns = $NamespaceNew$NamespaceSet$Namespace = "%SYS"Set stmt = ##class(%SQL.Statement).%New() Set stmt.%SelectMode = 0Set result = ##class(%SQL.Statement).%ExecDirect(stmt,"select %DLIST(""Date"") ""Dates"" from SYS.ApplicationError_DateList(?)",ns) $$$ThrowSQLIfError(result.%SQLCODE,result.%Message) If 'result.%Next(.sc) { Return sc } Set qHandle("list") = result.%Get("Dates") Set qHandle("pointer") = 0Quit$$$OK }

/// Récupèreation de la ligne suivante, en passant à la date suivante si nécessaireClassMethod AllFetch(ByRef qHandle As%Binary, ByRef Row As%List, ByRef AtEnd As%Integer = 0) As%Status [ PlaceAfter = AllExecute ] { Set sc = $$$OKSet ns = $NamespaceNew$NamespaceSet$Namespace = "%SYS"If$Get(qHandle("dateResult")) = "" { // Passage à la date suivanteSet pointer = qHandle("pointer") If '$ListNext(qHandle("list"),pointer,oneDate) { Set AtEnd = 1Quit$$$OK } Set qHandle("pointer") = pointer Set qHandle("currentDate") = oneDate Set qHandle("dateResult") = ##class(%SQL.Statement).%ExecDirect(,"select * from SYS.ApplicationError_ErrorList(?,?)",ns,oneDate) $$$ThrowSQLIfError(qHandle("dateResult").%SQLCODE,qHandle("dateResult").%Message) } If qHandle("dateResult").%Next(.sc) { // Si nous avons une ligne pour la date actuelle, ajoutons-laSet Row = $ListBuild(qHandle("currentDate"),qHandle("dateResult").%GetData(1),qHandle("dateResult").%GetData(2),qHandle("dateResult").%GetData(6)) } ElseIf$$$ISOK(sc) { // Sinon, il faut vider le jeu de résultats et appeler AllFetch pour avancerSet qHandle("dateResult") = ""Set$Namespace = ns Set sc = ..AllFetch(.qHandle,.Row,.AtEnd) } Quit sc }

ClassMethod AllClose(ByRef qHandle As%Binary) As%Status [ PlaceAfter = AllExecute ] { New$NamespaceSet$Namespace = "%SYS"// Il semble parfois nécessaire pour que %OnClose s'exécute correctementKill qHandle("dateResult") Quit$$$OK }

}

Dans cet exemple, nous commençons dans un espace de noms utilisateur, mais toutes les requêtes s'exécutent en réalité dans %SYS. Execute obtient une liste des dates d'erreur pour l'espace de noms actuel et la stocke dans qHandle. Fetch passe à la date suivante lorsque cela est approprié, puis renvoie l'erreur suivante pour la date actuelle. Et Close s'assure que la requête de classe sort de la portée dans %SYS, car j'obtenais parfois des erreurs si ce n'était pas le cas. C'était un peu surprenant, mais cela semble logique, car la requête de classe que nous appelons n'existe que dans %SYS.

La réutilisabilité des fonctions table offre de nombreuses possibilités. Par exemple, nous pouvons en ajouter une autre dans la même classe:

/// Obtenir le nombre d'erreurs survenues au cours des derniers <var>Days</var> jours
Query ErrorCounts(Days As%Integer) As%SQLQuery(ROWSPEC = "Occurrences:%Integer,ErrorMessage:%String") [ SqlProc ]
{
    SELECT COUNT(*) AS Occurrences, ErrorMessage
    FROM AppS_Util.ApplicationErrorLog_All()
    WHERE DATEDIFF(D,"Date",$h) <= :Days
    GROUP BY ErrorMessage
    ORDER BY Occurrences DESC
}

Et maintenant, pour obtenir les erreurs d'application les plus courantes au cours des 14 derniers jours, il suffit de:

call AppS_Util.ApplicationErrorLog_ErrorCounts(14)

Maintenant, il ne nous reste plus qu'à les corriger! 😅

0
0 18
Article Lorenzo Scalese · Sept 23, 2025 8m read

Mes clients me contactent régulièrement à propos du dimensionnement de la mémoire lorsqu'ils reçoivent des alertes indiquant que la mémoire libre est inférieure à un seuil ou lorsqu'ils constatent que la mémoire libre a soudainement diminué. Existe-t-il un problème? Leur application va-t-elle cesser de fonctionner parce qu'elle manque de mémoire pour exécuter les processus système et applicatifs? La réponse est presque toujours non, il est inutile de s'inquiéter. Mais cette réponse simple n'est généralement pas suffisante. Que se passe-t-il?

Considérez le graphique ci-dessous. Il montre le résultat de la métrique free dans vmstat. Il existe d'autres moyens d'afficher la mémoire libre d'un système, par exemple la commande free -m. Parfois, la mémoire libre disparaît progressivement au fil du temps. Le graphique ci-dessous est un exemple exagéré, mais il illustre bien ce qui se passe.

image

Comme vous pouvez le constater, vers 2 heures du matin, une partie de la mémoire est récupérée, puis chute soudainement à près de zéro. Ce système exécute l'application IntelliCare EHR sur la base de données InterSystems IRIS. Les informations vmstat proviennent d'un fichier HTML ^SystemPerformance qui collecte les métriques vmstat, iostat et plusieurs autres métriques système. Que se passe-t-il d'autre sur ce système ? Comme nous sommes en pleine nuit, je ne m'attends pas à ce qu'il se passe grand-chose à l'hôpital. Examinons iostat pour les volumes de la base de données.

image

On constate une augmentation soudaine des lectures au moment où la mémoire libre diminue. La baisse de la mémoire libre signalée correspond à un pic des lectures en gros blocs (taille de requête de 2048 Ko) indiqué dans iostat pour le disque de la base de données. Il s'agit très probablement d'un processus de sauvegarde ou d'une copie de fichiers. Bien sûr, corrélation n'est pas synonyme de causalité, mais cela vaut la peine d'être examiné et, en fin de compte, cela explique ce qui se passe.

Examinons d'autres résultats de ^SystemPerformance. La commande free -m est exécutée à la même fréquence que vmstat (par exemple, toutes les 5 secondes) et est accompagnée de la date et de l'heure, ce qui nous permet également de représenter graphiquement les compteurs dans free -m.

Les compteurs:

  • Memtotal – Total de RAM physique.
  • used – RAM activement utilisée (applications + système d'exploitation + cache).
  • free – RAM complètement inutilisée.
  • shared – Mémoire partagée entre les processus.
  • buf/cache – RAM utilisée pour les tampons et le cache, récupérable si nécessaire.
  • available – RAM disponible sans swap.
  • swaptotal – Espace de swap total sur le disque.
  • swapused – Espace de swap actuellement utilisé.
  • swapfree – Espace de swap inutilisé.

Pourquoi la mémoire libre diminue-t-elle à 2 heures du matin?

  • Les lectures séquentielles de grande taille remplissent le cache de page du système de fichiers, consommant temporairement de la mémoire qui apparaît comme "utilisée" dans free -m.
  • Linux utilise de manière agressive la mémoire inexploitée pour la mise en cache des E/S afin d'améliorer les performances.
  • Une fois la sauvegarde terminée (≈ 03h00), la mémoire est progressivement récupérée au fur et à mesure que les processus en ont besoin.
  • Vers 6 heures du matin, l'hôpital commence à s'activer et la mémoire est utilisée pour IRIS et d'autres processus.

Une mémoire libre insuffisante ne constitue pas une pénurie, mais plutôt une utilisation de la mémoire "libre" par le système à des fins de mise en cache. Il s'agit d'un comportement normal sous Linux! Le processus de sauvegarde lit de grandes quantités de données, que Linux met agressivement en cache dans la mémoire tampon/cache. Le noyau Linux convertit la mémoire "libre" en mémoire "cache" afin d'accélérer les opérations d'E/S.

Résumé

Le cache du système de fichiers est conçu pour être dynamique. Si la mémoire est requise par un processus, elle sera immédiatement récupérée. Il s'agit d'un élément normal de la gestion de la mémoire sous Linux.


Les Huge Pages ont-elles un impact?

Pour optimiser les performances et réserver de la mémoire pour la mémoire partagée IRIS, la meilleure pratique pour les déploiements IRIS en production sur des serveurs dotés d'une mémoire importante consiste à utiliser les Huge Pages de Linux. Pour IntelliCare, j'utilise généralement 8 Go de mémoire par noyau et environ 75 % de la mémoire pour la mémoire partagée d'IRIS (tampons Routine et Global, GMHEAP et autres structures de mémoire partagée). La répartition de la mémoire partagée dépend des exigences de l'application. Vos exigences peuvent être complètement différentes. Par exemple, en utilisant ce rapport CPU/mémoire, 25 % suffisent-ils pour les processus IRIS et les processus du système d'exploitation de votre application?

InterSystems IRIS utilise l'E/S directe pour les fichiers de base de données et les fichiers journaux, ce qui contourne le cache du système de fichiers. Ses segments de mémoire partagée (globales, routines, gmheap, etc.) sont alloués à partir de Huge Pages.

  • Ces pages immenses (huge pages) sont dédiées à la mémoire partagée IRIS et n'apparaissent pas comme "libres" ou "cache" dans free -m.
  • Once allocated, huge pages are not available for filesystem cache or user processes.

Cela explique pourquoi les métriques free -m semblent "insuffisantes" même si la base de données IRIS elle-même ne manque pas de mémoire.


Comment la mémoire libre pour un processus est-elle calculée?

À partir de ce qui précède, dans free -m, les lignes pertinentes sont les suivantes:

  • free – RAM totalement inutilisée.
  • available – RAM encore utilisable sans échange.

La disponibilité est un bon indicateur: elle inclut la mémoire cache et les tampons récupérables, indiquant ce qui est réellement disponible pour les nouveaux processus sans échange. Quels processus? Pour plus d'informations, consultez InterSystems Data Platforms and Performance Part 4 - Looking at Memory . Voici une liste simple: système d'exploitation, autres processus d'application non-IRIS et processus IRIS.

Examinons un graphique de la sortie free -m.

image

Bien que la valeur de la mémoire libre (free) chute à près de zéro pendant la sauvegarde, la valeur de la mémoire disponible (available) reste beaucoup plus élevée (plusieurs dizaines de Go). Cela signifie que le système pourrait fournir cette mémoire aux processus si nécessaire.

A quel endroit apparaissent les pages immenses dans la mémoire libre?

Par défaut, free -m n'affiche pas directement les pages immenses. Pour les voir, vous avez besoin des entrées /proc/meminfo telles que HugePages_Total, HugePages_Free et Hugepagesize.

Puisque le système d'exploitation réserve des pages immenses au démarrage, elles sont effectivement invisibles pour free -m. Elles sont verrouillées et isolées du pool de mémoire général.

Résumé

  • La "mémoire disponible" insuffisante constatée vers 02h00 est due au remplissage du cache de pages Linux par des lectures de sauvegarde. Il s'agit d'un comportement normal qui n'indique pas une pénurie de mémoire.
  • Les pages immenses réservées à IRIS ne sont pas affectées et continuent à servir efficacement la base de données.
  • La mémoire réellement disponible pour les applications est mieux mesurée par la colonne disponible, qui montre que le système dispose encore d'une marge suffisante.

Mais attendez, que se passe-t-il si je n'utilise pas les Huge Pages?

Généralement, on n'utilise pas les Huge Pages sur les systèmes non productifs ou à mémoire limitée. Les gains de performances des Huge Pages ne sont généralement pas significatifs en dessous de 64 Go, bien qu'il soit toujours recommandé d'utiliser les Huge Pages pour protéger la mémoire partagée IRIS.

A propos. J'ai vu des sites rencontrer des problèmes en allouant des pages immenses moins grandes que la mémoire partagée, ce qui oblige IRIS à essayer de démarrer avec des tampons globaux très petits ou à échouer au démarrage si memlock est utilisé (envisagez memlock=192 pour les systèmes de production).

Sans Huge Pages, les segments de mémoire partagée IRIS ( globales, routines, gmheap, etc.) sont alloués à partir de pages de mémoire normales du système d'exploitation. Cela apparaîtrait sous la mémoire "utilisée" dans free -m. Cela contribuerait également à réduire la mémoire "disponible", car cette mémoire ne peut pas être facilement récupérée.

  • utilisée – Beaucoup plus élevée, reflétant la mémoire partagée IRIS + le noyau + d'autres processus.
  • libre – Probablement moins suffisante, car plus de RAM est allouée en permanence à IRIS dans le pool régulier.
  • buf/cache – Augmenterait toujours pendant les sauvegardes, mais la marge apparente pour les processus semblerait plus restreinte, car la mémoire IRIS se trouve dans le même pool.
  • disponible – Plus proche de la véritable “mémoire libre + cache récupérable” moins la mémoire IRIS. Cela semblerait plus petit que dans votre configuration Huge Pages.

Alors, faut-il utiliser Huge Pages dans des systèmes de production?

OUI!

Pour la protection de la mémoire. La mémoire partagée IRIS est protégée contre:

  • Remplacement en cas de sollicitation de la mémoire.
  • Concurrence avec les opérations du système de fichiers telles que les sauvegardes et les copies de fichiers, comme nous l'avons vu dans cet exemple.

Autres remarques - trop profondément dans les détails...

Comment les données sont-elles collectées?

La commande utilisée dans ^SystemPerformance pour une collecte de 24 heures (17 280 secondes) avec des coches toutes les 5 secondes est la suivante:

free -m -s 5 -c 17280 | awk '{now=strftime(""%m/%d/%y %T""); print now "" "" $0; fflush()}' > ","/filepath/logs/20250315_000100_24hours_5sec_12.log

0
0 22
Article Lorenzo Scalese · Sept 18, 2025 5m read

IrisTest est un outil léger, puissant et facile à utiliser, conçu pour simplifier la génération de rapports de tests unitaires. Il comprend un interpréteur de commandes interactif et une API pour faciliter la communication, permettant aux développeurs de gérer et de générer facilement des rapports pour leurs tests dans des formats variés. Que vous déboguez ou créiez des rapports détaillés pour analyse, IrisTest rend le processus fluide et efficace!

Table des matières

  • Sommaire
  • Caractéristiques principales
  • Commandes shell
  • Utilisation
  • Formats de rapport
  • Installation
  • Configuration
  • Exemples
  • Commandes
  • Contribution
  • Licence

Sommaire

IrisTest est un outil en ligne de commande conçu pour générer des rapports de tests unitaires dans des formats variés avec une configuration minimale. Il est particulièrement pratique pour les développeurs et les testeurs qui recherchent un moyen efficace de suivre les résultats des cas de test, de générer des rapports et d'automatiser les workflows d'assurance qualité. Compatible avec une utilisation interactive et automatisation basée sur une API, IrisTest offre une flexibilité maximale.


Caractéristiques principales

  • 📊 Génération de rapports multiformats – Exportez vos rapports au format HTML, XML, JUnitXML, Allure, JSON, CSV, etc.
  • 🖥️ Interpréteur de commandes interactif – Pour exécuter des tests, gérer les configurations et afficher les résultats directement depuis l'interpréteur de commandes.
  • 🔌 Integration API – Pour automatiser vos workflows de génération de rapports de test.
  • ⚙️ Configuration simple – Pour personnaliser facilement les formats de sortie, les répertoires et les identifiants de test.
  • 🕒 Traçage de l'historique des commandes – Pour retracer vos actions avec les journaux d'historique de l'interpréteur de commandes.

Commandes de l'interpréteur de commandes

L'interpréteur de commandes interactif est l'endroit où IrisTest est le plus performant pour les opérations manuelles. Au lancement, une interface de l'interpréteur de commandes facile à utiliser s'affiche:

═════════════════════════════════════════════════════════════════════════════════════════════════
|| Bienvenue dans l'interpréteur de commandes iristest 0.1.0                                                                      ||
|| Saisissez “q” ou “quit” pour quitter l'interpréteur de commandes et “?” ou “help” pour afficher les commandes disponibles.     ||
||                                                                                                                                ||
|| ➤ Instance      : IRISHEALTH2025COM                                                                                            ||
|| ➤ System        : C11V344                                                                                                      ||
|| ➤ System Mode   : DEVELOPMENT                                                                                                  ||
|| ➤ Logged in     : _SYSTEM                                                                                                      ||
|| ➤ Session Start : 2025-07-27 13:07:52                                                                                          ||
════════════════════════════════════════════════════════════════════════════════════════════════════════════

Utilisation

Syntaxe de la commande

Pour exécuter IrisTest:

ziristest [OPTIONS]

Options disponibles

  • -i, --id <UnitTestId> – Définition d'un identifiant de test unique
  • -o, --output <FORMAT> – Sélection d'un ou plusieurs formats de rapport: html, xml, junitxml, allure, shell, json, csv, text
  • -d=<DIR>, --output-dir=<DIR> – Définition du répertoire de sortie (par exemple: ./reports)

Configuration

Vous pouvez configurer le chemin d'accès à chaque rapport IrisTest via

do##class(IrisTest.Report.Base).DefineFilePath("html", "C:\html\")

Affichage de la version et des paramètres d'IrisTest à l'aide de:

INFO

Exemples

Génération d'un rapport HTML unique:

ziristest --id=123 --output=html

Génération de plusieurs formats:

ziristest -i=123 -o=html,xml,junitxml

Enregistration dans un répertoire particulier:

ziristest -i=123 -o=html,xml,junitxml --output-dir=./reports

Commandes

CommandeDescription
CLEAREffacer l'écran de l'interpréteur de commandes
CONFIGAffichage de la configuration du système
DEL <ID>Suppression d'un cas de test
HELPAffichage du menu aide 
HISTAffichage de l'historique des commandes
HIST CLEAREffacement de l'historique des commandes
INFOAffichage de la version/date de l'interpréteur de commandes
RUNALLExécution de tous les cas de test disponibles
SHOW <ID>Affichage des résultats d'un test spécifique
SHOWALLAffichage de tous les résultats des tests
QUITSortie de l'interpréteur de commandes

Formats de rapport

FormatDescription
htmlRapport de test élégant, prêt à être utilisé dans un navigateur
xmlFormat XML standard
junitxmlCompatible avec les outils JUnit
allure Rapport de test compatible avec Allure
jsonDonnées structurées pour les API et les outils
csvDonnées simples prêtes à être utilisées dans un tableur
shellRésultat minimal, compatible avec les terminaux
textTexte brut pour les besoins élémentaires
0
0 21
Article Lorenzo Scalese · Août 27, 2025 4m read

Cet excellent article a récemment déclenché une discussion privée, et j'aimerais partager certaines de mes réflexions à ce sujet.
La question motivante se résume ainsi : pourquoi devons-nous absolument établir des règles ou des conventions de codage ? Où est passée la merveilleuse époque des artistes-programmeurs de la Renaissance qui traçaient leur propre voie, avant d'être supplantés par les artisans, puis (pire encore) par IA?
En bref, il existe plusieurs raisons pour expliquer l'utilité des normes et des directives de codage, et les artistes-programmeurs de la Renaissance n'ont pas complètement disparu.

Raison 1: De nos jours, lorsque vous enseignez à un artiste, à un peintre débutant, vous commencez par lui demander de colorier à l'intérieur des lignes. Ce peintre sera peut-être brillant un jour, mais pas encore. Si vous voulez apprendre à créer des œuvres d'art, vous devez d'abord faire cela, puis apprendre progressivement les techniques et les concepts des maîtres. Ensuite, si vous êtes vraiment doué ou si vous avez beaucoup de chance, vous pourrez développer votre propre style et créer quelque chose de nouveau que le reste du monde voudra imiter. Mais vous devez commencer par suivre les règles.

(remarque: j'ai trouvé cela sur l'Internet ; aucun de mes enfants ne sait encore faire ça.)

Raison 2: Si vous travaillez dans un domaine où votre créativité doit s'intégrer à celle des autres, comme dans un ouvrage de patchwork, vous devez établir certaines règles, sans quoi votre œuvre ne fonctionnera pas, car les pièces ne s'emboîteront jamais. Vous pouvez soit convenir du principe selon lequel "nous faisons tous des carrés de 40 cm", soit travailler encore plus étroitement et en collaboration avec un groupe d'autres personnes, ce qui n'est peut-être pas la meilleure option pour un artiste solitaire qui passe quatre ans à peindre un plafond. (Surtout si ses responsables décident de faire appel à quelques autres artistes solitaires pour "l'aider.”)

(C'est ChatGPT qui m'a inspiré cette image. Toutes mes excuses aux artistes réels.)

Raison 3: Si vous travaillez sur un élément de codage existant, particulièrement ancien ou particulièrement génial, vous êtes souvent confronté à un dilemme : comprendre et accepter l'intention et l'élégance de la conception et de l'exécution de l'auteur , ou simplement dire "C'est moi le responsable maintenant, on va tout jeter et faire à ma façon." Je vais vous donner un exemple : une application web sur laquelle j'ai travaillé se sert beaucoup de "pages XML" où un trio (pageName.xml, pageName.js, pageName.mac) forme une page. Le fichier pageName.xml était, jusqu'à récemment, probablement écrit dans cet étrange dialecte "WD-xsl" qui ressemble beaucoup au XSLT standard mais qui ne fonctionne que dans Internet Explorer Microsoft Edge s'il se fait passer pour IE5. Le fichier pageName.js contient probablement environ 40 fois plus de "frame" que nécessaire et comfortable. Le fichier pageName.mac est probablement rempli de syntaxe à points, de commandes abrégées et de macros incohérentes. Si vous êtes un développeur débutant, vous pleurez et vous fuyez parce que cela n'a aucun sens. Si vous êtes un développeur expérimenté, vous lisez le code, vous essayez de le comprendre, puis vous décidez que “c'est dégoûtant, je vais faire mieux autrement” – mais alors la personne qui travaillera ensuite sur l'application devra apprendre le paradigme original et votre nouveau paradigme astucieux. Faites cela pendant 20 ans, et un véritable cauchemar vous attend. Mais si vous êtes vraiment un artiste expert, vous pouvez vous lancer dans la restauration artistique: observez l'élégance de la structure d'origine et travaillez en respectant celle-ci, en corrigeant délicatement les éléments architecturaux sans importance qui font pleurer le nouveau développeur et poussent le développeur senior à appuyer sur "Supprimer" sans risquer de créer un chaos fragmenté ou de passer des années  à tout repeindre. Peut-être même réaliserez-vous vos propres œuvres dans le style du grand maître. La leçon la plus importante à tirer de ce conte est qu'en tant qu'artiste véritable, vous êtes tout à fait intéressé à produire un travail d'une telle qualité que quelqu'un qui en hériterait sans passer des années pour s'entraîner ne déciderait pas de tout jeter. Les "petites choses" comme le style de code, la lisibilité et la gestion de la dette technique contribuent grandement à préserver l'architecture et même chaque ligne de code.

En bref:

  • Les règles et les conventions nous permettent de former de nouveaux artistes brillants bien avant qu'ils ne le deviennent
  • Les règles et les conventions nous offrent un cadre fixe dans lequel nous pouvons exercer notre créativité sans perdre de temps à essayer de nous entendre avec les autres
  • Les règles et les conventions nous permettent de créer quelque chose de beau qui ne sera pas abandonné par la première personne qui en héritera
0
0 17
Article Lorenzo Scalese · Août 11, 2025 4m read

Mon intention est de montrer à quel point il est simple de générer un tableau de recherche en tenant compte des informations qui arrivent dans notre messagerie HL7. Certes, le tableau de recherche de messages HL7 fourni par IRIS est suffisant pour la plupart des recherches que nous souhaitons effectuer, mais nous avons toujours ce champ spécial de notre HIS, LIS, RIS, etc. dans lequel nous aimerions rechercher. Mais il se trouve dans un segment en dehors de cette table. Ce champ nous oblige à générer une recherche spécifique en utilisant les critères de recherche avancés. Nous aurons certainement beaucoup de messages et nous devrons également filtrer par date et heure pour éviter un délai d'expiration.
 

Comment résoudre ce problème?

En créant notre propre table de recherche.

Et comment créer notre table de recherche?

Comme nous l'avons toujours fait, EN COPIANT! Dans notre vie, nous rencontrerons 3 ou 4 génies, ce seront ceux qui inventeront, créeront, visualiseront l'au-delà, etc. Si vous êtes l'un d'entre eux, vous savez déjà comment faire. Vous serez dans le top 100 du classement mondial, vous aurez plus de 500 000 points et tout un arsenal de produits InterSystems chez vous. Les autres feront comme nous avons fait toutes ces années avant le CHAT GPT, copier sans aucune honte, la tête bien haute. Alors, commençons.

Première étape

Nous allons générer notre classe dans Visual Studio Code, ouvrir la classe EnsLib.HL7.SearchTableque fournie par IRIS , et copier tout le contenu de la classe CTRL+C. Nous allons ensuite dans notre classe et CTRL+V.
 

Très important: nous copierons également les Extends, les ClassType, Inheritance. Nous ne laissons rien de côté, à l'exception du Copyright, qui ne nous intéresse pas.  Nous n'avons pas assez d'ego pour laisser notre empreinte sur un travail remarquable.

Si vous êtes d'ancienne génération, cet écran vous semblera très familier. 😉

Deuxième étape

Maintenant nous faisons opérer la magie: nous ajoutons nos champs, supprimons ceux qui ne nous intéressent pas, et pouvons même les traduire en espagnol pour paraître plus compétents.
 

Dans ce cas, j'ai ajouté la source, la destination, l'événement, le service et le dossier patientau message, et c'est là que vous intervenez. Ajoutez tous les champs que vous souhaitez/devez ajouter. Comme vous le voyez, vous pouvez ajouter le segment, le champ et le composant sous forme numérique. Pour moi, c'est plus facile que d'ajouter le nom en anglais, mais gardez à l'esprit que nous ne pouvons garantir que le segment 1 sera le MSH ; le reste dépendra de chaque message. Donc, dans le cas des segments, il est préférable d'utiliser le code (PID, PV1).

Après tout ce travail fastidieux, nous compilons et avons maintenant notre table de recherche. Il ne nous reste plus qu'à l'attribuer aux composants de notre production, évidemment les composants HL7. Passons à la suite.

Troisième étape

Nous ouvrons notre production et recherchons les composants HL7, nous allons dans les paramètres supplémentaires et nous trouvons notre table de recherche, nous la sélectionnons et procédons à l'application des modifications. À partir de ce moment, tous les messages qui entrent par ce composant seront stockés dans notre table de recherche. Si nous ajoutons ultérieurement d'autres champs à la table de recherche, ils seront enregistrés à partir de la compilation de celle-ci, et pour les messages précédents, ce champ sera vide.

Qu'ils soient entrants ou sortants, dans les opérations de type HL7, nous avons également la table de recherche. Normalement, dans les composants de sortie, rien n'est activé par défaut, mais si nous souhaitons contrôler la sortie, cette option est également disponible.

Grâce à ces étapes simples, notre production est prête à enregistrer les données qui nous intéressent. Il ne nous reste plus qu'à générer les requêtes, et pour cela, nous allons dans la visionneuse de messages.

Démonstration

Dans la messagerie, dans les critères, lorsque nous sélectionnons les tables de recherche, nous retrouvons ce que nous avons fait. Lorsque nous le sélectionnons, nous pouvons maintenant commencer à jouer avec nos champs

Je vais vous donner plusieurs exemples, mais ne limitez pas votre imagination. Je me suis laissé emporter. Les champs du tableau de recherche vous indiqueront les limites.


1. Recherche sans condition. Nous voulons uniquement voir l'événement du message entrant et l'identifiant du message.

2. Recherche par événement affichant les codes du patient et de l'épisode


3. Recherche par épisode montrant le service, le nom du patient et le type de message HL7


À partir de là, c'est à vous de jouer, profitez-en bien 😋

0
0 30
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 Lorenzo Scalese · Juin 18, 2025 5m read

Le bon vieux temps

La classe %Library.DynamicObject existe dans IRIS depuis bien avant que IRIS ne devienne IRIS. Si vous l'utilisez depuis l'époque de Cache, vous souhaiterez peut-être vous familiariser avec certaines de ses modifications.

Dans Cache 2018, la méthode %Get n'avait qu'un seul argument. Il s'agissait de la clé permettant de récupérer les données dans le JSON. Ainsi, si votre objet JSON s'appelait myObj, cela ressemblerait à ceci:

{
    “mybool”:true,
    “mynum”:1234,
    “mystring”:”Hello World!”
}

L'exploitation de myObj.%Get(“mybool”) renverrait 1, myObj.%Get(“mynum”) renverrait 1234 et myObj.%Get("mystring") renverrait la chaîne "Hello World!" 

La définition de ces paramètres, en revanche, nécessitait un peu plus de travail. Par exemple, l'attribution d'une propriété JSON à 0 pouvait signifier le nombre 0, une valeur booléenne signifiant faux ou une chaîne littérale "0". C'est pourquoi la méthode %Set avait toujours un troisième argument facultatif. Pour créer l'objet JSON mentionné ci-dessus, nous pouvions utiliser le code suivant:

set myObj = ##class(%Library.DynamicObject).%New()
do myObj.%Set(“mybool”,1,”boolean”)
do myObj.%Set(“mynum”,1234,”number”)
do myObj.%Set(“mystring”,”Hello World!”,”string”)

Cependant, dans ce cas, nous pourrions également omettre le troisième argument des deux derniers. Ainsi, 1234 serait reconnu comme un nombre, car il n'est pas entre guillemets, tandis que "Hello World!" serait identifié comme une chaîne, car il est entre guillemets. Si nous voulions ajouter la valeur 1234 à l'objet en tant que chaîne, nous pourrions changer la chaîne en nombre. Nous pourrions également spécifier le type "null". Dans ce cas, la valeur doit être "". En pratique, cependant, nous ajoutons souvent ces valeurs à partir de variables dans notre code ObjectScript. Pour cette raison, il peut être préférable de spécifier cet argument, juste au cas où la variable serait une chaîne ou un nombre, afin de garantir que notre JSON arrive à destination correctement encodé.

Comment j'ai appris à ne plus m'inquiéter à propos de <MAXSTRING> et aimer JSON

Comme l'a dit un jour le sage Billy Joel, ""Le bon vieux temps n'était pas toujours bon, demain n'est pas aussi mauvais qu'il n'y paraît." La liste des types pour %Set s'est allongée, et la méthode %Get a acquis deux nouveaux arguments. Ce qui est crucial, c'est qu'elles prennent toutes deux en charge le type "stream". Si vous avez déjà traité de grandes quantités de données JSON, vous avez probablement déjà rencontré une erreur lorsque les données contenues dans le JSON dépassaient la longueur maximale autorisée pour une chaîne dans IRIS. La nouvelle méthode %Get vous permet de spécifier deux arguments supplémentaires, une valeur par défaut et un type. Votre ancien code continue toutefois de fonctionner, car ces deux arguments sont facultatifs et, s'ils sont omis, les méthodes fonctionnent exactement comme en 2018. Si rien n'est trouvé pour la clé donnée, la valeur par défaut est renvoyée. La fonction type fonctionne de manière similaire à l'argument type de la méthode %Set. Vous pouvez également spécifier le type de données que vous récupérez. Prenons l'exemple suivant du bloc try/catch:

try{
    Set mydata = myObj.%Get(“mydata”,”N/A”)
}
catch ex{
    if ex.Name = “<MAXSTRING>”{
        set mydata = myObj.%Get(“mydata”,,”stream”)
    }
}

Il tentera de définir mydata à la valeur située à l'intérieur de "mydata" dans l'objet JSON. Si cet élément n'existe pas, il renverra "N/A" à la place. Si cet élément est trop long pour une chaîne, le système lèvera une exception vers le bloc catch. Nous devons vérifier le nom de cette exception, car si une exception différente s'est produite, il ne serait pas logique d'essayer d'obtenir les données sous forme de flux. Vous pouvez en savoir plus sur la gestion des exceptions ici. Si la valeur est , nous spécifions que nous voulons récupérer mydata sous forme de flux. La récupération des données sous forme de flux renvoie un objet de la classe %Stream.DynamicCharacter. Cela ne déclenche jamais d'exception , mais peut générer une exception si la limite de mémoire du processus est dépassée.

Si vous suivez l'approche décrite ci-dessus, vous ne saurez pas si mydata dans le code est une chaîne ou un flux. Cela signifie que vous devrez suivre le code similaire à celui ci-dessous:

if$ISOBJECT(mydata){
    //Traitement des flux ici
}
else{
    //Traitement des chaînes ici
}

Vous pouvez également utiliser l'option stream chaque fois afin de vous assurer que vous avez toujours un flux à votre disposition. Cependant, cela entraînerait une utilisation inutile des ressources et alourdirait votre code.

Une autre option consiste à ajouter un flux à un objet dynamique à l'aide de %Set. Consultez l'exemple suivant:

set mystream = ##class(%Stream.FileBinary).%New()
do mystream.LinkToFile(“/path/to/your/file”)
do myObj.%Set(“mydata”,mystream,”stream”)

Les données de votre fichier seront désormais enregistrées dans le champ mydata de votre objet dynamique.

%Set et %Get encodent et décodent également les chaînes à l'aide de l'encodage Base64. 

Gardez toujours à l'esprit que l'encodage Base64 est un encodage et non un cryptage ! Il n'y a pas de clés secrètes ni de mots de passe pour décoder votre message, et il est facilement réversible. Par conséquent, vous devez toujours utiliser un protocole crypté, tel que TLS ou HTTPS, pour la transmission ! Base64 est utilisé pour transmettre des caractères non ASCII de manière à ce que les systèmes ASCII puissent les recevoir et les transmettre.

Maintenant que cette remarque importante est faite, nous pouvons enfin voir comment cela fonctionne. Si nous apportons une petite modification à l'exemple de code précédent, le contenu du flux de fichiers sera encodé en Base64.

set mystream = ##class(%Stream.FileBinary).%New()
do mystream.LinkToFile(“/path/to/your/file”)
do myObj.%Set(“mydata”,mystream,”stream>base64”)

D'autre part, si les données du fichier étaient déjà encodées en Base64 et que nous voulions les convertir en données décodées, il suffirait de changer un seul caractère.

set mystream = ##class(%Stream.FileBinary).%New()
do mystream.LinkToFile(“/path/to/your/file”)
do myObj.%Set(“mydata”,mystream,”stream<base64”)

Les signes " supérieur à" ou "inférieur à" indiquent toujours la direction dans laquelle la conversion a lieu. Si nous convertissons un flux non codé en une chaîne Base64, le signe pointera vers Base64. Si nous convertissons un flux codé en Base64 en un flux non codé, le signe pointera de Base64 vers le flux. La même fonctionnalité existe pour les chaînes lorsque string>base64 et string

set something = myObj.%Get(“something”,”NA”,”string>base64”)

Si l'élément "quelque chose" existe, il sera renvoyé sous sa forme encodée en Base64. Cependant, s'il n'existe pas, "NA" sera renvoyé sans être encodé.

Il existe une restriction concernant l'option d'encodage Base64. Seuls les caractères dont le code est compris entre 0 et 255 peuvent être encodés en Base64. Les codes de caractères supérieurs à 255 entraîneront une exception <WIDE CHAR>. FPar exemple, la ligne suivante provoquera une telle exception:

set mychar = $C(256)
do myobj.%Set(“mychar”,mychar,”string>base64”)

Alors, j'ai entendu dire que vous aimiez JSON . . .

Parfois, il y a du JSON à l'intérieur de votre JSON. La manière par défaut de gérer cette situation est généralement celle que vous choisiriez. Cependant, une autre option a été ajoutée à l'argument de type pour traiter un cas d'utilisation différent. Il s'agit du type "json". Regardez l'objet JSON suivant:

{
    “mystring”:”Hello World!”,
    “mynumber”:1234,
    “myjson”:{
        “subitem1”:”Hello Mars!”,
        “subitem2”:”Hello Stars!”
    }
}

En règle générale, lorsque vous rencontrez ce problème, vous devez utiliser la méthode %Get avec la clé « myjson » pour obtenir un objet dynamique. Voici un exemple:

set myjson = myobj.%Get(“myjson”)
write myjson.%Get(“subitem1”)

La ligne ci-dessus écrirait "Hello Mars!". C'est le cas le plus courant dans cette situation. Cependant, dans certains cas, vous préférerez peut-être obtenir le JSON réel contenu dans cet élément sous forme de chaîne. Dans ce cas, vous pouvez procéder comme suit:

set myjson = myobj.%Get(“myjson”,,”json”)
write myjson

La chaîne JSON sera écrite exactement telle qu'elle est:

{“subitem1”:”Hello Mars!”,”subitem2”:”Hello Stars!”}

Cela peut s'avérer utile dans les cas où nous voulons transmettre le JSON tel quel à un autre processus. Notez que, contrairement à tous les autres types nouveaux, celui-ci n'est pris en charge que pour la méthode %Get, et non pour la méthode %Set.

Hourra, hourra, les massives!

Jusqu'à présent, nous avons discuté de ces nouveaux objets dans le contexte de la classe %Library.DynamicObject, mais ils sont également pris en charge pour la classe %Library.DynamicArray. Dans cette classe, %Set et %Get prennent en charge les mêmes arguments de type que dans la classe %Library.DynamicObject. La classe de tableaux dynamiques dispose toutefois d'une méthode %Push supplémentaire. Elle prend en charge les mêmes types que %Set, à l'exception du type JSON.

Sans plus attendre, c'est probablement le bon moment pour revoir vos anciens codes et implémenter ces changements à votre avantage!

0
0 35
Article Lorenzo Scalese · Juin 10, 2025 11m read

Introduction

Les performances des bases de données sont devenues essentielles à la réussite des environnements applicatifs modernes. Il est donc indispensable d'identifier et d'optimiser les requêtes SQL les plus exigeantes en ressources afin de garantir une expérience utilisateur fluide et la stabilité des applications. 

Cet article présente une approche rapide pour analyser les statistiques d'exécution des requêtes SQL sur une instance InterSystems IRIS afin d'identifier les domaines à optimiser au sein d'une macro-application.

Au lieu de nous concentrer sur la surveillance en temps réel, nous allons mettre en place un système qui collecte et analyse les statistiques précalculées par IRIS une fois par heure.  Cette approche, bien qu'elle ne permette pas de surveillance instantanée, offre un excellent compromis entre la richesse des données disponibles et la simplicité de mise en œuvre. 

Nous utiliserons Grafana pour la visualisation et l'analyse des données, InfluxDB pour le stockage des séries chronologiques et Telegraf pour la collecte des métriques.  Ces outils, reconnus pour leur puissance et leur flexibilité, nous permettront d'obtenir un aperçu clair et exploitable.

Plus précisément, nous détaillerons la configuration de Telegraf pour récupérer les statistiques. Nous configurerons également l'intégration avec InfluxDB pour le stockage et l'analyse des données, et créerons des tableaux de bord personnalisés dans Grafana. Cela nous aidera à identifier rapidement les requêtes nécessitant une attention particulière.

Pour faciliter l'orchestration et le déploiement de ces différents composants, nous utiliserons Docker.

logos.png

Conditions préalables

Avant de commencer, assurez-vous d'avoir les éléments suivant:

  • Git: Git est nécessaire pour cloner le référentiel du projet contenant les fichiers de configuration et les scripts.
  • Docker ou Docker Desktop: Docker peut être utilisé pour conteneuriser les applications InfluxDB, Telegraf et Grafana, ce qui facilite leur déploiement et leur gestion.
  • Instance InterSystems IRIS:Au moins la version 2022.1, idéalement 2023.1 ou supérieure, avec le package sql-stats-api installé.  Ce package est essentiel pour exposer les statistiques SQL IRIS et permettre à Telegraf de les collecter. [Lien vers OpenExchange]

Bien que nos fichiers docker-compose incluent une instance IRIS, celle-ci ne contiendra aucune donnée statistique SQL, car elle vient d'être créée et démarrée. Elle ne constituera donc pas un choix pratique pour tester le système. C'est pourquoi nous vous recommandons vivement de disposer d'une autre instance IRIS "active" (celle qui contient l'historique des requêtes SQL) afin de pouvoir visualiser des données réelles et tester l'outil d'analyse.

À propos des statistiques

IRIS collecte des statistiques d'exécution des requêtes SQL à une granularité horaire et quotidienne. Les statistiques horaires identifient les variations de performances tout au long de la journée, tandis que les statistiques quotidiennes fournissent une vue d'ensemble de l'activité de la base de données.

Vous trouverez ci-dessous les données que nous collectons pour chaque requête SQL:

  • Nombre d'exécutions: indique le nombre de fois où la requête a été exécutée.
  • Durée totale d'exécution: mesure la durée totale d'exécution de la requête.
  • Variance des temps d'exécution: utilisée pour identifier les variations de performances et les problèmes ponctuels.
  • Nombre total de lignes renvoyées (RowCount): disponible pour IRIS 2023.1 et versions ultérieures, cette métrique indique le nombre total de lignes renvoyées par la requête. Elle peut vous aider à identifier les requêtes exigeantes en ressources.
  • Nombre total de commandes exécutées: également disponible pour IRIS 2023.1 et versions ultérieures, cette métrique facilite une analyse plus détaillée de l'activité de la base de données et identifie les requêtes qui pourraient être optimisées en réduisant le nombre d'opérations.

Ces informations sont accessibles via les tables suivantes:

  • INFORMATION_SCHEMA.STATEMENT_DAILY_STATS
  • INFORMATION_SCHEMA.STATEMENT_HOURLY_STATS

Ces tables sont disponibles depuis IRIS 2022.1. Vous trouverez ci-dessous un exemple de requête SQL permettant de récupérer des statistiques:

SELECT ds.* 
FROM INFORMATION_SCHEMA.STATEMENT_DAILY_STATS ds
INNERJOIN INFORMATION_SCHEMA.STATEMENTS st On ds.Statement = st.Hash


SELECTDATEADD('hh',"Hour",$PIECE(hs."Day",'||',2)) As DateTime, hs.*
FROM INFORMATION_SCHEMA.STATEMENT_HOURLY_STATS hs
INNERJOIN INFORMATION_SCHEMA.STATEMENTS st On $PIECE(hs."Day",'||',1) = st.Hash

Pour les versions plus anciennes que IRIS 2022.1, je recommande vivement l'article de David Loveluck, qui explique comment récupérer des statistiques similaires.

Architecture

Le projet repose sur l'interaction de quatre composants clés : IRIS, Grafana, InfluxDB et Telegraf. Le diagramme ci-dessous illustre l'architecture globale du système et le flux de données entre les différents composants:

archi.png

  • InterSystems IRIS: il s'agit de l'instance que nous utiliserons pour récupérer les statistiques.
  • Package sql-stats-api: ce package ObjectScript expose les données statistiques IRIS via l'API REST. Il offre deux formats de sortie : JSON pour une utilisation générale et Line Protocol, un format optimisé pour l'ingestion rapide de données de séries chronologiques dans InfluxDB.
  • Telegraf: il s'agit d'un agent de collecte de métriques qui fournit le lien entre IRIS et InfluxDB. Dans ce projet, nous utiliserons deux instances de Telegraf:
    • un agent interroge périodiquement l'API REST IRIS pour récupérer les statistiques SQL en temps réel.
    • Un autre agent fonctionne en mode "scan de répertoire". Il surveille un répertoire contenant les fichiers stockés et les transmet à InfluxDB, ce qui permet d'intégrer les données inaccessibles via l'API REST.
  • InfluxDB:  cette base de données de séries chronologiques stocke et gère les statistiques SQL collectées par Telegraf, car son architecture est optimisée pour ce type de données. InfluxDB offre également une intégration native avec Grafana, ce qui facilite la visualisation et l'analyse des données. Nous avons préféré InfluxDB à Prometheus, car ce dernier est davantage axé sur la surveillance en temps réel et n'est pas bien adapté au stockage de données agrégées, telles que les sommes ou les moyennes horaire ou journalière, qui sont essentielles pour notre analyse.
  • Grafana: il s'agit d'un outil de visualisation qui permet de créer des tableaux de bord personnalisés et interactifs pour analyser les performances SQL. Il récupère les données d'InfluxDB et offre une variété de graphiques et de widgets pour visualiser les statistiques de manière claire et exploitable.

Installation

Commencez par cloner le référentiel:

git clone https://github.com/lscalese/iris-sql-dashboard.git
cd irisiris-sql-dashboard

Configuration de l'environnement

Ce projet utilise Docker pour orchestrer Grafana, InfluxDB, Telegraf et IRIS. Pour des raisons de sécurité, les informations sensibles telles que les clés API et les mots de passe sont stockées dans un fichier .env.

Créez le fichier .env en utilisant l'exemple fourni ci-dessous:

cp .env.example .env

Éditez le fichier .env pour configurer les variables:

Configuration des variables

  • TZ: fuseau horaire. Il est recommandé de modifier cette variable en fonction de votre fuseau horaire afin de garantir l'exactitude de l'horodatage des données.
  • DOCKER_INFLUXDB_INIT_PASSWORD: mot de passe administrateur permettant d'accéder à InfluxDB.
  • IRIS_USER: il s'agit d'un utilisateur IRIS de l'instance Docker IRIS (_system par défaut).
  • IRIS_PASSWORD : Il s'agit du mot de passe de l'instance IRIS Docker (SYS par défaut).

Les clés API permettent les connexions suivantes:

  • GRAFANA_INFLUX_API_KEY : Grafana <-> InfluxDB.
  • TELEGRAF_INFLUX_API_KEY : Telegraf <-> InfluxDB.

Génération de clés API

Pour des raisons de sécurité, InfluxDB nécessite des clés API pour l'authentification et l'autorisation.  Ces clés sont utilisées pour identifier et autoriser divers composants (Telegraf, Grafana) à accéder à Influx DB.

Le script init-influxdb.sh, inclus dans le référentiel, facilite la génération de ces clés.  Il sera exécuté automatiquement lors du premier démarrage du conteneur infxludb2:

docker compose up -d influxdb2

Après quelques secondes, le fichier .env sera mis à jour avec vos clés API générées.

Remarque: cette étape ne doit être effectuée que lors du premier démarrage du conteneur.

Vérifiez si vous avez accès à l'interface d'administration InfluxDB via l'URL http://localhost:8086/

Connectez-vous avec le nom d'utilisateur "admin" et le mot de passe spécifiés dans la variable d'environnement "DOCKER_INFLUXDB_INIT_PASSWORD" dans le fichier ".env". Lorsque vous naviguez dans "Load Data >> Buckets”, vous devriez découvrir un bucket "IRIS_SQL_STATS" préconfiguré.   

influxdb-2.png
En parcourant "Load Data >> API Tokens", vous devriez trouver nos deux clés API : "Grafana_IRIS_SQL_STATS" et "Telegraf_IRIS_SQL_STATS":

influxdb-3.png

L'environnement est prêt maintenant, et nous pouvons passer à l'étape suivante!

Démarrage

L'environnement étant configuré et les clés API générées, vous pouvez enfin lancer l'ensemble de conteneurs. Pour ce faire, exécutez la commande suivante dans le répertoire racine du projet:

docker compose up -d

Cette commande lancera en arrière-plan tous les services définis dans le fichier docker-compose.yml : InfluxDB, Telegraf, Grafana et l'instance IRIS.

Tableau de bord Grafana

Grafana est désormais disponible à l'adresse http://localhost:3000.

Connexion à Grafana

Ouvrez votre navigateur Web et rendez-vous à l'adresse http://localhost:3000. Le nom d'utilisateur et le mot de passe par défaut sont admin/admin. Cependant, vous serez invité à modifier le mot de passe lors de votre première connexion.

grafana-login.png

Vérification de la source de données InfluxDB

La source de données InfluxDB est préconfigurée dans Grafana. Il vous suffit de vérifier son bon fonctionnement.

Accédez à “Connections > Data sources (sources de données)”.

grafana-ds.png

Vous devriez voir une source de données nommée “influxdb”.Cliquez dessus pour la modifier.

Cliquez ensuite sur “Save & Test”. Le message “Datasource is working. 1 bucket found” (La source de données fonctionne. 1 bucket trouvé) devrait maintenant s'afficher à l'écran.

grafana-datasources-influxdb.png

Exploration des tableaux de bord

À ce stade, vous avez vérifié que la communication entre Grafana et InfluxDB est établie, ce qui signifie que vous pouvez explorer les tableaux de bord prédéfinis.

Passez à “Dashboards” (tableaux de bord).

grafana-dashboard-list.png

Vous trouverez deux tableaux de bord prédéfinis:

  • InfluxDB - SQL Stats: ce tableau de bord affiche des statistiques générales sur l'exécution des requêtes SQL, par exemple le nombre d'exécutions, le temps total d'exécution et la variance du temps d'exécution.
  • InfluxDB - SQL Stats Details: ce tableau de bord fournit des informations plus détaillées sur chaque requête SQL, par exemple le nombre total de lignes renvoyées ou de commandes exécutées.

Pourquoi les tableaux de bord sont vides

Si vous ouvrez les tableaux de bord, vous verrez qu'ils sont vides.  Cela s'explique par le fait que notre agent Telegraf est actuellement connecté à l'instance IRIS fournie dans le référentiel Docker, dont les tables ne contiennent aucune donnée statistique. Les statistiques SQL ne sont collectées que si l'instance IRIS est active et conserve un historique des requêtes SQL.

Dans la section suivante, nous allons voir comment injecter des données dans l'instance IRIS afin d'afficher les statistiques dans Grafana.

 

Telegraf

Le système de surveillance utilise deux agents Telegraf ayant des rôles spécifiques:

  • telegraf-iris.conf:cet agent collecte des données en temps réel à partir d'une instance IRIS active. Il interroge l'API REST IRIS pour récupérer des statistiques SQL afin de les envoyer à InfluxDB.
  • telegraf-directory-scan.conf: cet agent intègre les données historiques stockées dans des fichiers. Il surveille le répertoire ./telegraf/in/, lit les fichiers contenant des statistiques SQL et les envoie à InfluxDB.

Pour collecter des données en temps réel, il faut connecter Telegraf à une instance IRIS active sur laquelle le package sql-stats-api est installé.  Ce package expose les statistiques SQL via une API REST, ce qui permet à Telegraf d'y accéder.

Configuration de telegraf-iris.conf

Pour connecter Telegraf à votre instance IRIS, vous devez modifier le fichier ./telegraf/config/telegraf-iris.conf. Vous trouverez ci-dessous un exemple de configuration:

[[inputs.http]]  ## Une ou plusieurs URL à partir desquelles lire les métriques formatées  urls = [    "http://iris:52773/csp/sqlstats/api/daily",    "http://iris:52773/csp/sqlstats/api/hourly"  ]  ## Méthode HTTP  method = "GET"
  ## En-têtes HTTP facultatifs  headers = {"Accept" = "text/plain"}  ## Informations d'identification pour l'authentification HTTP de base (facultatif)  username = "${IRIS_USER}"  password = "${IRIS_PASSWORD}"  data_format = "influx"

Assurez-vous que${IRIS_USER} et ${IRIS_PASSWORD} sont correctement définis dans votre dossier .env file.
Remarque: Vous pouvez copier le fichier et modifier les paramètres pour connecter Telegraf à plusieurs instances IRIS.

Redémarrage de Telegraf:

Après avoir modifié le fichier de configuration, il est nécessaire de redémarrer le conteneur Telegraf pour que les modifications prennent effet:

docker compose up -d telegraf --force-recreate

Récupération des données historiques

Pour récupérer les statistiques SQL historiques, utilisez la méthode ObjectScript CreateInfluxFile sur votre instance IRIS:

; Adaptez le chemin à vos besoinsSet sc = ##class(dc.sqlstats.services.SQLStats).CreateInfluxFile("/home/irisowner/dev/influxdb-lines.txt",,1)

Ce script enregistre l'historique des statistiques SQL dans des fichiers texte dont la longueur maximale est de 10 000 lignes par fichier. Vous pouvez ensuite placer ces fichiers dans le répertoire ./telegraf/in/ afin de les traiter et de les injecter dans InfluxDB.

Vérification de l'injection des données

Vous pouvez vérifier que les données ont été correctement injectées dans InfluxDB à l'aide de l'interface Web. Accédez à "Data Explorer" (Explorateur de données) et vérifiez:

influxdb-explorer.png

Visualisation des données dans Grafana

Une fois les données injectées, vous pouvez les afficher dans vos tableaux de bord Grafana fournis.

grafana-dashboard-daily-stats.png

grafana-dashboard-daily-details.png

Nous sommes arrivés à la fin de notre article. J'espère qu'il vous a été utile et qu'il vous avez appris à configurer facilement un système de surveillance et d'analyse des statistiques SQL sur vos instances IRIS.

Comme vous l'avez peut-être constaté, cet article a mis l'accent sur les aspects pratiques de la configuration et de l'utilisation de divers outils. Nous n'avons pas étudié en détail le fonctionnement interne d'InfluxDB, du format Line Protocol ou du langage Flux Query, ni examiné la multitude de plugins disponibles pour Telegraf.

Ces sujets, aussi fascinants soient-ils, nécessiteraient un article beaucoup plus long.  Je vous encourage vivement à consulter la documentation officielle d'InfluxDB Get started with InfluxDB (Débuter avec InfluxDB) et Telegraf Plugin Directory (Répertoire des plugins) pour approfondir vos connaissances et découvrir toutes les possibilités offertes par ces outils.

N'hésitez pas à partager vos expériences dans les commentaires.

Merci d'avoir lu cet article, et à bientôt!

0
0 52
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 Lorenzo Scalese · Mai 15, 2025 10m read

Introduction

MonLBL est un outil permettant d'analyser des performances d'exécution de code ObjectScript ligne par ligne. codemonitor.MonLBL est un wrapper reposant sur le package %Monitor.System.LineByLine d'InterSystems IRIS pour collecter des métriques précises sur l'exécution de routines, classes ou CSP.

Le wrapper et tous les exemples présentés dans cet article sont disponibles dans le repository GitHub suivant : iris-monlbl-example

Fonctionnalités

L'utilitaire permet de collecter plusieurs types de métriques :

  • RtnLine : Nombre d'exécutions de la ligne
  • GloRef : Nombre de références globales générées par la ligne
  • Time : Temps d'exécution de la ligne
  • TotalTime : Temps total d'exécution incluant les sous-routines appelées

Le tout exporté dans des fichiers CSV.

En plus des métriques par ligne, dc.codemonitor.MonLBL collecte des statistiques globales :

  • Temps d'exécution total
  • Nombre total de lignes exécutées
  • Nombre total de références globales
  • Temps CPU système et utilisateur :
    • Le temps CPU utilisateur correspond au temps passé par le processeur à exécuter le code de l'application
    • Le temps CPU système correspond au temps passé par le processeur à exécuter des opérations du système d'exploitation (appels système, gestion mémoire, I/O)
  • Temps de lecture disque

Prérequis

Pour pouvoir monitorer du code avec MonLBL :

  1. Disposez de la classe dc.codemonitor.MonLBL (disponible ici)
  2. Les routines ou classes à analyser doivent être compilées avec les flags "ck"

⚠️ Mise en garde importante

L'utilisation du monitoring ligne par ligne a un impact sur les performances du serveur. Il est important de respecter les recommandations suivantes :

  • N'utilisez cet outil que sur un ensemble limité de code et de processus (idéalement de l'exécution ponctuel dans un terminal)
  • Évitez son utilisation sur un serveur de production
  • Utilisez de préférence cet outil dans un environnement de développement ou de test

Ces précautions sont essentielles pour éviter des problèmes de performance qui pourraient affecter les utilisateurs ou les systèmes en production. Sachez que le code monitoré s'exécute environ 15-20% plus lentement que s'il ne l'est pas.

Utilisation

Exemple basique

// Création d'une instance de MonLBL
Set mon = ##class(dc.codemonitor.MonLBL).%New()

// Définition des routines à monitorer
Set mon.routines = $ListBuild("User.MaClasse.1")

// Démarrage du monitoring
Do mon.startMonitoring()

// Code à analyser...
// ...

// Arrêt du monitoring et génération des résultats
Do mon.stopMonitoring()

Note: Le monitoring démarré ici n'est valable que pour le processus courant.
Les autres processus qui exécuteraient le même code ne seront pas pris en compte dans les mesures.

Options de configuration

Le wrapper offre plusieurs options configurables :

  • directory : Répertoire où seront exportés les fichiers CSV (par défaut le répertoire Temp d'IRIS)
  • autoCompile : Recompile automatiquement les routines avec les flags "ck" si nécessaire
  • metrics : Liste personnalisable des métriques à collecter
  • decimalPointIsComma : Utilise la virgule comme séparateur décimal pour une meilleure compatibilité avec Excel
  • metricsEnabled : Active ou désactive la collecte des métriques ligne par ligne

Exemple d'utilisation avancée

Voici un exemple plus complet (disponible dans la classe dc.codemonitor.Example) :

ClassMethod MonitorGenerateNumber(parameters As %DynamicObject) As %Status
{
    Set sc = $$$OK
    Try {
        // Affichage des paramètres reçus
        Write "* Parameters :", !
        Set formatter = ##class(%JSON.Formatter).%New()
        Do formatter.Format(parameters)
        Write !
        
        // Création et configuration du moniteur
        Set monitor = ##class(dc.codemonitor.MonLBL).%New()
        
        // ATTENTION : en environnement de production, définissez autoCompile à $$$NO
        // et compilez manuellement le code à monitorer
        Set monitor.autoCompile = $$$YES
        Set monitor.metricsEnabled = $$$YES
        Set monitor.directory = ##class(%File).NormalizeDirectory(##class(%SYS.System).TempDirectory())
        Set monitor.decimalPointIsComma = $$$YES

        // Configuration de la routine à monitorer (forme "int" de la classe)
        // Pour trouver le nom exact de la routine, utilisez la commande :
        // Do $SYSTEM.OBJ.Compile("dc.codemonitor.DoSomething","ck")
        // La ligne "Compiling routine XXX" vous donnera le nom de la routine
        Set monitor.routines = $ListBuild("dc.codemonitor.DoSomething.1")

        // Démarrage du monitoring
        $$$TOE(sc, monitor.startMonitoring())
        
        // Exécution du code à monitorer avec gestion des erreurs
        Try {
            Do ##class(dc.codemonitor.DoSomething).GenerateNumber(parameters.Number)

            // Important : toujours arrêter le monitoring
            Do monitor.stopMonitoring()
        }
        Catch ex {
            // Arrêt du monitoring même en cas d'erreur
            Do monitor.stopMonitoring()
            Throw ex
        }
    }
    Catch ex {
        Set sc = ex.AsStatus()
        Do $SYSTEM.Status.DisplayError(sc)
    }

    Return sc
}

Cet exemple montre plusieurs bonnes pratiques importantes:

  • Utilisation d'un bloc Try/Catch pour gérer les erreurs
  • Arrêt systématique du monitoring, même en cas d'erreur
  • Documentation sur la façon de trouver le nom exact de la routine correspondant à une classe à monitorer
  • Paramétrage complet du moniteur

Exemple d'utilisation avec des pages CSP

MonLBL permet également de monitorer des pages CSP. Voici un exemple (également disponible dans la classe dc.codemonitor.ExampleCsp) :

ClassMethod MonitorCSP(parameters As %DynamicObject = {{}}) As %Status
{
    Set sc = $$$OK
    Try {
        // Affichage des paramètres reçus
        Write "* Parameters :", !
        Set formatter = ##class(%JSON.Formatter).%New()
        Do formatter.Format(parameters)
        Write !
        
        // Création et configuration du moniteur
        Set monitor = ##class(dc.codemonitor.MonLBL).%New()
        Set monitor.autoCompile = $$$YES
        Set monitor.metricsEnabled = $$$YES
        Set monitor.directory = ##class(%File).NormalizeDirectory(##class(%SYS.System).TempDirectory())
        Set monitor.decimalPointIsComma = $$$YES

        // Pour monitorer une page CSP, on utilise la routine générée
        // Exemple: /csp/user/menu.csp --> classe: csp.menu --> routine: csp.menu.1
        Set monitor.routines = $ListBuild("csp.menu.1")

        // Les pages CSP nécessitent les objets %session, %request et %response
        // On crée ces objets avec les paramètres nécessaires
        Set %request = ##class(%CSP.Request).%New()
        // Configurer les paramètres de requête si nécessaire
        // Set %request.Data("<param_name>", 1) = <value>
        Set %request.CgiEnvs("SERVER_NAME") = "localhost"
        Set %request.URL = "/csp/user/menu.csp"

        Set %session = ##class(%CSP.Session).%New(1234)
        // Configurer les données de session si nécessaire
        // Set %session.Data("<data_name>", 1) = <value>

        Set %response = ##class(%CSP.Response).%New()
            
        // Démarrage du monitoring
        $$$TOE(sc, monitor.startMonitoring())
        
        Try {
            // Pour éviter d'afficher le contenu de la page CSP dans le terminal,
            // on peut utiliser la classe IORedirect pour rediriger la sortie vers null
            // (nécessite l'installation via zpm "install io-redirect")
            Do ##class(IORedirect.Redirect).ToNull() 
            
            // Appel de la page CSP via sa méthode OnPage
            Do ##class(csp.menu).OnPage()
            
            // Restauration de la sortie standard
            Do ##class(IORedirect.Redirect).RestoreIO()

            // Arrêt du monitoring
            Do monitor.stopMonitoring()
        }
        Catch ex {
            // Toujours restaurer la sortie et arrêter le monitoring en cas d'erreur
            Do ##class(IORedirect.Redirect).RestoreIO()
            Do monitor.stopMonitoring()
           
            Throw ex
        }
    }
    Catch ex {
        Set sc = ex.AsStatus()
        Do $SYSTEM.Status.DisplayError(sc)
    }

    Return sc
}

Points importants pour le monitoring des pages CSP :

  1. Identification de la routine : Une page CSP est compilée en une classe et une routine. Par exemple, /csp/user/menu.csp génère la classe csp.menu et la routine csp.menu.1.

  2. Environnement CSP : Il est nécessaire de créer les objets de contexte CSP (%request, %session, %response) pour que la page s'exécute correctement.

  3. Redirection de sortie : Pour éviter que le contenu HTML ne s'affiche dans le terminal, on peut utiliser l'utilitaire IORedirect (disponible sur OpenExchange via zpm "install io-redirect").

  4. Appel de la page : L'exécution se fait via la méthode OnPage() de la classe générée.

Exemple de sortie

Voici un exemple de sortie obtenue lors de l'exécution de la méthode MonitorGenerateNumber :

USER>d ##class(dc.codemonitor.Example).MonitorGenerateNumber({"number":"100"})
* Parameters :
{
  "number":"100"
}

* Metrics are exported to /usr/irissys/mgr/Temp/dc.codemonitor.DoSomething.1.csv
* Perf results :
{
  "startDateTime":"2025-05-07 18:45:42",
  "systemCPUTime":0,
  "userCPUTime":0,
  "timing":0.000205,
  "lines":19,
  "gloRefs":14,
  "diskReadInMs":"0"
}

On peut observer dans cette sortie :

  1. L'affichage des paramètres d'entrée
  2. La confirmation que les métriques ont été exportées dans un fichier CSV
  3. Un résumé des performances globales au format JSON, incluant :
    • La date et l'heure de début
    • Le temps CPU système et utilisateur
    • Le temps d'exécution total
    • Le nombre de lignes exécutées
    • Le nombre de références globales
    • Le temps de lecture disque

Interprétation des résultats CSV

Après l'exécution, des fichiers CSV (1 par routine dans le $ListBuild routines) sont générés dans le répertoire configuré. Ces fichiers contiennent :

  • Le numéro de ligne
  • Les métriques collectées pour chaque ligne
  • Le code source de la ligne (attention si vous n'avez pas compilé les classes avec le flag "k", le code source ne sera pas disponible dans le fichier csv)

Voici un exemple du contenu d'un fichier CSV exporté (dc.codemonitor.DoSomething.1.csv) :

LigneRtnLineGloRefTimeTotalTimeCode
10000 ;dc.codemonitor.DoSomething.1
20000 ;Generated for class dc.codemonitor.DoSomething. Do NOT edit. 05/07/2025 10:16:07AM
30000 ;;59595738;dc.codemonitor.DoSomething
40000 ;
50000GenerateNumber(n=1000000) methodimpl {
6100,0000050,000005 For i=1:1:n {
710000,0000190,000019 Set number = $Random(100000)
810000,0000150,000015 Set isOdd = number # 2
910000,0000130,000013 }
10100,0000030,000003 Return }

Dans ce tableau, nous pouvons analyser :

  • RtnLine : Indique combien de fois chaque ligne a été exécutée (ici, les lignes 6 et 10 ont été exécutées une fois)
  • GloRef : Montre les références globales générées par chaque ligne
  • Time : Présente le temps d'exécution propre à chaque ligne
  • TotalTime : Affiche le temps total incluant les appels à d'autres routines

Ces données peuvent être facilement importées dans un tableur pour analyse approfondie. Les lignes les plus coûteuses en termes de temps ou d'accès aux données peuvent ainsi être facilement identifiées.

Remarque sur l'efficacité du cache

L'efficacité du cache de base de données (global buffer) peut masquer des problèmes de performance réels. Lors de l'analyse, les accès aux données peuvent paraître rapides grâce à ce cache, mais pourraient être bien plus lents dans certaines conditions réelles d'utilisation.

Sur les systèmes de développement, vous pouvez vider le cache entre vos mesures avec la commande suivante :

Do ClearBuffers^|"%SYS"|GLOBUFF()

⚠️ ATTENTION : Soyez prudent avec cette commande, car elle s'applique à l'ensemble du système. Ne l'utilisez jamais sur un environnement de production, car cela pourrait impacter les performances de toutes les applications en cours d'exécution.

Conclusion

Le monitoring ligne par ligne est un outil précieux pour l'analyse de performance de code ObjectScript. En identifiant précisément les lignes de code qui consomment le plus de ressources, il permet aux développeurs de gagner beaucoup de temps dans l'analyse de problèmes de lenteurs.

0
0 45
Article Lorenzo Scalese · Mai 7, 2025 6m read

Introduction

À mesure que l'automatisation pilotée par l'IA devient un élément essentiel des systèmes d'information modernes, l'intégration des capacités d'IA dans les plateformes existantes doit être transparente et efficace. Le projet IRIS Agent montre comment l'IA générative peut fonctionner sans effort avec InterSystems IRIS, grâce à son puissant cadre d'interopérabilité, sans qu'il soit nécessaire d'apprendre Python ou de créer des workflows d'IA distincts à partir de zéro.
Ce projet examine la manière dont ChatGPT et Anthropic Claude, deux modèles d'IA parmi les plus avancés, peuvent interagir avec IRIS à l'aide de GPT personnalisés et du protocole MCP (Model Context Protocol). Plutôt que de créer des pipelines d'IA isolés, IRIS Agent traite l'IA comme un service d'interopérabilité, permettant ainsi aux organisations d'améliorer l'automatisation, la prise de décision et le traitement des données sans perturber leur architecture existante.
En s'appuyant sur des outils d'interopérabilité natifs d'IRIS, les développeurs peuvent intégrer des modèles d'IA de manière transparente, comme n'importe quel autre composant du système. Cette approche garantit la stabilité, la sécurité, l'évolutivité et l'auditabilité, tout en permettant une interaction en langage naturel, la récupération de données en temps réel et l'administration automatisée du système, le tout dans l'environnement familier d'IRIS.

Technologies sous-jacentes à IRIS Agent

Le projet IRIS Agent s'appuie sur un ensemble de technologies puissantes pour garantir efficacité, évolutivité et intégration transparente:

  • InterSystems IRIS – Une plateforme robuste dédiée au développement d'applications et à l'intégration de données, qui utilise ObjectScript pour le code côté serveur.
  • Custom GPT Bot – Un assistant IA personnalisé conçu pour rationaliser les interactions avec le système, basé sur ChatGPT.
  • Claude AI Desktop – Facilite la communication avec le serveur via le protocole MCP (Model Context Protocol).
  • Node.js – Gère les communications avec le serveur MCP
  • OpenAPI – Documentation API standardisée.
  • Docker – Compartimente l'application pour simplifier le déploiement et la gestion des dépendances.

Présentation interne

Voici à quoi ressemble notre projet sous le capot IRIS:

La Production comprend les éléments suivants:

  • LanguageModelIn: un service qui reçoit une requête API et la redirige vers l'opération responsable.
  • Meds: une opération qui recherche des médicaments sur une API tierce.
  • Metrics: une opération qui recherche des informations OPS telles que les journaux, les erreurs et les messages dans le système Iris.
  • Namespaces: une opération qui répertorie, recherche et modifie les espaces de noms dans le système Iris.
  • Users: une opération qui répertorie, recherche, crée et supprime des utilisateurs dans le système Iris.

Le fonctionnement du service est particulièrement visible dans l'afficheur de messages et les traces. Il peut nous aider à suivre les opérations et à diagnostiquer les problèmes, par exemple les requêtes:

…et les réponses:


L'un des points forts de ce projet réside dans le fait que le service LanguageModelIn génère automatiquement une documentation API ouverte pour les opérations métier dans le cadre de la production IRIS Interoperability. Cette API  nous permet de connecter facilement les GPT personnalisés à ChatGPT et au serveur MCP (Model Context Protocol) à Claude AI Desktop. 

Intégration avec ChatGPT 

Après avoir terminé tous les processus d'installation et de configuration, que vous trouverez dans notre fichier ReadMe, ReadMe, demandons à notre agent IRIS GPT d'OpenAI quelles sont ses fonctionnalités actuelles:

  

Et créez un nouvel utilisateur…

   


Vérification des mises à jour...

   

Intégration avec des Sources de données externes

L'une des fonctionnalités remarquables de l'agent IRIS est sa capacité à interroger de manière transparente non seulement les données stockées dans la base de données InterSystems IRIS, mais également des sources de données externes. Dans notre exemple, l'agent s'intègre à l'OpenFDA API pour fournir des informations en temps réel sur les médicaments. Cela permet aux utilisateurs de rechercher des informations sur les médicaments, des rapports de sécurité et des données de conformité réglementaire directement dans le système. 
Dans notre projet, l'API offre la possibilité de rechercher des médicaments par leur nom. Recherchons donc les médicaments dont le nom contient le mot "flu".

   

Si vous souhaitez l'essayer dès maintenant, suivez le lien et engagez une conversation avec notre IRIS Agent démo.

Intégration avec Claude AI Desktop

L'une des fonctionnalités clés de l'agent IRIS est sa capacité à interagir avec Claude AI Desktop, un assistant IA local développé par Anthropic. L'intégration est assurée par le protocole MCP (Model Context Protocol), qui facilite la communication entre Claude et les applications externes. Pour ce faire, l'agent IRIS utilise un serveur MCP Node.js dédié qui sert d'intermédiaire, traitant les requêtes entrantes et les acheminant entre Claude et le système IRIS.
Cette configuration permet aux utilisateurs d'interagir directement avec l'agent IRIS via l'interface Claude, en émettant des commandes en langage naturel pour récupérer des données système, gérer les configurations et exécuter des tâches administratives. Le serveur MCP garantit un échange de données fluide, tout en maintenant l'efficacité et la sécurité, et fournit aux administrateurs un assistant puissant basé sur l'IA pour la gestion du système.

         

Conclusion

Le projet IRIS Agent présente le potentiel de l'automatisation pilotée par l'IA dans les environnements d'entreprise modernes. En intégrant de manière transparente InterSystems IRIS, Claude AI et Custom GPT, il simplifie la gestion des systèmes, améliore l'efficacité et ouvre la voie à de futures innovations. Notre projet démontre également que vous êtes déjà prêt pour la révolution de l'IA avec InterSystems IRIS!


Si vous avez apprécié la découverte d'IRIS Agent et de ses fonctionnalités et que vous souhaitez nous soutenir, veuillez prendre quelques instants pour voter pour notre application ici https://openexchange.intersystems.com/contest/40. Je vous remercie!


Développeurs de projet:

Banksia Global est une société de conseil technologique de premier plan qui aide les organisations à exploiter pleinement le potentiel des solutions modernes de gestion des données. En mettant l'accent sur la plateforme InterSystems IRIS, Banksia Global fournit des conseils stratégiques, des services d'intégration de systèmes et de développement de logiciels personnalisés à des clients issus de divers secteurs. Son expertise réside dans la fourniture de solutions hautement performantes et évolutives qui permettent aux entreprises de stimuler l'innovation, de rationaliser leurs opérations et de saisir de nouvelles opportunités. Réputée pour son approche collaborative et agile, Banksia Global travaille en étroite collaboration avec ses clients afin de s'assurer que chaque solution est adaptée à leurs besoins spécifiques, favorisant ainsi des partenariats à long terme et une croissance durable.

0
0 59
Article Lorenzo Scalese · Avr 30, 2025 5m read

Après tant d'années d'attente, nous avons enfin un pilote officiel disponible sur Pypi

De plus, j'ai découvert que le pilote JDBC était enfin disponible sur Maven depuis déjà 3 mois,  et le pilote .Net driver - surNuget depuis plus d'un mois.

 La mise en œuvre de la DB-API et que les fonctions devraient au moins être définies par cette norme. La seule différence devrait se situer au niveau de SQL.

Et ce qui est intéressant dans l'utilisation de bibliothèques existantes, c'est qu'elles ont déjà mis en œuvre d'autres bases de données en utilisant le standard DB-API, et que ces bibliothèques s'attendent déjà à ce que le pilote fonctionne.

J'ai décidé de tester le pilote officiel d'InterSystems en mettant en œuvre son support dans la bibliothèque SQLAlchemy-iris.

executemany

Préparez une opération de base de données (requête ou commande) et exécutez-la en fonction de toutes les séquences de paramètres ou de mappages trouvées dans la séquence seq_of_parameters.

Cette fonction très utile permet d'insérer plusieurs lignes à la fois. Commençons par un exemple simple

import iris

host = "localhost" port = 1972 namespace = "USER" username = "_SYSTEM" password = "SYS" conn = iris.connect( host, port, namespace, username, password, )

with conn.cursor() as cursor: cursor = conn.cursor()

res = cursor.execute(<span class="hljs-string">"DROP TABLE IF EXISTS test"</span>)
res = cursor.execute(
    <span class="hljs-string">"""
CREATE TABLE test (
        id IDENTITY NOT NULL,
        value VARCHAR(50)
) WITH %CLASSPARAMETER ALLOWIDENTITYINSERT = 1
"""</span>
)

cursor = conn.cursor()
res = cursor.executemany(
    <span class="hljs-string">"INSERT INTO test (id, value) VALUES (?, ?)"</span>, [
        (<span class="hljs-number">1</span>, <span class="hljs-string">'val1'</span>),
        (<span class="hljs-number">2</span>, <span class="hljs-string">'val2'</span>),
        (<span class="hljs-number">3</span>, <span class="hljs-string">'val3'</span>),
        (<span class="hljs-number">4</span>, <span class="hljs-string">'val4'</span>),
    ]
)</code></pre>

Cela fonctionne bien, mais que se passe-t-il s'il faut insérer une seule valeur par ligne.

    res = cursor.executemany(
        "INSERT INTO test (value) VALUES (?)", [
            ('val1', ),
            ('val2', ),
            ('val3', ),
            ('val4', ),
        ]
    )

Cela conduit malheureusement à une exception inattendue

RuntimeError: Cannot use list/tuple for single values (Impossible d'utiliser une liste/tuple pour des valeurs uniques)

Pour certaines raisons, une seule valeur par ligne est autorisée, et InterSystems demande d'utiliser une méthode différente

    res = cursor.executemany(
        "INSERT INTO test (value) VALUES (?)", [
            'val1',
            'val2',
            'val3',
            'val4',
        ]
    )

De cette façon, cela fonctionne bien

fetchone

Récupère la ligne suivante d'un ensemble de résultats de requête, en renvoyant une seule séquence, ou None lorsqu'il n'y a plus de données disponibles.

Un exemple simple sur sqlite

import sqlite3
con = sqlite3.connect(":memory:")

cur = con.cursor() cur.execute("SELECT 1 one, 2 two") onerow = cur.fetchone() print('onerow', type(onerow), onerow) cur.execute("SELECT 1 one, 2 two union all select '01' as one, '02' as two") allrows = cur.fetchall() print('allrows', type(allrows), allrows)

fournit

onerow <class 'tuple'> (1, 2)
allrows <class 'list'> [(1, 2), ('01', '02')]

Et avec le pilote InterSystems

import iris

con = iris.connect( hostname="localhost", port=1972, namespace="USER", username="_SYSTEM", password="SYS", )

cur = con.cursor() cur.execute("SELECT 1 one, 2 two") onerow = cur.fetchone() print("onerow", type(onerow), onerow) cur.execute("SELECT 1 one, 2 two union all select '01' as one, '02' as two") allrows = cur.fetchall() print("allrows", type(allrows), allrows)

par certaines raisons fournit

onerow <class 'iris.dbapi.DataRow'> <iris.dbapi.DataRow object at 0x104ca4e10>
allrows <class 'tuple'> ((1, 2), ('01', '02'))

Qu'est-ce que DataRow, et pourquoi ne pas utiliser un tuple ou au moins une liste

Exceptions

SLa norme décrit une variété de classes d'exceptions que le pilote est censé utiliser, au cas où quelque chose ne fonctionnerait pas. Or, le pilote InterSystems ne les utilise pas du tout, se contentant de déclencher une erreur RunTime pour toute raison, ce qui, de toute façon, est contraire à la norme.

L'application peut s'appuyer sur le type d'exception qui se produit et se comporter en conséquence. Mais le pilote InterSystems ne fournit aucune différence. Par ailleurs, SQLCODE serait utile, mais il doit être extrait du message d'erreur

Conclusion

Au cours des tests, j'ai donc trouvé plusieurs bogues

  • Erreurs aléatoires survenant à tout moment <LIST ERROR> Format de liste incorrect, type non supporté pour IRISList; Détails : type détecté : 32
    • fonctionnent correctement, si vous réessayez juste après l'erreur
  • Des erreurs de segmentation ont été détectées, je ne sais même pas comment cela se produit
  • Résultat inattendu de la fonction fetchone
  • Fonctionnement inattendu de la fonction executemany, pour une seule ligne de valeur
  • Les exceptions ne sont pas du tout implémentées, des exceptions différentes devraient être générées en cas d'erreurs différentes, et les applications s'appuient sur ces exceptions
  • Python intégré peut être interrompu en cas d'installation à côté d'IRIS
    • en raison du même nom utilisé par Python intégré et ce pilote, il remplace ce qui est déjà installé avec IRIS et peut l'interrompre

SQLAlchemy-iris supporte maintenant le pilote officiel d'InterSystems, mais ceci en raison d'une incompatibilité avec Python intégré et de plusieurs bogues découverts lors des tests. Installation à l'aide de cette commande, avec l'option définie

pip install sqlalchemy-iris[intersystems]

Et pour une utilisation simple, l'URL devrait être iris+intersystems://

from sqlalchemy import Column, MetaData, Table
from sqlalchemy.sql.sqltypes import Integer, VARCHAR
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase

DATABASE_URL = "iris+intersystems://_SYSTEM:SYS@localhost:1972/USER" engine = create_engine(DATABASE_URL, echo=True)

# Create a table metadata metadata = MetaData()

classBase(DeclarativeBase):passdefmain(): demo_table = Table( "demo_table", metadata, Column("id", Integer, primary_key=True, autoincrement=True), Column("value", VARCHAR(50)), )

demo_table.drop(engine, checkfirst=<span class="hljs-keyword">True</span>)
demo_table.create(engine, checkfirst=<span class="hljs-keyword">True</span>)
<span class="hljs-keyword">with</span> engine.connect() <span class="hljs-keyword">as</span> conn:
    conn.execute(
        demo_table.insert(),
        [
            {<span class="hljs-string">"id"</span>: <span class="hljs-number">1</span>, <span class="hljs-string">"value"</span>: <span class="hljs-string">"Test"</span>},
            {<span class="hljs-string">"id"</span>: <span class="hljs-number">2</span>, <span class="hljs-string">"value"</span>: <span class="hljs-string">"More"</span>},
        ],
    )
    conn.commit()
    result = conn.execute(demo_table.select()).fetchall()
    print(<span class="hljs-string">"result"</span>, result)

main()

En raison de bogues dans le pilote InterSystems, certaines fonctionnalités peuvent ne pas fonctionner comme prévu. J'espère que cela sera corrigé à l'avenir

0
0 27
Article Lorenzo Scalese · Avr 16, 2025 7m read

Qu'est-ce que JWT ??

JWT (JSON Web Token) est un standard ouvert (RFC 7519) qui offre une méthode légère, compacte et autonome pour transmettre en toute sécurité des renseignements entre deux parties. Il est couramment utilisé dans les applications web pour l'authentification, l'autorisation et l'échange d'informations.

Un JWT est généralement composé de trois parties:

1. En-tête JOSE (JSON Object Signing and Encryption)
2. Payload
3. Signature

Ces parties sont encodées au format Base64Url et concaténées avec des points (.) qui les séparent.

Structure d'un JWT

En-tête

{ "alg": "HS256", "typ": "JWT"}

Payload

{"sub": "1234567890", "name": "John Doe", "iat": 1516239022}

Signature:
La signature permet de vérifier que l'expéditeur du JWT est bien celui qu'il prétend être et de s'assurer que le message n'a pas été falsifié.

Pour créer la signature:

1. base64 En-tête et payload encodés en base64.
2. Application de l'algorithme de signature (par exemple, HMAC SHA256 ou RSA) avec une clé secrète (pour les algorithmes symétriques tels que HMAC) ou une clé privée (pour les algorithmes asymétriques tels que RSA).
3. Codage Base64Url du résultat pour obtenir la signature.

Exemple de JWT. Consultez le contenu du JWT 

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Création de JWT dans IRIS

Remarque : Avant 2024, la classe %OAuth2.JWT était utilisée pour générer des JWT dans IRIS. La classe %Net.JSON.JWT est désormais la classe principale pour la création de JWT, et j'utiliserai cette classe dans l'exemple de code.

JWK overview

Les JWK représentent une clé cryptographique, en particulier pour la signature et la vérification des JWT. Les JWK permettent de représenter les clés publiques (pour la vérification) et les clés privées (pour la signature) dans un format normalisé qui peut être facilement échangé entre les systèmes. Les JWKS contiennent plusieurs JWKs

Flux de travail JWT

1. Construisez votre en-tête en tant que %DynamicObject et ajoutez des en-têtes personnalisés si nécessaire.

2. Construisez le corps/les revendications directement en tant que %DynamicObject

3. Appelez la méthode Create de la classe %Net.JSON.JWT.

Set sc = ##Class(%Net.JSON.JWT).Create(header, , claims, jwks, , .JWT)

Création de JWK

Set sc = ##Class(%Net.JSON.JWK).Create("HS256","1212ASD!@#!#@$@#@$$#SDFDGD#%+_)(*@$SFFS",.privateJWK,.publicJWK)

Cela renverra la clé privée

{"kty":"oct","k":"MTIxMkFTRCFAIyEjQCRAI0AkJCNTREZER0QjJStfKSgqQCRTRkZT","alg":"HS256"

Quelques propriétés importantes de JWK

"kty": "oct" - représente l'algorithme symétrique
"kty": "RSA" / "kty": "EC" - represente l'algorithme asymétrique

Une fois que le JWK est créé, il peut être ajouté aux JWKS.

Créons des JWKS dans IRIS

Set sc = ##class(%Net.JSON.JWKS).PutJWK(jwk,.JWKS)

Cette méthode renvoie le JWKS

Génération du JWT dans IRIS

Vous pouvez créer des JWT à clé symétrique ou asymétrique dans IRIS. La classe %Net.JSON.JWK est essentiellement utilisée pour générer le JWT. Avant d'appeler la méthode, assurez-vous de créer et d'envoyer les JWKS pour le chiffrement symétrique et asymétrique lors de la génération du JWT.

Encryptage symétrique

Les algorithmes symétriques utilisent une clé secrète partagée, où l'expéditeur et le destinataire utilisent la même clé pour signer et vérifier le JWT. Ces algorithmes, tels que HMAC (HS256, HS512, HS384), génèrent un hachage (signature) pour le payload du JWT. Cette approche n'est pas recommandée pour les systèmes de haute sécurité, car la signature et la vérification sont exposées, ce qui pose des risques potentiels pour la sécurité.

La méthode Create de la classe %Net.JSON.JWK est utilisée pour générer le JWK. Elle accepte deux paramètres d'entrée et renvoie deux paramètres de sortie:

1. algorithm - l'algorithme pour lequel le JWK doit être créé.
2. secert - la clé utilisée pour signer et vérifier le JWT
3. privateJWK - la clé Web JSON privée qui est créée.
4. publicJWK - la clé Web JSON publique qui est créée.

Pour les algorithmes à clé symétrique, vous obtiendrez privateJWK

Pour les algorithmes à clé asymétrique, vous obtiendrez privateJWK et publicJWK

 
SymmetricKeyJWT

Le résultat 

LEARNING>d ##class(Learning.JWT.NetJWT).SymmetricKeyJWT()
privateJWK={"kty":"oct","k":"MTIxMkFTRCFAIyEjQCRAI0AkJCNTREZER0QjJStfKSgqQCRTRkZT","alg":"HS256"}  ; <DYNAMIC OBJECT>
privateJWKS="{""keys"":[{""kty"":""oct"",""k"":""MTIxMkFTRCFAIyEjQCRAI0AkJCNTREZER0QjJStfKSgqQCRTRkZT"",""alg"":""HS256""}]}"
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsIngtYyI6InRlIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.PcCs_I8AVy5HsLu-s6kQYWaGvuwqwPAElIad11NpM_E

Encryptage asymétrique

L'encryptage asymétrique fait référence à l'utilisation d'une paire de clés : une clé pour signer le jeton (clé privée) et l'autre pour le vérifier (clé publique). Il diffère de l'encryptage symétrique

Clé privée : cette clé est utilisée pour signer le jeton JWT. Elle est gardée secrète et ne doit jamais être exposée.
Clé publique : Cette clé est utilisée pour vérifier l'authenticité du JWT. Elle peut être partagée et distribuée en toute sécurité car elle ne peut pas être utilisée pour signer de nouveaux jetons.

Vous pouvez générer l'encryptage asymétrique JWT avec une clé/un certificat privé via %SYS.X509Credentials. Vous devez donc stocker votre certificat dans cette classe persistante.

 
AsymmetricWithx509

JWT dans les applications Web.

À partir de la version de 2023 , IRIS inclut par défaut la création de JWT intégrée pour les applications Web. Assurez-vous que l'authentification JWT est activée lors de la configuration de votre application Web

J'ai ajouté une brève explication  sur la configuration

1. Activez l' Authentication JWTdans votre application Web
2. Créez une classe REST si vous ne l'avez pas déjà fait
3. La ressource endpoint par défaut « /login » est incluse. Effectuez un appel API REST en utilisant l'authentification de base avec le payload comme {"user": "_SYSTEM", "password": "SYS"}.
4. La réponse sera un JSON contenant le "access_token," "refresh_token," et d'autres détails pertinents.
5. Utilisez le token d'accès pour l'autorisation.

0
1 42
Article Lorenzo Scalese · Mars 24, 2025 7m read

Je vais vous montrer comment vous pouvez installer très rapidement sur votre ordinateur un cluster de nœuds InterSystems IRIS en sharding. Dans cet article, mon objectif n'est pas de parler du sharding en détail, ni de définir une architecture de déploiement pour un cas réel, mais de vous montrer comment vous pouvez rapidement installer sur votre machine un cluster d'instances d'IRIS en sharding avec lequel vous pouvez jouer et faire des tests. Si vous souhaitez en savoir plus sur le sharding dans IRIS, vous pouvez consulter la documentation en cliquant ici: clicking here.  

Disons dès le départ que la technologie de sharding d'IRIS va nous permettre de faire deux choses:

  • Définir, charger et consulter des tables fragmentées ou des shards, dont les données seront réparties de manière transparente entre les nœuds du cluster
  • Définir  des tables fédérées, qui offrent une vue globale et composite des données appartenant à des tables différentes et qui, physiquement, sont stockées sur différents nœuds distribués

Donc, comme je l'ai dit, nous laissons le sujet du jeu avec des shards ou des tableaux fédérés pour d'autres articles, et nous nous concentrons maintenant sur l'étape précédente, c'est-à-dire sur la mise en place du cluster de nœuds en sharding.

Pour notre exemple, nous allons utiliser Docker Desktop (for Windows or MacOS) et nous appuyer sur la fonctionnalité d'IRIS: CPF Merge; ou fusion de fichier de configuration; qui nous permet d'utiliser un fichier texte brut dans lequel nous inclurons les sections et propriétés de configuration d'IRIS que nous voulons appliquer pour modifier la configuration actuelle de l'instance d'IRIS. Cette fichier se superpose au fichier  iris.cpf  qui définit la configuration par défaut de l'instance.

Ce merge est automatiquement « activé » lors de l'ajout de la variable d'environnement: ISC_CPF_MERGE_FILE à laquelle  nous devons avoir attribué un chemin valide vers un fichier contenant les sections du fichier cpf que nous voulons modifier. Au démarrage, IRIS vérifie si on lui a demandé de faire une fusion (merge) (en gros, si cette variable d'environnement existe et pointe vers un fichier valide). Si c'est le cas, vous pouvez procéder à la fusion et démarrer.

Je ne fais pas plus de détours et je vous inclus le fichier docker-compose.yml qui fera la magie:

 
docker-compose.yml
services:
  # iris container
  irisnode1:
    init: true 
    hostname: irishost1
    image: shardnode:latest 
    container_name: irisnode1
    build: 
      context: ./cluster
      dockerfile: Dockerfile
    ports:
    - "9991:1972"
    environment: 
    - ISC_DATA_DIRECTORY=/durable/irishost1
    - ISC_CPF_MERGE_FILE=/iris-shared/merge_first_data-node.cpf
    command: 
       --check-caps false --ISCAgent false --key /iris-shared/iris.key -a /iris-shared/configure_first_data-node.sh
    volumes:
    - ./cluster/iris-instance:/iris-shared:delegated
    - ./DDBBs:/durable:delegated

irisnode2: init: true hostname: irishost2 image: shardnode:latest container_name: irisnode2 build: context: ./cluster dockerfile: Dockerfile ports: - "9992:1972" environment: - ISC_DATA_DIRECTORY=/durable/irishost2 - ISC_CPF_MERGE_FILE=/iris-shared/merge_data-node.cpf command: --check-caps false --ISCAgent false --key /iris-shared/iris.key -a /iris-shared/configure_data-node.sh volumes: - ./cluster/iris-instance:/iris-shared - ./DDBBs:/durable depends_on: irisnode1: condition: service_healthy

# conteneur de passerelle web webgateway: image: containers.intersystems.com/intersystems/webgateway:latest-em init: true container_name: webgateway hostname: webgateway ports: - 7772:80 - 7773:443 environment: - ISC_CSP_CONF_FILE=/webgateway-shared/CSP.conf - ISC_CSP_INI_FILE=/webgateway-shared/CSP.ini volumes: - ./webgateway/CSP.conf:/webgateway-shared/CSP.conf - ./webgateway/CSP.ini:/webgateway-shared/CSP.ini

Dans ce cas, nous créons 3 services:

  • irisnode1 - Premier nœud du cluster, qui a un rôle spécial, et c'est pourquoi nous l'appelons spécifiquement node1
  • irisnode2 - Nœud de données supplémentaire du cluster, dont le rôle est data (nous pouvons en avoir autant que nous le voulons)
  • webgateway - Serveur web préconfiguré pour accéder aux instances IRIS (Apache + Webgateway)

Pour créer l'image shardnode:latest, j'ai utilisé le dockerfile suivant:

 
Dockerfile
FROM containers.intersystems.com/intersystems/irishealth:2024.3#FROM containers.intersystems.com/intersystems/iris-community:latest-em#FROM containers.intersystems.com/intersystems/irishealth-arm64:2024.3USER root
WORKDIR /opt/irisapp
RUN chown -R irisowner:irisowner /opt/irisapp
USER irisowner
WORKDIR /opt/irisapp
COPY --chown=irisowner:irisowner src src
COPY --chown=irisowner:irisowner iris.script iris.script
RUN iris start IRIS \
    && iris session IRIS < iris.script \
    && iris stop IRIS quietly

 

Les fichiers utilisés pour effectuer la fusion (merge) pour nodo1 et d'autre cluster IRIS  data nodes sont les suivants:

 
merge_first_data-node.cpf
 
merge_data-node.cpf
# Noeud de données supplémentaires
# 2 GB 8k / 204 MB gmpheap / 64 nodos max
[config]
globals=0,0,2048,0,0,0
gmheap=204800
MaxServerConn=64
MaxServers=64
# Définition d'un nœud de données et son ajout au cluster. Création d'un point de terminaison REST de test (optionnel)
[Actions]
ConfigShardedCluster:ClusterURL=IRIS://irishost1:1972/IRISCLUSTER,Role=data
# CreateApplication:Name=/rest/testapp,MatchRoles=:%ALL,NameSpace=USER,DispatchClass=Test.RESTserver,AutheEnabled=64

 

On pourrait avoir plus de nœuds de type data dans le cluster en ajoutant simplement plus de services avec la même définition que l' irisnode2 (en changeant le nom bien sûr)

D'autre part, pour que l'acheminement fonctionne correctement sur notre serveur web et que nous puissions accéder aux portails d'administration de chacune des instances, nous devons modifier le paramètre CSPConfigName dans chacune d'entre elles, et nous le faisons avec les fichiers: configure_first_data-node.sh y configure_data-node.sh; qui sont identiques dans cet exemple, mais que j'ai laissés différents car, à un moment donné, nous pourrions vouloir effectuer des actions différentes au démarrage de chaque instance d'IRIS, selon qu'il s'agit du node1 ou d'un nœud de type data du cluster.

 
configure_data-node.sh

Je pense que c'est à peu près tout.

Les nœuds pourraient être définis à l'aide de l'API disponible dans la classe %SYSTEM.Cluster  mais la possibilité d'introduire des actions conjointement avec la fonctionnalité CPF Merge simplifie grandement la tâche. Je vous recommande de consulter ce lien, en particulier la section relative à la rubrique the section [Actions].

Pour construire les images et déployer le cluster, nous pourrions construire notre image sharnode:latest et lancer le docker-compose from depuis VS Code. Alternativement, nous pourrions le faire dans notre ligne de commande, à partir du dossier dans lequel nous avons le fichier: docker-compose.yml, en exécutant ces commandes:

docker compose build
docker compose up

Cela prendra un peu de temps la première fois car l'instance marquée comme node1 doit être instanciée avant que tout autre nœud de type data du cluster ne démarre pour la première fois. Mais tout devrait fonctionner et être prêt en une minute ou moins.

Si tout s'est bien passé, vous devriez pouvoir accéder aux portail de gestion de chaque instance avec les URL ci-dessous:

Portail de gestion du nœud de cluster 1 node1http://localhost:7772/irishost1/csp/sys/UtilHome.csp
Portail de gestion du nœud de données datahttp://localhost:7772/irishost2/csp/sys/UtilHome.csp
Accès à la passerelle WebGateway: http://localhost:7772/csp/bin/Systems/Module.cxw

Et voilà! À partir de là, la limite en termes de volume de stockage de bases de données et de taille de tables est déjà fixée par votre hardware. Vous auriez un cluster de nœuds IRIS prêt à définir des tables en sharding ou des tables fédérées.

J'espère que cela vous sera utile!! À bientôt... 

0
0 30
Article Lorenzo Scalese · Fév 5, 2025 6m read


Salut la Communauté!
Dans cet article, je présenterai le framework web Python Streamlit.
Ci-dessous, vous trouverez les sujets que nous aborderons:

  • 1-Introduction au framework web Streamlit
  • 2-Installation du module Streamlit
  • 3-Lancement de l'application Streamlit
  • 4-Commandes de base de Streamlit
  • 5-Affichage du contenu multimédia 
  • 6-Widgets d'input
  • 7-Affichage des progrès et de l'état
  • 8-Barre latérale et conteneur
  • 9-Visualisation des données
  • 10-Affichage de DataFrame

Commençons donc par le premier sujet.
1-Introduction au framework web Python Streamlit 

Streamlit est un framework Python open-source qui permet aux data scientists et aux ingénieurs en apprentissage automatique de créer des applications web interactives de manière simple et rapide.
Grâce à sa syntaxe simple et à son intégration facile avec les bibliothèques de science des données les plus répandues, Streamlit est devenu la référence en matière de prototypage et de partage de projets.

Pour plus de détails, veuillez consulterDocumentations Streamlit

2-Installation du module Streamlit

Pour commencer à construire notre application Web Streamlit, il est nécessaire d'installer le module à l'aide du programme d'installation de paquets pip.

Pour installer Streamlit, exécutez la commande suivante:

pip install streamlit

La commande pour tester l'installation se trouve ci-dessous: 

streamlit hello


Lorsque la commande mentionnée ci-dessus est saisissée dans le terminal, la page suivante s'ouvre automatiquement:
Install Streamlit 3

 

3-Lancement de l'application Streamlit

Streamlit est simple à utiliser. Tout d'abord, on ajoute quelques commandes Streamlit à un script Python ordinaire, puis on le lance à l'aide de streamlit run:

streamlit run your_python_file.py 

Aussitôt que vous exécutez le script, un serveur Streamlit local s'active et votre application s'ouvre dans un nouvel onglet de votre navigateur web par défaut.  Notez que l'application est votre canevas, où vous dessinerez des graphiques, des textes, des widgets, des tableaux, etc.

Une autre façon d'exécuter Streamlit est de le faire en tant que module Python. Cela peut s'avérer utile pour configurer un IDE, par exemple PyCharm, afin qu'il fonctionne avec Streamlit:

python -m streamlit run your_python_file.py

N'oubliez pas de sauvegarder le fichier source chaque fois que vous souhaitez mettre à jour votre application. Dans ce cas, Streamlit détecte les changements éventuels et vous demande si vous souhaitez réexécuter votre application. Sélectionnez "Always rerun" (toujours réexécuter) en haut à droite de votre écran pour mettre à jour automatiquement votre application à chaque fois que vous modifiez son code source. Cela vous permettra de travailler dans une boucle interactive rapide : vous saisissez du code, vous l'enregistrez, vous l'essayez en direct, puis vous saisissez à nouveau du code, vous l'enregistrez, vous l'essayez, et ainsi de suite jusqu'à ce que vous soyez satisfait des résultats. Cette boucle étroite entre le codage et la visualisation des résultats en direct est l'une des façons dont Streamlit vous facilite la vie.

4-Commandes de base de Streamlit

Cette fonction permet d'ajouter à une application web des chaînes formatées, des graphiques de Matplotlib, des diagrammes d'Altair, des graphiques de Plotly, des cadres de données, des modèles de Keras et beaucoup d'autres.

Créons le fichier main.py ci-dessous:

import streamlit as st

st.write("Hello ,let's learn how to build a streamlit app together")

Lancez le fichier main.py en exécutant la commande suivante:

streamlit run main.py


st.title() : Cette fonction permet d'ajouter un titre à l'application.
st.header() : Cette fonction est utilisée pour attribuer l'en-tête d'une section.
st.markdown() : Cette fonction est utilisée pour définir la démarque d'une section. 
st.subheader(): Cette fonction est utilisée pour définir le sous-titre d'une section.
st.caption(): Cette fonction est utilisée pour écrire des légendes.
st.code(): Cette fonction est utilisée pour définir un code.  
st.latex(): Cette fonction affiche des expressions mathématiques formatées en LaTeX. 

import streamlit as st

st.title("This is the app title") st.header("This is the header") st.markdown("This is the markdown") st.subheader("This is the subheader") st.caption("This is the caption") st.code("x = 2021") st.latex(r''' a+a r^1+a r^2+a r^3 ''')

Display texts with Streamlit 2

5-Affichage du contenu multimédia 

Nous avons énuméré ci-dessous quelques fonctions permettant d'afficher des images, des vidéos et des fichiers audio.

st.image(): Cette fonction est utilisée pour représenter une image.
st.audio(): Cette fonction est utilisée pour afficher un fichier audio. 
st.video(): Cette fonction est utilisée pour afficher une vidéo.

import streamlit as st

st.subheader("Image :") st.image("kid.jpg")

st.subheader("Audio :") st.audio("audio.mp3")

st.subheader("Video :") st.video("video.mp4")


6-Widgets d'input

Les widgets sont les composants les plus importants de l'interface utilisateur. Streamlit dispose de plusieurs widgets qui vous permettent de créer de l'interactivité directement dans vos applications grâce à des boutons, des curseurs, des saisies de texte, etc.

st.checkbox(): Cette fonction renvoie une valeur booléenne. Lorsque la case est cochée, elle renvoie la valeur True (vrai). Sinon, elle renvoie une valeur False (Faux).
st.button(): Cette fonction permet d'afficher un widget de type bouton. 
st.radio(): Cette fonction permet d'afficher un widget de type bouton radio 
st.selectbox(): Cette fonction permet d'afficher un widget de type sélection. 
st.multiselect(): Cette fonction est utilisée pour afficher un widget de sélection multiple. 
st.select_slider(): Cette fonction est utilisée pour afficher un widget de sélection. 
st.slider(): Cette fonction est utilisée pour afficher un widget de courseur.

import streamlit as st

st.checkbox('Yes') st.button('Click Me') st.radio('Pick your gender', ['Male', 'Female']) st.selectbox('Pick a fruit', ['Apple', 'Banana', 'Orange']) st.multiselect('Choose a planet', ['Jupiter', 'Mars', 'Neptune']) st.select_slider('Pick a mark', ['Bad', 'Good', 'Excellent']) st.slider('Pick a number', 0, 50)

st.number_input(): Cette fonction affiche un widget de saisie numérique..
st.text_input(): Cette fonction expose un widget de saisie de texte.
st.date_input(): Cette fonction affiche un widget de saisie de date pour choisir une date..
st.time_input(): Cette fonction affiche un widget de saisie de l'heure pour choisir une heure.
st.text_area(): Cette fonction montre un widget de saisie de texte avec plus d'une ligne de texte.
st.file_uploader(): Cette fonction est exploitée pour démontrer un widget de téléchargement de fichiers.
st.color_picker(): Cette fonction est exploitée pour démontrer un widget de téléchargement de fichiers.

import streamlit as st

st.number_input('Pick a number', 0, 10) st.text_input('Email address') st.date_input('Traveling date') st.time_input('School time') st.text_area('Description') st.file_uploader('Upload a photo') st.color_picker('Choose your favorite color')

7-Affichage des progrès et de l'état

Nous allons maintenant expliquer comment ajouter une barre de progression et des messages d'état tels qu'erreur et succès à notre application.

st.balloons(): Cette fonction est utilisée pour afficher des ballons de célébration. 
st.progress(): Cette fonction est utilisée pour afficher une barre de progression. 
st.spinner(): Cette fonction est utilisée pour afficher un message d'attente temporaire pendant l'exécution.

import streamlit as st
import time

st.balloons() # Ballons pour une célébration st.subheader("Progress bar") st.progress(10) # Barre de progression st.subheader("Wait the execution") with st.spinner('Wait for it...'): time.sleep(10) # Simulation d'un délai de processus

Affichage des progrès et de l'état with Streamlit 1

st.success(): Cette fonction affiche un message de succès.
st.error(): Cette fonction est utilisée pour afficher un message d'erreur. 
st.warning(): Cette fonction est utilisée pour afficher un message d'avertissement.
st.info(): Cette fonction révèle un message d'information.<
st.exception(): Cette fonction est utilisée pour afficher un message d'exception.

import streamlit as st

st.success("You did it!") st.error("Error occurred") st.warning("This is a warning") st.info("It's easy to build a Streamlit app") st.exception(RuntimeError("RuntimeError exception"))


Affichage des progrès et de l'état with Streamlit 2

8-Barre latérale et conteneur

Nous pouvons également créer une barre latérale ou un conteneur sur votre page pour organiser votre application. La hiérarchie et la disposition des pages de votre application peuvent avoir un impact considérable sur le confort d'utilisation. L'organisation de votre contenu permet aux visiteurs de mieux comprendre votre site et d'y naviguer plus facilement. Cela les aide également à trouver plus rapidement ce qu'ils cherchent et augmente la probabilité qu'ils reviennent. 

Transmettre un élément à st.sidebar()&nbsp permet d'épingler cet élément à gauche, ce qui permet aux utilisateurs de se concentrer sur le contenu et rend l'application plus organisée et plus facile à gérer.

import streamlit as st

st.sidebar.title("This is writter inside sidebar") st.sidebar.button("Click") st.sidebar.radio("Pick your gender",["Male","Female"])

Sidebar

Conteneur

st.container() ;est utilisé pour construire un conteneur invisible dans lequel vous pouvez placer des éléments créant une disposition et une hiérarchie utiles.

import streamlit as st

container = st.container() container.write("This is written inside the container") st.write("This is written outside the container")

Container

import streamlit as st
import numpy as np

with st.container(): st.write("This is inside the container")

st.bar_chart(np.random.randn(<span class="hljs-number">50</span>, <span class="hljs-number">3</span>))

st.write("This is outside the container")

9-Visualisation des données

La visualisation des données simplifie la narration en rassemblant les données dans un format plus simple, en mettant en évidence les tendances et les valeurs aberrantes. Une bonne visualisation transmet un message narratif, en éliminant le bruit des données et en mettant l'accent sur les informations utiles. Cependant, c'est bien plus compliqué que de simplement habiller un graphique pour l'embellir ou d'ajouter la partie "info" d'une infographie.
Une visualisation de données efficace est un délicat exercice d'équilibre entre la forme et la fonction. Un graphique simple pourrait être trop ennuyeux pour attirer l'attention ou communiquer un message puissant, tandis que la visualisation la plus étonnante pourrait ne pas réussir à transmettre la bonne idée. Les données et les éléments visuels doivent fonctionner ensemble. Cependant, combiner une bonne analyse avec une excellente narration est un art. 

st.pyplot(): Cette fonction est utilisée pour afficher une graphique matplotlib.pyplot.

import streamlit as st
import matplotlib.pyplot as plt
import numpy as np

rand = np.random.normal(1, 2, size=20) fig, ax = plt.subplots() ax.hist(rand, bins=15) st.pyplot(fig)

This function is used to display a line chart.

st.line_chart(): Cette fonction permet d'afficher un graphique linéaire.

import streamlit as st
import pandas as pd
import numpy as np

df = pd.DataFrame(np.random.randn(10, 2), columns=['x', 'y']) st.line_chart(df)

This function is used to display a bar chart.

st.bar_chart(): Cette fonction permet d'afficher un diagramme à barres.

import streamlit as st
import pandas as pd
import numpy as np

df = pd.DataFrame(np.random.randn(10, 2), columns=['x', 'y']) st.bar_chart(df)

This function is used to display an area chart.

st.map(): Cette fonction permet d'afficher des cartes dans l'application. Cependant, elle nécessite les valeurs de latitude et de longitude qui ne peuvent pas être nulles/NA.

import pandas as pd
import numpy as np
import streamlit as st

df = pd.DataFrame(     np.random.randn(500, 2) / [50, 50] + [37.76, -122.4], columns=['lat', 'lon'] ) st.map(df)

Display maps with Streamlit 1

10-Affichage de DataFrame

st.dataframe(): Cette commande permet d'afficher un DataFrame sous forme de table interactive. Elle fonctionne avec une variété d'objets de type collection et DataFrame.

import streamlit as st
import pandas as pd
import numpy as np

df = pd.DataFrame(np.random.randn(50, 20), columns=("col %d" % i for i in range(20)))

st.dataframe(df) # Identique à st.write(df)

Vous pouvez également passer un objet Pandas Styler pour modifier le style du DataFrame rendu:

import streamlit as st
import pandas as pd
import numpy as np

df = pd.DataFrame(np.random.randn(10, 20), columns=("col %d" % i for i in range(20)))

st.dataframe(df.style.highlight_max(axis=0))


Summary

Dans cet article, après avoir présenté le framework web Streamlit, j'ai montré comment installer l'application Streamlit et la faire fonctionner. Nous avons également exploré quelques commandes de base, des widgets et des fonctionnalités de visualisation de données.

Dans mon prochain article, nous créerons une application web Streamlit pour nous connecter au jeu de données IRIS et nous explorerons ensemble les concepts avancés de Streamlit.

Merci!

0
1 96
Article Lorenzo Scalese · Jan 22, 2025 5m read

Mise en œuvre de l'idée DPI-I-456

Idée

Le rôle de cet échantillon

Cet exemple a été cloné à partir de la version iris-interoperability-template. J'ai reconfiguré la production d' interopérabilité Production avec un adaptateur Inbound HTTP Adapter qui est utilisé par un service métier HTTP Business Service. Les détails de la configuration du service métier sont spécifiés dans la rubrique des paramètres par défaut du système System Default Settings. J'ai configuré le paramètre de l'intervalle d'appel pour appeler le serveur HTTPS une fois par heure. Vous pouvez modifier l'URL et la fréquence dans les paramètres du service. Screenshot

À l'origine, le service HTTP avait deux cibles. Le corps de réponse de chaque appel était envoyé en tant que message générique HTTP à un processus métier BPL et à une opération de fichier qui sauvegardait les données dans un dossier iris-http-calls.

Le service HTTP envoie un message générique HTTP à une opération de fichier. Ensuite, un service de fichiers envoie le fichier à un processus métier BPL.

Conditions préalables

Assurez-vous d'avoir installé git et Docker desktop.

Installation: ZPM

Ouverture de l'espace de noms IRIS avec l'interopérabilité activée. Ouvrez le terminal et appelez: USER>zpm "install iris-http-calls"

Installation: Docker

Clone/git extrait le référentiel dans n'importe quel répertoire local.

git clone https://github.com/oliverwilms/iris-http-calls.git

Ouvrez le terminal dans ce répertoire et lancez:

docker-compose build
  1. Lancez le conteneur IRIS avec votre projet:
docker-compose up -d

Comment exécuter l'échantillon

Ouvrez la production et lancez-la si elle n'est pas déjà en cours d'exécution. Il effectue des appels HTTP vers le serveur HTTPS à l'aide d'une URL.

Comment modifier le modèle

Ce référentiel est prêt à être codé dans VSCode avec le plugin ObjectScript. Installez VSCode, Docker et le pluginObjectScript et ouvrez le dossier dans VSCode.

Utilisez le menu pratique VSCode pour accéder à l'éditeur de règles de production et de règles métier et lancez un terminal: Screenshot 2020-10-29 at 20 15 56

utilisation des variables d'environnement

Cet exemple montre la façon dont vous pouvez introduire des variables env dans votre environnement dev. Supposons que vous ayez besoin de configurer la production avec un jeton secret pour accéder à une API à accès limité. Bien sûr, vous ne voulez pas exposer le secret à GitHub. Dans ce cas, le mécanisme des variables Env peut être utile. Tout d'abord, introduisez le fichier .env et configurez .gitignore pour filtrer .env à partir de git.

Ajoutez ensuite le jeton secret dans .env sous la forme ENV_VARIABLE="TOKEN VALUE"

L'introduction suivante consiste à faire importer les variables d'environnement dans dockerfile. Pour que cela fonctionne, ajoutez la section environnement dans [docker-compose.yml] (https://github.com/intersystems-community/iris-interoperability-template/blob/d2d7114de7c551e308e742359babebff5d535821/docker-compose.yml), par exemple:

environnement:
      - SAMPLE_TOKEN=${SAMPLE_TOKEN}

Ensuite, vous pourrez initialiser le conteneur en cours d'exécution avec les données des variables env, par exemple avec l'appel suivant, qui utilise la valeur du fichier .env comme paramètre de la production:

USER> d ##class(dc.Demo.Setup).Init($system.Util.GetEnviron("SAMPLE_TOKEN"))

paramètres de production du gestionnaire de paquets

Les utilisateurs de ce module peuvent utiliser des paramètres pour transmettre des données au module lors de l'installation et personnaliser le chemin d'accès au fichier pour l'opération de fichier et le service de fichier, ainsi que modifier l'URL. Il peut être utile lorsque les paramètres de configuration sont des jetons secrets permettant d'accéder à une API particulière. En tant que spécialiste, vous pouvez fournir de tels paramètres avec une balise par défaut dans module.xml.

<Default Name="FilePath" Value="iris_http_calls" />
<Default Name="UrlModify" Value="/Patient?_id=egqBHVfQlt4Bw3XGXoxVxHg3" />

Ces paramètres par défaut permettent aux utilisateurs d'appeler l'installation du paquet avec la possibilité de transmettre des paramètres. Par exemple, l'appel à l'installation peut être exécuté sous la forme suivante:

zpm "install iris-http-calls -D FilePath=iris_http_calls -D UrlModify=/MedicationStatement?patient=egqBHVfQlt4Bw3XGXoxVxHg3"
USER>zpm "install iris-http-calls -D FilePath=iris_http_calls -D UrlModify=/MedicationStatement?patient=egqBHVfQlt4Bw3XGXoxVxHg3"

[USER|iris-http-calls]        Reload START (/usr/irissys/mgr/.modules/USER/iris-http-calls/0.3.37/)
[USER|iris-http-calls]        Reload SUCCESS
[iris-http-calls]       Module object refreshed.
[USER|iris-http-calls]        Validate START
[USER|iris-http-calls]        Validate SUCCESS
[USER|iris-http-calls]        Compile START
[USER|iris-http-calls]        Compile SUCCESS
[USER|iris-http-calls]        Activate START
[USER|iris-http-calls]        Configure START
[USER|iris-http-calls]        Configure SUCCESS
[USER|iris-http-calls]        Activate SUCCESS

Les paramètres par défaut sont utilisés pour configurer la production dans l'appel suivant:

<Invoke Class="dc.Demo.Setup" Method="Init" >
  <Arg>${FilePath}</Arg>
  <Arg>${UrlModify}</Arg>
</Invoke>

La méthode Init de la classe dc.Demo.Setup configure le service et l'opération de fichier à l'aide du paramètre FilePath. Le paramètre UrlModify est utilisé pour modifier l'URL du service HTTP.

La production appelle le serveur HTTPS à l'aide de l'URL modifiée selon CallInterval. Le corps de réponse est envoyé dans un StreamContainer à une FileOperation. Un service de fichiers lit le fichier et transmet un conteneur de flux à un processus BPL.

0
0 34
Article Lorenzo Scalese · Jan 6, 2025 6m read

Salut la Communauté,

image
Dans cet article, je présenterai mon application iris-HL7v2Gen.

IRIS-HL7v2Gen est une application CSP qui facilite la génération dynamique de messages de test HL7. Ce processus est essentiel pour tester, déboguer et intégrer les systèmes de données de soins de santé. L'application permet aux utilisateurs de générer une grande variété de types de messages HL7, de valider leur structure par rapport aux spécifications HL7, d'explorer la hiérarchie des messages et de transmettre les messages par TCP/IP aux systèmes de production. Ces fonctionnalités sont particulièrement utiles dans les contextes où la conformité aux normes HL7 est obligatoire pour assurer l'interopérabilité entre différents organismes ou systèmes de soins de santé.

Fonctionnalités de l'application

  • Génération Dynamique de Messages HL7: Création instantanée de messages HL7 pour une gamme de types de messages, facilitant ainsi les tests complets.
  • Exploration de la structure des messages: Visualisation de la structure des messages générés sur la base des spécifications HL7.
  • Visualisation des jeux de valeurs: Visualisation des jeux de valeurs codées prédéfinis pour des champs spécifiques.
  • Validation des messages: Validation des messages par rapport aux normes HL7 pour garantir la conformité.
  • Communication TCP/IP: Transmission facile de messages à la production à l'aide de paramètres TCP/IP.
  • Prise en charge d'un grand nombre de types de messages: Prise en charge de 184 types de messages HL7, garantissant la polyvalence pour les différents besoins d'intégration des soins de santé.
  • ClassMethod: Génération d'un message de test par l'invocation d'une méthode de classe
  • Version prise en charge: Actuellement, la version 2.5 de HL7 est prise en charge
0
0 55
Article Lorenzo Scalese · Nov 21, 2024 8m read

Dans l'article précédent. Pratiques des membres de la classe et leur exécution au sein de Embedded Python. WNous allons maintenant aborder le processus de changement d'espace de noms, d'accès aux variables globales, de traversée et d'exécution de routine  au sein de Embedded Python.

Avant de passer aux autres fonctions, examinons brièvement la fonction execute du paquet iris. Cette fonction est particulièrement utile pour l'exécution de fonctions ObjectScript arbitraires et l'invocation de classes.

>>> b = iris.execute('return $Piece("test^aaaa","^",2)')
>>> b
'aaaa'
>>> b = iris.execute('return $Extract("123456",2,5)')
>>> b
'2345'
>>> b = iris.execute('return $Length(123456)')
>>> iris.execute('write ##Class(%SYSTEM.SYS).NameSpace()')
LEARNING>>>
>>> b = iris.execute('return ##Class(%SYSTEM.SYS).NameSpace()')
>>> b
'LEARNING'

Commençons!

4. Changement d'espace de noms

Il est souvent nécessaire de changer d'espace de noms en cours d'exécution. Toutefois, contrairement à IRIS, il n'est pas possible de changer directement d'espace de noms dans Embedded Python. Il est donc essentiel d'utiliser les définitions de classes existantes ou de créer une méthode wrapper pour faciliter le changement d'espace de noms.  

ClassMethod SwitchNM() [ Language = python ]
{
    import iris
    print(iris.cls('%SYSTEM.SYS').NameSpace())
    print(iris.system.Process.SetNamespace("USER"))
    try:
        iris.cls('User.EmbeddedPython').pyGetTemplateString()
    except RuntimeError as e:
        print("Wrong NameSpace",e)
}

 

5. Globale

Pour utiliser les capacités d'une globale pour les données de la traversée ou pour l'extraction directe d'informations à partir de systèmes globaux existants, plutôt que par le biais de SQL ou d'objets dans Embedded Python, on peut y accéder directement en employant la fonction gref du paquetage iris. Pour définir ou obtenir des valeurs globales, la fonction gref peut être utilisée pour établir une référence à la variable globale et assigner directement des valeurs dans Python.

 
iris.gref
class gref(builtins.object)
 |  Objet de référence global d'InterSystems IRIS.
 |  UUtilisez la méthode iris.gref() pour obtenir une référence à une globale
 |
 |  Les méthodes sont définies ci-dessous:
 |
 |  __delitem__(self, key, /)
 |      Suppression de self[key].
 |
 |  __getitem__(self, key, /)
 |      Renvoie de self[key].
 |
 |  __len__(self, /)
 |      Renvoie de len(self).
 |
 |  __repr__(self, /)
 |      Renvoie de repr(self).
 |
 |  __setitem__(self, key, value, /)
 |      Mise à la valeur de self[key].
 |
 |  __str__(self, /)
 |      Renvoie de str(self).
 |
 |  data(self, key)
 |      Étant donné les clés d'une globale sous forme de liste, renvoie son état.
 |      Exemple : x = g.data([i,j]) attribue à x les valeurs 0,1,10,11 0-si indéfini, 1-défini, 10-indéfini mais a des descendants, 11-a une valeur et des descendants
 |
 |  get(self, key)
 |      Étant donné les clés d'un global sous forme de liste, renvoie la valeur stockée à ce nœud de globales.
 |      Exemple : x = g.get([i,j]) attribue à x la valeur stockée à la clé i,j de globale g.
 |
 |  getAsBytes(self, key)
 |      Étant donné les clés d'une globale sous forme de liste, renvoie une chaîne de caractères stockée à ce nœud de la globale, sous forme d'octets.
 |      Exemple : x = g.getAsBytes([i,j]) attribue à x la valeur stockée à la clé i,j de la globale g, sous forme d'octets.
 |
 |  keys(self, key)
 |      Traverse une globale à partir de la clé spécifiée, en retournant chaque clé dans la globale.
 |      Exemple : for key in g.keys([i, j]) traverse g à partir de la clé i,j, en retournant chaque clé à son tour. Deuxième argument facultatif 1 ou -1, si -1 inverse l'ordre retourné
 |
 |  kill(self, key)
 |      Étant donné les clés d'une globale sous forme de liste, supprime ce nœud de la globale et sa sous-arborescence.
 |      Exemple : g.kill([i,j]) supprime le nœud stocké à la clé i,j de la globale g et tous ses descendants.
 |
 |  order(self, key)
 |      Étant donné les clés d'une globale sous forme de liste, renvoie la clé suivante de la globale, second argument facultatif 1 ou -1, si -1 renvoie la clé précédente.
 |      Exemple : j = g.order([i,j]) attribue à j la clé de deuxième niveau suivante de la globale g.
 |
 |  orderiter(self, key)
 |      Traverse une globale à partir de la clé spécifiée, en renvoyant la clé et la valeur suivantes sous la forme d'un tuple.
 |      Exemple : pour (clé, valeur) dans g.orderiter([i,j]) traverse g à partir de la clé i,j, en renvoyant la clé et la valeur suivantes. Deuxième argument facultatif : 1 ou -1, si -1, l'ordre retourné est inversé.
 |
 |  query(self, key)
 |      Traverse une globale à partir de la clé spécifiée, en renvoyant chaque clé et chaque valeur sous la forme d'un tuple.
 |      Exemple : pour (clé, valeur) dans g.query([i,j]) traverse g à partir de la clé i,j, en renvoyant chaque clé et chaque valeur à tour de rôle. Deuxième argument facultatif : 1 ou -1, si -1, l'ordre retourné est inversé.
 |
 |  set(self, key, value)
 |      Étant donné les clés d'une globale sous forme de liste, définit la valeur stockée à cette clé de la globale.
 |      Exemple : g.set([i,j], 10) fixe la valeur du nœud à la clé i,j de la globale g à 10
 |
 |  ----------------------------------------------------------------------
 |  Les méthodes statiques sont définies ci-dessous:
 |
 |  __new__(*args, **kwargs) from builtins.type
 |      Création et retour d'un nouvel objet.  Consultez help(type) pour obtenir une signature précise.

5.1 Définition des valeurs globales

ClassMethod SetGlobal() [ Language = python ]
{
import iris
#création d'une référence globale
g = iris.gref('^mygbl') 
g[1],g[2]='Mon','Tue'
g["95752455",1]=iris.execute('return $LFS("Ashok,55720,9639639639,test@gmail.com",",")')
g["85752400",1]=iris.execute('return $LB("Test","9517539635","t@gmail.com")')
g["test","c1"]=iris.execute('return ##Class(MyLearn.EmbeddedPython).executeAndGetResult()') # method wil return some listbuild values# déclaration de valeurs à l'aide de la fonction set
g.set([3],'Wed')
g.set([3,1,1],'Test multilevel')
}

5.2 Obtention des valeurs globales
Récupérez les valeurs globales à partir de python directement en utilisant la méthode subscripts ou get.

ClassMethod GetGlobal() [ Language = python ]
{
    import iris
    #obtient une référence globale
    g = iris.gref('^mybgl') 
    # obtention de valeurs
    print(g[3,1,1])
    print(g.get([2,1]))
    print(g["95752455",1])
}

5.3 Traversée 

order - Traverser la globale est essentiel pour collecter plusieurs niveaux de données de la globale. Cette commande en Embedded Python  order fonctionne de manière similaire à la commande $Order en utilisant la fonction order du fichier iris.gref. Au départ, il est nécessaire d'établir une référence à l'entité globale qui doit être traversée.

Traversée à un seul niveau d'indice

ClassMethod DollarOrder() [ Language = python ]
{
    import iris
    g = iris.gref('^MyLearn.EmbeddedPythonD') # I use my persistent class global
    key = ''
    while True:
        key = g.order([key])
        if key == None:
            breakprint(f'{key} {g.get([key])}')
}

Traversée à plusieurs niveaux d'indices

 
global
zw ^mygbl
^mygbl(1)="Mon"
^mygbl(2)="Tue"
^mygbl(3)="Wed"
^mygbl(3,1,1)="Test multilevel"
^mygbl(85752400,1)=$lb("Test","9517539635","t@gmail.com")
^mygbl(95752455,1)=$lb("Ashok","55720","9639639639","test@gmail.com")
^mygbl("test","c1")=$lb("Test","8527538521","pylearn@gmail.com")
 
ClassMethod DollarOrderMultiLevel() [ Language = python ]
{
 import iris
 g = iris.gref('^mygbl')
 key1= ''whileTrue:
 	key1 = g.order([key1])
 	if key1== None:
 		break
 	key2 = ''whileTrue:
 		key2 = g.order([key1,key2])
 		if key2 == None:
 			break
 		value = g.get([key1,key2])
 		print(key1,key2,value)
}

query - La fonction de requête à partir de iris.gref est similaire à $query. Cette fonction is rassemble toutes les valeurs globales en tuples. Le résultat du tuple contient les identifiants dans la liste et les valeurs sont le tuple suivant. Vous pouvez consulter l'exemple de tuple ci-dessous 

 
tuple
ex: 
zw ^mybgl
^mybgl(1)="Mon"
^mybgl(2)="Tue"
^mybgl(3)="Wed"
^mybgl(3,1,1)="Test multilevel"
^mybgl(95752455,1)=$lb("Ashok","55720","9639639639","test@gmail.com")

Python tuple : ( [ids], data)
(['1'], 'Mon')
(['2'], 'Tue')
(['3'], 'Wed')
(['3', '1', '1'], 'Test multilevel')
(['95752455', '1'], '\x07\x01Ashok\x07\x0155720\x0c\x019639639639\x10\x01test@gmail.com')

ClassMethod DollarQuery() [ Language = python ]
{
 	import iris
 	g = iris.gref('^mygbl')
 	key = g.query()#cela renverra des tuples de tous les indicesfor x in key:
 		print(x) # résultat (['3', '1', '1'], 'Test multilevel')
}

data - la fonction data Vérifie si l'indice donné existe dans le global et renvoie les valeurs $data en utilisant la fonction de données

ClassMethod DollarData() [ Language = python ]
{
    import iris
    g = iris.gref('^mygbl')
    key1= ''
    print(g.data([1]))
}

 

6. Routines

En outre, il est essentiel d'implémenter les membres de la classe. Nous devons exécuter les routines dans le cadre de la mise en œuvre pour les systèmes de base de code hérités et d'autres situations connexes. Par conséquent, il existe une fonction spécifique dans le paquet de la bibliothèque iris qui permet l'invocation de routines à partir de Embedded Python grâce à l'utilisation de la fonction routine.

 
myirispython.mac
myirispython
 123
 q
ir1
 "running ir1"
 q
add(p1,p2) public{
return p1+p2
}
sub(p1,p2)
 c= p1-p2
ClassMethod RunRoutines() [ Language = python ]
{
    import iris
    iris.routine('^myirispython')
    iris.routine('add^myirispython',1,2) # same aswrite$$add^myirispython(1,2)
}

En outre, vous pouvez également exécuter la routine à l'aide de la fonction d'exécution. iris.execute('do ^myirispython')

remarque : si la routine n'est pas trouvée 
>>> iris.routine('^myirispythonT')
Traceback (dernier appel récent):
  File "<input>", line 1, in <module>
RuntimeError: Routine introuvable

Les autres sujets seront abordés dans le prochain article.

0
0 40
Article Lorenzo Scalese · Nov 18, 2024 8m read

Bonjour la communauté,

Dans cet article, je vais décrire et illustrer le processus de mise en œuvre d'ObjectScript au sein de Embedded Python. Cette discussion fera également référence à d'autres articles relatifs à Embedded Python, et répondra aux questions qui ont été utiles à mon apprentissage.

Comme vous le savez peut-être, l'intégration des fonctionnalités de Python dans IRIS est possible depuis un certain temps. Cet article se concentrera sur la manière dont ObjectScript peut être incorporé de manière transparente à Embedded Python.

Essentiellement, Embedded Python sert d'extension qui permet une écriture et une exécution indépendantes. Il permet l'intégration transparente du code Python avec ObjectScript et vice versa, permettant aux deux de s'exécuter dans le même contexte. Cette fonctionnalité améliore considérablement les capacités de votre implémentation.

Pour commencer, vous devez spécifier le langage de votre code Python dans la définition de la classe en utilisant le mot-clé "language" [language = "python"]. Une fois cette étape franchie, vous êtes prêt à écrire votre code Python.

import iris - Ce paquet iris est une bibliothèque Python essentielle qui facilite la communication avec les classes, routines, globales et SQL de l'API native d'InterSystems. Ce paquet est facilement disponible par défaut. Quoi qu'il en soit, il est nécessaire d'importer ce paquet au début de votre code Python si vous souhaitez interagir avec IRIS.

Quelques notes importantes avant d'écrire

  • Vous pouvez utiliser une variable spéciale python __name__ pour référencer le nom de classe dans la définition de classe.
  • Use _ for %Methods ex: %New  == _New , %OpenId == _OpenId

Commençons

Mise en œuvre des éléments d'une classe en Python intégré

1.  Objets et Propriétés

Cette partie est essentielle car elle couvre le processus d'initialisation d'un nouvel objet, la modification des valeurs des objets existants et la configuration des propriétés dans des contextes statiques et dynamiques. Créez votre propre définition de classe et utilisez les propriétés littérales simples

1.1 initialisation new d'un nouvel objet / Modification d'un objet existant

Utilisez _New pour initialiser un nouvel objet et _OpenId(id) pour modifier l'objet existant

ClassMethod SaveIRISClsObject() [ Language = python ]
{
 #cette méthode appelle la méthode de rappel %OnNew et récupère l'objetimport iris
 try:
     iris_obj =  iris.cls(__name__)._New()
     ifnot iris.cls(__name__).IsObj(iris_obj):
      #IsObj est la méthode wrapper d'objectscript : elle contient $Isobject()raise ReferenceError('Object Initlize Error')
 except ReferenceError as e:
     print(e)
     return#définition des propriétés de l'objet et enregistrement des valeurs 
 iris_obj.Name = 'Ashok'
 iris_obj.Phone = 9639639635
 status = iris_obj._Save()
 print(status)
 return status
}

1.2 Accès  aux propriétés

Avant de procéder à la partie sur les propriétés, il est important de noter que le type de données IRIS diffère des types de données Python et que, par conséquent, les types de données de collecte IRIS ne peuvent pas être utilisés directement dans Python. Pour résoudre ce problème, InterSystems a proposé une solution complète pour convertir les types de données IRIS en formats compatibles avec Python, tels que les listes, les ensembles et les tuples. Pour ce faire, il suffit d'importer le module "builtins" dans la base de code IRIS, en utilisant les méthodes de classe ##class(%SYS.Python).Builtins() ou en définissant les builtins = ##class(%SYS.Python).Import("builtins"). Je reviendrai sur ce point dans les prochaines parties.

J'utilise donc cette méthode pour convertir les propriétés $LB en liste  python afin d'accéder aux propriétés au moment de l'exécution en python

LEARNING>Set pyList = ##class(%SYS.Python).ToList($LB("Name","Phone","City"))
 
LEARNING>zw pyList
pyList=5@%SYS.Python  ; ['Name', 'Phone', 'City']  ; <OREF>
ClassMethod GetProperties() [Language = objectscript]
{
    set pyList = ##class(%SYS.Python).ToList($LB("Name","Phone","City"))
    do..pyGetPropertiesAtRunTime(pyList)
}
ClassMethod pyGetPropertiesAtRunTime(properties) [ Language = python ]
{
    import iris
    iris_obj = iris.cls(__name__)._OpenId(1)
    for prop in properties:
        print(getattr(iris_obj,prop))
}

 

1.3 Définition des propriétés au moment de l'exécution.

J'utilise ce dictionnaire python pour désigner ma propriété en tant que clé et, avec les valeurs de propriété correspondantes servant de valeurs dans ce dictionnaire. Vous pouvez vous référer au code fourni ci-dessous et à l'article de la communauté concernant ce jeu de propriétés .

ClassMethod SetProperties()
{
	Set pyDict = ##class(%SYS.Python).Builtins().dict()
	do pyDict.setdefault("Name1", "Ashok kumar")
	do pyDict.setdefault("Phone", "9639639635")
	do pyDict.setdefault("City", "Zanesville")
	Set st = ..pySetPropertiesAtRunTime(pyDict)
}

ClassMethod pySetPropertiesAtRunTime(properties As%SYS.Python) [ Language = python ] { import iris iris_obj = iris.cls(name)._New() for prop in properties: setattr(iris_obj, prop,properties[prop])

status = iris_obj._Save()
<span class="hljs-keyword">return</span> status

}

1.4 Contexte de partage d'objets

Comme j'ai indiqué précédemment, Python et ObjectScript opèrent dans le même contexte de mémoire et partagent des objets. Cela implique que vous pouvez créer ou ouvrir un objet dans la classe InCache et, par la suite, le définir ou le récupérer dans la classe Python.

ClassMethod ClassObjectAccess() [Language = objectscript]
{
	Set obj = ..%OpenId(1)
	Write obj.PropAccess(),! ; prints "Ashok kumar"Do obj.DefineProperty("test")
	Write obj.PropAccess() ; prints "test"
}

Method PropAccess() [ Language = python ] {

return self.Name }

Method DefineProperty(name) [ Language = python ] {

self.Name = name }

2.  Parameters

Get the parameter arbitrary key value pair by using the _GetParameter. Refer the useful community post 

ClassMethod GetParam(parameter = "MYPARAM") [ Language = python ]
{
	import iris
	value = iris.cls(__name__)._GetParameter(parameter)
	print(value)
}

3. La méthode de classe et les méthodes

3.1 La méthode de classe

L'invocation des méthodes et des fonctions de classe est très utile pour l'exécution du code de script d'objet.

Il est possible d'invoquer la méthode de classe en tant qu'appel statique,  par exemple: Do ..Test() 

ClassMethod InvokeStaticClassMethods(clsName = "MyLearn.EmbeddedPython") [ Language = python ]
{
	import iris
	print(iris.cls(clsName).Test())
	# print(iris.cls(__name__).Test()) 
}


Invocation de la méthode de Classe au moment de l'exécution Set method="Test" Do $ClassMethod(class, method, args...)

ClassMethod InvokeClassMethodsRunTime(classMethod As %String = "Test") [ Language = python ]
{
 import iris
 clsMethodRef = getattr(iris.cls(__name__), classMethod) # renvoie la référence de la méthode
 print(clsMethodRef()) 
}

3.2  Méthodes

Invocation des méthodes d'instance est identique au format "script d'objet". Dans le code ci-dessous, j'ai d'abord créé l'objet, puis j'ai appelé la méthode d'instance avec des paramètres.

ClassMethod InvokeInstanceMethodWithActualParameters() [ Language = python ]
{
	import iris
	obj = iris.cls(__name__)._New()
	print(obj.TestMethod(1,2,4))
}

3.3  Transmission d'arguments par valeur et  par référence entre python et ObjectScript

Fondamentalement, la transmission des arguments est  inévitable entre les fonctions et elle en sera de même entre ObjectScript et Python

3.4  Transmission d'arguments par valeur - C'est comme d'habitude la transmission d'arguments par valeur

ClassMethod passbyvalfromCOStoPY()
{
    Set name = "test", dob= "12/2/2002", city="chennai"Do..pypassbyvalfromCOStoPY(name, dob, city)
}

ClassMethod pypassbyvalfromCOStoPY(name As%String, dob As%String, city As%String) [ Language = python ] { print(name,' ',dob,' ',city) }

/// transmission par valeur de python au script d'objetClassMethod pypassbyvalfromPY2COS() [ Language = python ] { import iris name = 'test' dob='12/2/2002' city='chennai' iris.cls(name).passbyvalfromPY2COS(name, dob, city) }

ClassMethod passbyvalfromPY2COS(name As%String, dob As%String, city As%String) { zwrite name,dob,city }

3.5 Transmission par référence- C'est au contraire de la transmission par valeur. Comme Python ne supporte pas nativement l'appel par référence, il faut donc utiliser la fonction iris.ref()  dans le code Python pour que la variable devienne une référence. à savoir, la référence . A ma connaissance, il n'y a pas d'effets du côté du script d'objet concernant les variables de type pass-by-reference (transmission par référence), même lorsque ces variables sont modifiées en Python. Par conséquent, les variables Python seront affectées par ce mécanisme de pass-by-reference lorsque les méthodes du script d'objet seront invoquées

ClassMethod pypassbyReffromPY2COS() [ Language = python ]
{
	import iris
	name='python'
	dob=iris.ref('01/01/1991')
	city = iris.ref('chennai')
	print('before COS ',name,'  ',dob.value,'  ',city.value)
	#transmission par référence de la date de naissance, ville
	iris.cls('MyLearn.EmbeddedPythonUtils').passbyReffromPY2COS(name, dob, city)	
	print('after COS ',name,'  ',dob.value,'  ',city.value)
}

ClassMethod passbyReffromPY2COS(name, ByRef dob, ByRef city) { Set name="object script", dob="12/12/2012", city="miami" }

// résultat LEARNING>do##class(MyLearn.EmbeddedPythonUtils).pypassbyReffromPY2COS() before COS python 01/01/1991 chennai after COS python 12/12/2012 miami


3.5 **kwargs- Il existe un support supplémentaire pour passer les arguments de mot-clé python (**kwargs) à partir d'un script d'objet. InterSystems IRIS n'ayant pas de concept d'arguments de mot-clé, il faut créer un  %DynamicObject pour contenir les paires mot-clé/valeur et passer les valeurs en tant qu' Args...de syntax

J'ai créé le dynamicObject "name""ashok""city""chennai"}et j'y ai inséré les paires clé-valeur requises, que j'ai ensuite transmises au code python.

ClassMethod KWArgs()
{
    set kwargs={ "name": "ashok", "city": "chennai"}
    do..pyKWArgs(kwargs...)
}

ClassMethod pyKWArgs(name, city, dob = "") [ Language = python ] { print(name, city, dob) }

// résultat LEARNING>do##class(MyLearn.EmbeddedPythonUtils).KWArgs() ashok chennai

Je décrirai les globales, les routines et SQL dans le prochain article

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

L'utilisation traditionnelle d'une production IRIS consiste, pour un adaptateur entrant, à recevoir des données d'une source externe, à envoyer ces données à un service IRIS, puis à faire en sorte que ce service envoie ces données par l'intermédiaire de la production.

Cependant, grâce à un adaptateur entrant personnalisé, nous pouvons faire en sorte qu'une production IRIS soit plus performante. Nous pouvons utiliser une production IRIS pour traiter les données de notre propre base de données sans aucun déclencheur externe.

BEn utilisant une production IRIS de cette manière, vos tâches de traitement des données peuvent désormais tirer parti de toutes les fonctionnalités intégrées d'une production IRIS, y compris:

  • Suivi et contrôle avancés
  • Traitement multithread pour l'évolutivité
  • Logique métier basée sur la configuration
  • Opérations IRIS intégrées pour se connecter rapidement à des systèmes externes
  • Récupération rapide des défaillances du système

La documentation pour créer un adaptateur entrant personnalisé peut être consultée à: https://docs.intersystems.com/hs20231/csp/docbook/DocBook.UI.Page.cls?KEY=EGDV_adv#EGDV_adv_adapterdev_inbound

Regardons 3 exemples d'une production simple configurée pour traiter des objets "Fish" à partir d'une base de données.

Dans le premier exemple, nous allons créer une production pilotée par les données qui traitera continuellement les données.

Dans le deuxième exemple, nous modifierons cette production pour traiter les données uniquement à des moments précis.

Dans le troisième exemple, nous modifierons cette production pour traiter les données uniquement lorsqu'elles sont déclenchées via une tâche système.

Exemple 1: Traitement continu des données

Cet exemple est une simple production configurée pour traiter continuellement des objets "Fish" à partir d'une base de données. Tout ce que fait la production est de rechercher continuellement de nouveaux objets fish, de convertir ces objets fish en JSON, puis de recracher ce JSON dans un fichier.

Tout d'abord, nous créons l'objet Fish que nous avons l'intention de traiter:

Class Sample.Fish Extends (%Persistent, Ens.Util.MessageBodyMethods, %JSON.Adaptor, %XML.Adaptor)
{

Parameter ENSPURGE As%Boolean = 0;Property Type As%String;Property Size As%Numeric;Property FirstName As%String;Property Status As%String [ InitialExpression = "Initialized" ];
Index StatusIndex On Status;
}

L'état est important car c'est ainsi que nous distinguerons l'état des objets fish non traités de l'état des objets traités.

En fixant ENSPURGE à 0 empêchera cet objet d'être purgé avec les en-têtes du message à l'avenir.

Deuxièmement, nous créons un adaptateur personnalisé pour rechercher les nouveaux objets fish:

Class Sample.Adapter.FishMonitorAdapter Extends Ens.InboundAdapter
{

/// La valeur d'état d'objet Fish sera demandée par l'adaptateur. Tous les objets Fish correspondants auront leur état défini par SetFishStatus et seront ensuite envoyés au service.Property GetFishStatus As%String [ InitialExpression = "Initialized", Required ];/// L'état d'objet Fish correspond à la valeur que le service attribue à l'objet Fish avant qu'il ne soit envoyé au service.Property SetFishStatus As%String [ InitialExpression = "Processed", Required ];Parameter SETTINGS = "GetFishStatus:Basic,SetFishStatus:Basic";Parameter SERVICEINPUTCLASS = "Sample.Fish";
Method OnTask() As%Status
{
	//Curseur pour rechercher les objets Fish correspondantsset getFishStatus = ..GetFishStatus
	&sql(declare fishCursor cursorforselectIDinto :fishId
		from Sample.Fish
		whereStatus = :getFishStatus)
	
	//Exécution du curseur
	&sql(open fishCursor)
	for {
		&sql(fetch fishCursor)
		quit:SQLCODE'=0//Changez l'état de chaque objet Fish correspondant et envoyez-le au service (BusinessHost).set fishObj = ##class(Sample.Fish).%OpenId(fishId)
		set fishObj.Status = ..SetFishStatus$$$ThrowOnError(fishObj.%Save())
		$$$ThrowOnError(..BusinessHost.ProcessInput(fishObj))
	}
	&sql(close fishCursor)
	if SQLCODE < 0 {
		throw##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE,%msg)
	}
	
	quit$$$OK
}

La méthode OnTask() recherche tous les objet Fish correspondant à la valeur GetFishStatus configurée. Pour chaque objet Fish trouvé, elle modifie son état en fonction de la valeur SetFishStatus configurée, puis le transmet à la méthode ProcessInput du service.

Troisièmement, nous créons un service personnalisé pour utiliser cet adaptateur:

Class Sample.Service.FishMonitorService Extends Ens.BusinessService
{

/// Élément de configuration auquel les messages doivent être envoyésProperty TargetConfigName As Ens.DataType.ConfigName;Parameter SETTINGS = "TargetConfigName:Basic";Parameter ADAPTER = "Sample.Adapter.FishMonitorAdapter";
Method OnProcessInput(pInput As Sample.Fish, pOutput As%RegisteredObject) As%Status
{
    quit:..TargetConfigName=""//Envoyer l'objet Fish vers la cible configuréequit..SendRequestAsync(..TargetConfigName, pInput)
}

}

Ce service prend les objet Fish en entrée et les transmet via une requête asynchrone à la cible configurée.

Quatrièmement, nous créons un processus métier personnalisé pour convertir l'objet Fish en JSON.

Class Sample.Process.FishToJSONProcess Extends Ens.BusinessProcess
{

/// Élément de configuration auquel les messages doivent être envoyésProperty TargetConfigName As Ens.DataType.ConfigName;Parameter SETTINGS = "TargetConfigName:Basic";
Method OnRequest(pRequest As Sample.Fish, Output pResponse As Ens.Response) As%Status
{
	//Convertissez l'objet Fish en un flux JSONdo pRequest.%JSONExportToStream(.jsonFishStream)
	//Créez un nouveau conteneur de flux avec un flux JSONset tRequest = ##class(Ens.StreamContainer).%New(jsonFishStream)
	//Envoyez le conteneur de flux à la cible configuréequit..SendRequestAsync(..TargetConfigName, tRequest, 0)
}

Method OnResponse(request As Ens.Request, ByRef response As Ens.Response, callrequest As Ens.Request, callresponse As Ens.Response, pCompletionKey As%String) As%Status
{
    quit$$$OK
}

}

La méthode OnRequest() est la seule méthode qui agisse. Il accepte l'objet Fish, génère un flux JSON à partir de l'objet Fish, conditionne ce flux dans un conteneur Ens.StreamContainer, puis transmet ce conteneur de flux via une requête asynchrone à la cible configurée.

Enfin, nous configurons la production:

Class Sample.DataProduction Extends Ens.Production
{

XData ProductionDefinition
{
<Production Name="Sample.DataProduction" LogGeneralTraceEvents="false">
  <Description></Description>
  <ActorPoolSize>2</ActorPoolSize>
  <Item Name="Sample.Service.FishMonitorService" Category="" ClassName="Sample.Service.FishMonitorService" PoolSize="1" Enabled="true" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
    <Setting Target="Host" Name="TargetConfigName">Sample.Process.FishToJSONProcess</Setting>
  </Item>
  <Item Name="Sample.Process.FishToJSONProcess" Category="" ClassName="Sample.Process.FishToJSONProcess" PoolSize="1" Enabled="true" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
    <Setting Target="Host" Name="TargetConfigName">EnsLib.File.PassthroughOperation</Setting>
  </Item>
  <Item Name="EnsLib.File.PassthroughOperation" Category="" ClassName="EnsLib.File.PassthroughOperation" PoolSize="1" Enabled="true" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
    <Setting Target="Adapter" Name="FilePath">C:\temp\fish\</Setting>
  </Item>
</Production>
}

}

Il ne reste plus qu'à la tester. Pour cela, il suffit d'ouvrir une fenêtre de terminal et de créer un nouvel objet Fish.

En regardant les messages de production, nous pouvons voir que l'objet Fish a été trouvé et transformé:

Nous pouvons inspecter la trace des deux messages:

En regardant le dossier de sortie (C:\temp\fish\), nous pouvons voir le fichier de sortie:

Exemple 2: Traitement des données basé sur les horaires

Pour les cas d'utilisation où nous ne voulons traiter les données qu'à des moments précis, comme la nuit, nous pouvons configurer le service pour qu'il s'exécute selon des horaires précis.

Pour modifier l'exemple 1 pour qu'il fonctionne selon des horaires, nous créons d'abord une spécification de l'horaire. La documentation sur la manière de procéder est disponible ici: https://docs.intersystems.com/iris20231/csp/docbook/DocBook.UI.PortalHelpPage.cls?KEY=Ensemble%2C%20Schedule%20Editor

Ensuite, nous modifions la configuration du service pour utiliser cet horaire:

Class Sample.DataProduction Extends Ens.Production
{

XData ProductionDefinition
{
<Production Name="Sample.DataProduction" LogGeneralTraceEvents="false">
  <Description></Description>
  <ActorPoolSize>2</ActorPoolSize>
  <Item Name="Sample.Service.FishMonitorService" Category="" ClassName="Sample.Service.FishMonitorService" PoolSize="1" Enabled="true" Foreground="false" Comment="" LogTraceEvents="false" Schedule="@Midnight Processing">
    <Setting Target="Host" Name="TargetConfigName">Sample.Process.FishToJSONProcess</Setting>
  </Item>
  <Item Name="Sample.Process.FishToJSONProcess" Category="" ClassName="Sample.Process.FishToJSONProcess" PoolSize="1" Enabled="true" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
    <Setting Target="Host" Name="TargetConfigName">EnsLib.File.PassthroughOperation</Setting>
  </Item>
  <Item Name="EnsLib.File.PassthroughOperation" Category="" ClassName="EnsLib.File.PassthroughOperation" PoolSize="1" Enabled="true" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
    <Setting Target="Adapter" Name="FilePath">C:\temp\fish\</Setting>
  </Item>
</Production>
}

}

Maintenant, lorsque nous regardons cet onglet "Tâches" du service, nous voyons qu'il n'y a aucun tâche en cours:

Désormais, ce service n'aura plus que des tâches à exécuter entre minuit et 1 heure du matin.

Exemple 3: Traitement de données sur la base des événements avec le Gestionnaire de tâches

Pour les cas d'utilisation où nous ne voulons traiter les données qu'une seule fois à un moment précis ou lorsqu'un événement particulier a lieu, nous pouvons configurer le service pour qu'il ne s'exécute que lors de l'exécution d'une tâche système.

Pour modifier l'exemple 1 afin qu'il ne s'exécute que lorsqu'il est déclenché par une tâche, nous créons d'abord une tâche personnalisée pour déclencher le service.

Class Sample.Task.TriggerServiceTask Extends%SYS.Task.Definition
{

/// Le nom du service métier que cette tâche doit exécuter.Property BuinessServiceName As%String [ Required ];
Method OnTask() As%Status
{
	#dim pBusinessService As Ens.BusinessService
	$$$ThrowOnError(##class(Ens.Director).CreateBusinessService(..BuinessServiceName, .pBusinessService))
	Quit pBusinessService.OnTask()
}

}

Deuxièmement, nous configurons une nouvelle tâche système. La documentation sur la configuration des tâches système est disponible ici: https://docs.intersystems.com/iris20233/csp/docbook/Doc.View.cls?KEY=GSA_manage_taskmgr

Ceci est la partie personnalisée du processus de configuration pour cet exemple:

En outre, je configure la tâche pour qu'elle soit exécutée à la demande, mais vous pouvez aussi établir un horaire.

Enfin, nous configurons la production:

Class Sample.DataProduction Extends Ens.Production
{

XData ProductionDefinition
{
<Production Name="Sample.DataProduction" LogGeneralTraceEvents="false">
  <Description></Description>
  <ActorPoolSize>2</ActorPoolSize>
  <Item Name="Sample.Service.FishMonitorService" Category="" ClassName="Sample.Service.FishMonitorService" PoolSize="0" Enabled="true" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
    <Setting Target="Host" Name="TargetConfigName">Sample.Process.FishToJSONProcess</Setting>
  </Item>
  <Item Name="Sample.Process.FishToJSONProcess" Category="" ClassName="Sample.Process.FishToJSONProcess" PoolSize="1" Enabled="true" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
    <Setting Target="Host" Name="TargetConfigName">EnsLib.File.PassthroughOperation</Setting>
  </Item>
  <Item Name="EnsLib.File.PassthroughOperation" Category="" ClassName="EnsLib.File.PassthroughOperation" PoolSize="1" Enabled="true" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
    <Setting Target="Adapter" Name="FilePath">C:\temp\fish\</Setting>
  </Item>
</Production>
}

}

Notez que nous avons fixé le PoolSize de Sample.Service.FishMonitorService à 0.

Il ne reste plus qu'à la tester. Pour cela, il suffit d'ouvrir une fenêtre de terminal et de créer un nouvel objet Fish.

En regardant les messages de production, nous pouvons voir que l'objet Fish n'a pas encore été transformé:

Ensuite, nous exécutons la tâche à la demande pour déclencher le service:

Maintenant, en regardant les messages de production, nous pouvons voir que le service a été déclenché, ce qui a permis de trouver et de traiter l'objet Fish:

Nous pouvons inspecter la trace des deux messages:

En regardant le dossier de sortie (C:\temp\fish\), nous pouvons voir le fichier de sortie:

Conclusion

Les exemples ci-dessus sont assez simples. Vous pouvez cependant configurer les productions pour en faire beaucoup plus. Y compris…

En fait, il est possible de réaliser ici tout ce qui peut être fait dans le cadre d'une production typique d'IRIS.

0
0 44
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
Article Lorenzo Scalese · Août 27, 2024 5m read

 

Démarrage rapide des données SQL d'InterSystems Cloud dans Databricks

La mise en œuvre de Databricks en SQL d'InterSystems Cloud se compose de quatre parties.

  • Obtention du certificat et du pilote JDBC Driver pour InterSystems IRIS
  • Ajout d'un script d'initialisation et d'une bibliothèque externe à votre Cluster de calcul Databricks
  • Obtention de données
  • Placement des données

Téléchargement du certificat X.509/du pilote JDBC de Cloud SQL

Naviguez vers la page d'aperçu de votre déploiement, si vous n'avez pas activé de connexions externes, faites-le et téléchargez votre certificat et le pilote jdbc depuis la page d'aperçu.

 

J'ai utilisé intersystems-jdbc-3.8.4.jar et intersystems-jdbc-3.7.1.jar avec succès dans Databricks à partir de la distribution de pilotes Driver Distribution.

Script d'initialisation pour votre cluster Databricks

La façon la plus simple d'importer un ou plusieurs certificats CA personnalisés dans votre cluster Databricks est de créer un script d'initialisation qui ajoute la chaîne complète de certificats CA aux magasins de certificats par défaut SSL et Java de Linux, et définit la propriété REQUESTS_CA_BUNDLE. Collez le contenu du certificat X.509 que vous avez téléchargé dans le bloc supérieur du script suivant:

import_cloudsql_certficiate.sh
#!/bin/bash

cat << 'EOF' > /usr/local/share/ca-certificates/cloudsql.crt
-----BEGIN CERTIFICATE-----
<PASTE>
-----END CERTIFICATE-----
EOF

update-ca-certificates

PEM_FILE="/etc/ssl/certs/cloudsql.pem" PASSWORD="changeit" JAVA_HOME=$(readlink -f /usr/bin/java | sed "s:bin/java::") KEYSTORE="$JAVA_HOME/lib/security/cacerts" CERTS=$(grep 'END CERTIFICATE'$PEM_FILE| wc -l)

# Pour traiter plusieurs certificats avec keytool, vous devez extraire# chacun d'eux du fichier PEM et l'importer dans le KeyStore Java.for N in $(seq 0 $(($CERTS - 1))); do ALIAS="$(basename $PEM_FILE)-$N"echo"Adding to keystore with alias:$ALIAS" cat $PEM_FILE | awk "n==$N { print }; /END CERTIFICATE/ { n++ }" | keytool -noprompt -import -trustcacerts
-alias$ALIAS -keystore $KEYSTORE -storepass $PASSWORDdoneecho"export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt" >> /databricks/spark/conf/spark-env.sh echo"export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt" >> /databricks/spark/conf/spark-env.sh

Maintenant vous avez le script initial, téléchargez-le dans le catalogue Unity sur un Volume.

Une fois que le script est sur un volume, vous pouvez ajouter le script initial au cluster à partir du volume dans les propriétés avancées de votre cluster.


Ensuite, ajoutez le pilote/la bibliothèque intersystems jdbc au cluster...

...et démarrez ou redémarrez votre calcul.

Station Databricks - Entrée vers le Cloud SQL d'InterSystems IRIS

 

Créez un Notebook Python dans votre espace de travail, attachez-le à votre cluster et testez le glissement de données vers Databricks.  Sous le capot, Databricks va utiliser pySpark, si cela n'est pas immédiatement évident.

La construction du Dataframe Spark suivant est tout ce qu'il vous faut, vous pouvez récupérer vos informations de connexion à partir de la page d'aperçu comme auparavant.

df = (spark.read
  .format("jdbc")
  .option("url", "jdbc:IRIS://k8s-05868f04-a4909631-ac5e3e28ef-6d9f5cd5b3f7f100.elb.us-east-1.amazonaws.com:443/USER")
  .option("driver", "com.intersystems.jdbc.IRISDriver")
  .option("dbtable", "(SELECT name,category,review_point FROM SQLUser.scotch_reviews) AS temp_table;") 
  .option("user", "SQLAdmin")
  .option("password", "REDACTED")
  .option("driver", "com.intersystems.jdbc.IRISDriver")\
  .option("connection security level","10")\
  .option("sslConnection","true")\
  .load())

df.show()

Illustration de la production d'un dataframe à partir de données dans Cloud SQL... boom!

Station Databricks - Sortie du Cloud SQL d'InterSystems IRIS

 

Prenons maintenant ce que nous avons lu dans IRIS et écrivons-le avec Databricks. Si vous vous souvenez bien, nous n'avons lu que 3 champs dans notre cadre de données, nous allons donc les réécrire immédiatement et spécifier un mode "écraser".

df = (spark.read
  .format("jdbc")
  .option("url", "jdbc:IRIS://k8s-05868f04-a4909631-ac5e3e28ef-6d9f5cd5b3f7f100.elb.us-east-1.amazonaws.com:443/USER")
  .option("driver", "com.intersystems.jdbc.IRISDriver")
  .option("dbtable", "(SELECT TOP 3 name,category,review_point FROM SQLUser.scotch_reviews) AS temp_table;") 
  .option("user", "SQLAdmin")
  .option("password", "REDACTED")
  .option("driver", "com.intersystems.jdbc.IRISDriver")\
  .option("connection security level","10")\
  .option("sslConnection","true")\
  .load())

df.show()

mode = "overwrite" properties = { "user": "SQLAdmin", "password": "REDACTED", "driver": "com.intersystems.jdbc.IRISDriver", "sslConnection": "true", "connection security level": "10", }

df.write.jdbc(url="jdbc:IRIS://k8s-05868f04-a4909631-ac5e3e28ef-6d9f5cd5b3f7f100.elb.us-east-1.amazonaws.com:443/USER", table="databricks_scotch_reviews", mode=mode, properties=properties)

Exécution du Notebook

 
Illustration des données dans le Cloud SQL d'InterSystems!

Les points à prendre en considération

  • Par défaut, PySpark écrit les données en utilisant plusieurs tâches concurrentes, ce qui peut entraîner des écritures partielles si l'une des tâches échoue.
  • Pour garantir que l'opération d'écriture est atomique et cohérente, vous pouvez configurer PySpark de manière à ce que les données soient écrites à l'aide d'une seule tâche (c'est-à-dire en définissant le nombre de partitions à 1) ou utiliser une fonctionnalité spécifique à IRIS, telle que les transactions.
  • En outre, vous pouvez utiliser l'API DataFrame de PySpark pour effectuer des opérations de filtrage et d'agrégation avant de lire les données de la base de données, ce qui peut réduire la quantité de données à transférer sur le réseau.
0
0 44
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 Lorenzo Scalese · Août 8, 2024 3m read

Parfois, nous devons convertir le message FHIR en HL7 V2, par exemple pour enregistrer un patient dans le système PACS.
Dans cet article, les étapes à suivre pour obtenir les résultats souhaités en utilisant la production du serveur IRIS FHIR seront expliquées.

Voici les étapes à suivre:

  1. Assurez-vous que la production du serveur FHIR est démarrée.
  2. Enregistrez le service métier avec le point de terminaison FHIRServer.
  3. Définissez les processus métier pour convertir les messages FHIR en SDA, puis convertissez SDA en HL7 v2.
  4. Publiez la ressource JSON sur le point de terminaison FHIRServer et obtenez la réponse HL7 V2.

Examinons les étapes en détail.
 

Étape 1. Assurez-vous que la production du serveur FHIR est démarrée

Ouvrez la page de production et assurez-vous que la Production est démarrée. À l'étape suivante, nous devons nous assurer que le service commercial HS.FHIRServer.Interop.Service est enregistré auprès de FHIRServer


Step 2. Étape 2. Enregistrez le service métier avec le point de terminaison FHIRServer.

Depuis le portail de gestion, cliquez sur l'onglet Health (Santé)

Cliquez ensuite sur Configuration FHIR dans la liste, puis cliquez sur Server Configuration (configuration du serveur)

Sélectionnez le point de terminaison et assurez-vous que HS.FHIRServer.Interop.Service (service commercial défini dans la production) est défini sous le nom Service Config Name.

Étape 3. Définissez les processus métier pour convertir les messages FHIR en SDA, puis convertissez SDA en HL7 v2.

Définissez le rpocessus métier (le processus Solution.BP.Process est déjà défini dans l'application).
Veuillez noter que nous appliquons ici une condition selon laquelle le point de terminaison doit contenir une augmentation hl7 pour procéder à la conversion, sinon il est considéré comme une demande FHIR normale.


Transmettez ensuite le message au processus FHIR_SDA qui le convertira en SDA.
FHIR_SDA est dérivé d'une classe Solution.BP.Process définie par l'utilisateur.
Après conversion FHIR_SDA, le processus transmet le message au processus SDA_HL7
Le processus SDA_HL7 est dérivé d'une classe Solution.BP.SDATransformProcess définied  par l'utilisateur qui convertit le message SDA en message HL7 V2.


Définissez les processus de production comme suit:


Étape 4. Publiez la ressource JSON sur le point de terminaison FHIRServer et obtenez la réponse HL7 V2.

Depuis Postman, appelez le point de terminaison FHIRServer à l'aide de la méthode Post.
Notre point de terminaison FHIRServer est ci-dessous:
http://localhost:32783/csp/fhirserver/fhir/r4/hl7

REMARQUE:Notre point de terminaison FHIRServer est http://localhost:32783/csp/fhirserver/fhir/r4/, mais nous passons hl7 comme argument pour détecter à partir du processus métier de production qu'il ne s'agit pas d'une demande Post FHIR normale mais d'une demande de transformation du message FHIR.

Il s'agit de la même fonctionnalité que celle que nous pouvons utiliser à partir de nos applications Web. 
Sélectionnez la ressource patient, puis cliquez sur la ressource dans la liste, sélectionnez l'onglet HL7 FHIR ou Détails de la ressource, et cliquez sur le bouton "Transformer FHIR en HL7 V2"

L'application obtiendra le message de transformation HL7 V2 à l'aide de la production du serveur FHIR.



Transformation de HL7 V2 à FHIR

Sélectionnez "HL7 to FHIR" dans le menu et entrez les données HL7 V2. Cliquez sur le bouton de conversion pour transformer le message HL7 en message FHIR

La transformation de HL7 en FHIR utilise également la production pour convertir le message HL7 V2 en message FHIR.

Le service métier HL7_Http_Service envoie le message HL7 au processus HL7_SDA, puis HL7_SDA envoie les données SDA au processus SDA_FHIR, qui les convertit finalement en FHIR

Pour plus de détails et une révision du code, veuillez visiter la page d'application de l'échange ouvert iris-fhir-lab .
Merci!

0
0 69
Article Lorenzo Scalese · Juil 29, 2024 9m read

Supposons que vous ayez une application qui permette aux utilisateurs d'écrire des articles et de les commenter. (Attendez...  ça me dit quelque chose...)

L'objectif est de répertorier, pour un utilisateur donné, tous les messages publiés avec lesquels il a interagi, c'est-à-dire dont il est l'auteur ou qu'il a commentés. Comment faites-vous cela aussi vite que possible?

Voici à quoi pourraient ressembler les définitions de notre classe %Persistent comme point de départ (les définitions de stockage sont importantes, mais omises par souci de concision):

Class DC.Demo.Post Extends%Persistent
{

Property Title As%String(MAXLEN = 255) [ Required ];Property Body As%String(MAXLEN = "") [ Required ];Property Author As DC.Demo.Usr [ Required ];Property Draft As%Boolean [ InitialExpression = 1, Required ]; }

Class DC.Demo.Usr Extends%Persistent {

Property Name As%String [ Required ]; }

Class DC.Demo.Comment Extends%Persistent {

Property Post As DC.Demo.Post [ Required ];Property Usr As DC.Demo.Usr [ Required ];Property Comment As%String(MAXLEN = "") [ Required ]; }

Et notre requête, comme point de départ:

selectIDfrom DC_Demo.Post where (Author = ? orIDin (selectdistinct Post from DC_Demo.Comment where Usr = ?)) and Draft = 0

L' approche naïve consisterait simplement à:

  • Ajoutez des indices bitmap sur Author et Draft dans DC.Demo.Post.
  • Ajoutez un index standard sur (Usr, Post) dans DC.Démo.Commentaire..

Et ce n'est pas du tout une mauvaise approche! Pour certains cas d'utilisation, elle peut même être " suffisante ". Que va faire IRIS SQL sous le capot ? Nous pouvons examiner le plan de requête:

 Générer un flux de valeurs idkey en utilisant la combinaison multi-index:
     ((bitmap index DC_Demo.Post.Draft) INTERSECT ((bitmap index DC_Demo.Post.Author) UNION (bitmap index DC_Demo.Post.Draft)))
 Pour chaque valeur d'idkey:
     Affichage de la ligne.

Sous-requête C:
 Lecture de la carte d'index DC_Demo.Comment.UsrPost, en utilisant l'Usr et le Post donnés, et en bouclant sur l'ID.
 Pour chaque ligne:
     Détermination du résultat de la sous-requête.

Ce n'est pas dramatique. Supposons qu'il y ait 50000 publications et que chaque utilisateur ait commenté 500 d'entre elles en moyenne. Combien de références globales cette requête implique-t-elle? Eh bien, au minimum, trois pour les index bitmap, et environ 500 dans la sous-requête (itération sur l'index UsrPost). Il est clair que la sous-requête est le goulot d'étranglement. Comment pouvons-nous la rendre plus rapide?

La réponse est d'utiliser un index fonctionnel (une sous-classe de %Library.FunctionalIndex) avec la %FIND condition de prédicat (et une sous-classe de %SQL.AbstractFind). Notre index fonctionnel sera défini dans la classe Comment (commentaires), mais ne contiendra pas les identifiants des commentaires comme le ferait un index bitmap classique. Au lieu de cela, pour chaque utilisateur, il aura un bitmap d'identifiants de Post pour lesquels cet utilisateur a au moins un commentaire. Nous pouvons ensuite combiner très efficacement cette image bitmap avec d'autres conditions indexées par image bitmap dans la table Post. Il est évident que cela entraîne une certaine surcharge pour l'insertion/mise à jour/suppression de nouveaux commentaires, mais l'avantage en termes de performances pour les lectures peut la compenser.

Un index fonctionnel doit définir le comportement de l'index pour les opérations d'insertion, de mise à jour et de suppression, et mettre en œuvre quelques autres méthodes (purge, début de tri, fin de tri). Une Une implémentation %SQL.AbstractFind doit mettre en œuvre des méthodes pour parcourir et récupérer des fragments d'index bitmap. Pour s'amuser, nous utiliserons une implémentation générique %SQL.AbstractFind qui examine une structure d'index bitmap standard (avec une référence globale à son nœud racine).

Remarque - si vous ne savez pas ce qu'est un "fragment de bitmap" ou si vous avez l'impression que tout cela est du Chinois, nous vous conseillons de lire la documentation  sur les index de bitmap, en particulier les parties relatives à leur structure et à leur manipulation.

Passons au code, DC.Demo.ExistenceIndex est notre index fonctionnel:

Include %IFInclude/// Données:/// /// <code>/// Class Demo.ClassC Extends %Persistent/// {/// Properiété PropA As Demo.ClassA;/// Properiété PropB As Demo.ClassB;/// Index BValuesForA On (PropA, PropB) As DC.Demo.ExistenceIndex;/// }/// </code>/// /// Appel à partir de SQL comme suit, étant donné une valeur de PropA de 21532, pour retourner les valeurs de PropB associées à PropA=21532 dans ClassB:/// <code>/// selectionner * de Demo.ClassC où ID %FIND Demo.ClassB_BValuesForAFind(21532) et <other-bitmap-index-conditions>/// </code>Class DC.Demo.ExistenceIndex Extends%Library.FunctionalIndex [ System = 3 ]
{

/// Retourne une sous-classe %SQL.AbstractFind appropriée pour cet index fonctionnelClassMethod Find(pSearch As%Binary) As%Library.Binary [ CodeMode = generator, ServerOnly = 1, SqlProc ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) Set name = $Name(@tIdxGlobal@("id")) Set name = $Replace(name,$$$QUOTE("id"),"pSearch") $$$GENERATE(" Quit ##class(DC.Demo.ReferenceFind).%New($Name("name"))") } }

/// Retrouve un "true" s'il existe un enregistrement avec (prop1val, prop2val).ClassMethod Exists(prop1val, prop2val) [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set indexProp1 = $$$comSubMemberKeyGet(%class,$$$cCLASSindex,%property,$$$cINDEXproperty,1,$$$cINDEXPROPproperty) Set indexProp2 = $$$comSubMemberKeyGet(%class,$$$cCLASSindex,%property,$$$cINDEXproperty,2,$$$cINDEXPROPproperty) Set table = $$$comClassKeyGet(%class,$$$cCLASSsqlschemaname)"."$$$comClassKeyGet(%class,$$$cCLASSsqltablename) Set prop1 = $$$comMemberKeyGet(%class,$$$cCLASSproperty,indexProp1,$$$cPROPsqlfieldname) If (prop1 = "") { Set prop1 = indexProp1 } Set prop2 = $$$comMemberKeyGet(%class,$$$cCLASSproperty,indexProp2,$$$cPROPsqlfieldname) If (prop2 = "") { Set prop2 = indexProp2 } $$$GENERATE(" &sql(select top 1 1 from "table" where "prop1" = :prop1val and "prop2" = :prop2val)") $$$GENERATE(" Quit (SQLCODE = 0)") } }

/// Cette méthode est invoquée lorsqu'une nouvelle instance d'une classe est insérée dans la base de donnéesClassMethod InsertIndex(pID As%CacheString, pArg... As%Binary) [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) Set name = $Name(@tIdxGlobal@("id","chunk")) Set name = $Replace(name,$$$QUOTE("chunk"),"chunk")

    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">" If ($Get(pArg(1)) '= """") &amp;&amp; ($Get(pArg(2)) '= """") { "</span>)
    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">"  $$$IFBITOFFPOS(pArg(2),chunk,position)"</span>)
    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">"  Set $Bit("</span>_<span class="hljs-built_in">$Replace</span>(name,<span class="hljs-built_in">$$$QUOTE</span>(<span class="hljs-string">"id"</span>),<span class="hljs-string">"pArg(1)"</span>)_<span class="hljs-string">",position) = 1"</span>)
    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">" }"</span>)
}

}

/// Cette méthode est invoquée lorsqu'une instance existante d'une classe est mise à jour.ClassMethod UpdateIndex(pID As%CacheString, pArg... As%Binary) [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) Set name = $Name(@tIdxGlobal@("id","chunk")) Set name = $Replace(name,$$$QUOTE("chunk"),"chunk") $$$GENERATE(" If ($Get(pArg(3)) '= """") && ($Get(pArg(4)) '= """") { ") $$$GENERATE(" $$$IFBITOFFPOS(pArg(4),chunk,position)") $$$GENERATE(" Set $Bit("$Replace(name,$$$QUOTE("id"),"pArg(3)")",position) = .."%property"Exists(pArg(3),pArg(4))") $$$GENERATE(" }")

    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">" If ($Get(pArg(1)) '= """") &amp;&amp; ($Get(pArg(2)) '= """") { "</span>)
    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">"  $$$IFBITOFFPOS(pArg(2),chunk,position)"</span>)
    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">"  Set $Bit("</span>_<span class="hljs-built_in">$Replace</span>(name,<span class="hljs-built_in">$$$QUOTE</span>(<span class="hljs-string">"id"</span>),<span class="hljs-string">"pArg(1)"</span>)_<span class="hljs-string">",position) = 1"</span>)
    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">" }"</span>)
}

}

/// Cette méthode est invoquée lorsqu'une instance existante d'une classe est supprimée.ClassMethod DeleteIndex(pID As%CacheString, pArg... As%Binary) [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) Set name = $Name(@tIdxGlobal@("id","chunk")) Set name = $Replace(name,$$$QUOTE("chunk"),"chunk") $$$GENERATE(" If ($Get(pArg(1)) '= """") && ($Get(pArg(2)) '= """") { ") $$$GENERATE(" $$$IFBITOFFPOS(pArg(2),chunk,position)") $$$GENERATE(" Set $Bit("$Replace(name,$$$QUOTE("id"),"pArg(1)")",position) = .."%property"Exists(pArg(1),pArg(2))") $$$GENERATE(" }") } }

/// Méthode auxiliaire permettant d'obtenir la référence globale pour le stockage d'un index donné.ClassMethod IndexLocationForCompile(pClassName As%String, pIndexName As%String) As%String { Set tStorage = ##class(%Dictionary.ClassDefinition).%OpenId(pClassName).Storages.GetAt(1).IndexLocation Quit$Name(@tStorage@(pIndexName)) }

/// Purge l'indexClassMethod PurgeIndex() [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) $$$GENERATE(" Kill " _ tIdxGlobal) } }

/// Appelle SortBegin avant les opérations de masseClassMethod SortBeginIndex() [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) // No-op$$$GENERATE(" Quit") } }

/// Appelle SortEnd après les opérations de masseClassMethod SortEndIndex(pCommit As%Integer = 1) [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) // No-op$$$GENERATE(" Quit") } }

}

DC.Demo.ReferenceFind est notre implémentation générique de %SQL.AbstractFind qui permet de visualiser un tableau de fragments de cartes bitmap:

/// Utility class to wrap use of %SQL.AbstractFind against a bitmap index global referenceClass DC.Demo.ReferenceFind Extends%SQL.AbstractFind [ System = 3 ]
{

/// Référence globale à itérer sur / prendre en compte pour les méthodes d'opération %SQL.AbstractFind %FINDProperty reference As%String [ Private ]; Method %OnNew(pReference As%String) As%Status [ Private, ServerOnly = 1 ] { Set..reference = pReference Quit$$$OK }

Method NextChunk(ByRef pChunk As%Integer = "") As%Binary { Set pChunk=$Order(@i%reference@(pChunk),1,tChunkData) While pChunk'="",$bitcount(tChunkData)=0 { Set pChunk=$Order(@i%reference@(pChunk),1,tChunkData) } Return$Get(tChunkData) }

Method PreviousChunk(ByRef pChunk As%Integer = "") As%Binary { Set pChunk=$Order(@i%reference@(pChunk),-1,tChunkData) While pChunk'="",$bitcount(tChunkData)=0 { Set pChunk=$Order(@i%reference@(pChunk),-1,tChunkData) } Return$Get(tChunkData) }

Method GetChunk(pChunk As%Integer) As%Binary { If$Data(@i%reference@(pChunk),tChunkData) { Return tChunkData } Else { Return"" } }

}

Ainsi, DC.Demo.Comment ressemble maintenant à ceci, avec deux indices bitmap ajoutés (et les clés externes appropriées pour faire bonne mesure).:

Class DC.Demo.Comment Extends%Persistent
{

Property Post As DC.Demo.Post [ Required ];Property Usr As DC.Demo.Usr [ Required ];Property Comment As%String(MAXLEN = "") [ Required ]; Index Usr On Usr [ Type = bitmap ]; Index Post On Post [ Type = bitmap ]; Index UserPosts On (Usr, Post) As DC.Demo.ExistenceIndex; ForeignKey UsrKey(Usr) References DC.Demo.Usr(); ForeignKey PostKey(Post) References DC.Demo.Post() [ OnDelete = cascade ]; }

Notre requête SQL devient alors:

selectIDfrom DC_Demo.Post where (Author = ? orID %FIND DC_Demo.Comment_UserPostsFind(?)) and Draft = 0

Et le plan de requête devient:

 Générer un flux de valeurs idkey en utilisant la combinaison multi-index:
     ((bitmap index DC_Demo.Post.Draft) INTERSECT ((bitmap index DC_Demo.Post.Author) UNION (given bitmap filter for DC_Demo.Post.%ID)))
 Pour chaque valeur d'idkey:
     Afficher la ligne.

Combien de références globales y a-t-il maintenant ? Une pour Author bitmap, une pour Draft bitmap, et une pour le nœud d'index de bitmap "posts for a given user" dans DC.Demo.Comment. Désormais, la liste "Quels sont les publications auxquelles j'ai participé?" ne sera pas (autant) ralentie si vous commentez de plus en plus!

Avertissement : malheureusement, la communauté des développeurs n'est pas réellement soutenue par InterSystems IRIS, vous ne devriez donc probablement pas faire autant de commentaires.

0
0 37
Article Lorenzo Scalese · Juil 2, 2024 7m read

Enfin et avec un peu de retard, nous concluons cette série d'articles sur notre moteur de Workflow en montrant un exemple de connexion que nous pourrions établir à partir d'une application mobile.

Dans l'article précédent, nous avons présenté un exemple d'application permettant un contrôle détaillé d'une pathologie chronique telle que l'hypertension, tant pour le patient que pour son médecin associé. Dans cet exemple, le patient pourra accéder à une application web à partir de son téléphone portable (en fait, à une page web conçue pour s'adapter à cet appareil) dans laquelle il recevra des notifications basées sur les mesures que le tensiomètre portable envoie à l'instance IRIS.

Par conséquent, nous aurons deux accès différents à notre instance IRIS:

  • Accès utilisateur à partir d'une application mobile.
  • Accès à l'appareil pour soumettre les lectures de tension artérielle.

Dans cet article, nous verrons le premier d'entre eux qui permet aux patients de gérer les tâches que leurs lectures génèrent.

Application mobile de connexion - IRIS

Pour réaliser cette connexion, le plus simple est de configurer une application web dans IRIS et pour ce faire, nous y accéderons à partir du portail de gestion, System Administration -> Security -> Applications -> Web Applications (Administration du système > Sécurité > Applications > Applications web):

Ensuite, dans la liste affichée, nous cliquerons sur Create new application (Créer une nouvelle application), ce qui ouvrira un écran comme le suivant:

Sur cet écran, configurons les champs suivants:

  • Nom: dans ce champ, nous définirons l'URL à publier pour donner accès à notre fonctionnalité déployée dans IRIS.
  • Espace de Noms: l'espace de noms auquel nous voulons que l'application web soit associée, ce qui nous permettra plus tard de profiter des fonctionnalités des productions d'interopérabilité.
  • REST: Nous sélectionnerons cette option car ce que nous allons publier est une API REST pour autoriser les connexions HTTP.
  • Classe de répartition: Classe ObjectScript qui recevra l'appel HTTP et décidera quoi en faire.
  • Utilisation de l'authentification JWT: en cochant cette option, les points de terminaison /login et /logout seront activés sur l'URL que nous avons définie pour notre application, ce qui nous permettra d'obtenir un jeton Web JSON afin d'authentifier nos appels via IRIS.
  • Paramètres de sécurité -> Méthodes d'authentification autorisées: nous allons définir un mot de passe pour sécuriser nos appels.

Jetons un coup d'œil à notre classe Workflow.WS.Service:

Class Workflow.WS.Service Extends%CSP.REST
{

Parameter HandleCorsRequest = 0;Parameter CHARSET = "utf-8"; XData UrlMap [ XMLNamespace = "https://www.intersystems.com/urlmap" ] { <Routes> <Route Url="/getTasks" Method="GET" Call="GetTasks" /> <Route Url="/saveTask" Method="POST" Call="SaveTask" /> </Routes> }

ClassMethod OnHandleCorsRequest(url As%String) As%Status { set url = %request.GetCgiEnv("HTTP_REFERER") set origin = $p(url,"/",1,3) // origin = "http(s)://origin.com:port"// ici vous pouvez vérifier les origines spécifiques// sinon, toutes les origines seront autorisées (utile uniquement lors du développement)do%response.SetHeader("Access-Control-Allow-Credentials","true") do%response.SetHeader("Access-Control-Allow-Methods","GET,POST,PUT,DELETE,OPTIONS") do%response.SetHeader("Access-Control-Allow-Origin",origin) do%response.SetHeader("Access-Control-Allow-Headers","Access-Control-Allow-Origin, Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control") quit$$$OK }

ClassMethod GetTasks() As%Status { Try { Do##class(%REST.Impl).%SetContentType("application/json") If '##class(%REST.Impl).%CheckAccepts("application/json") Do##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) QuitDo##class(%REST.Impl).%SetStatusCode("200") set sql = "SELECT %Actions, %Message, %Priority, %Subject, TaskStatus_TimeCreated, ID FROM EnsLib_Workflow.TaskResponse WHERE TaskStatus_AssignedTo = ? AND TaskStatus_IsComplete = 0"set statement = ##class(%SQL.Statement).%New(), statement.%ObjectSelectMode = 1set status = statement.%Prepare(sql) if ($$$ISOK(status)) { set resultSet = statement.%Execute($USERNAME) if (resultSet.%SQLCODE = 0) { set tasks = [] while (resultSet.%Next() '= 0) { set task = {"actions": "", "message": "", "priority": "", "subject": "", "creation": "", "id": ""} set task.actions = resultSet.%GetData(1) set task.message = resultSet.%GetData(2) set task.priority = resultSet.%GetData(3) set task.subject = resultSet.%GetData(4) set task.creation = resultSet.%GetData(5) set task.id = resultSet.%GetData(6) do tasks.%Push(task) }
} } set result = {"username": ""} set result.username = $USERNAMEDo##class(%REST.Impl).%WriteResponse(tasks)

} <span class="hljs-keyword">Catch</span> (ex) {
    <span class="hljs-keyword">Do</span> <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%REST.Impl</span>).<span class="hljs-built_in">%SetStatusCode</span>(<span class="hljs-string">"400"</span>)
    <span class="hljs-keyword">return</span> ex.DisplayString()
}

<span class="hljs-keyword">Quit</span> <span class="hljs-built_in">$$$OK</span>

}

ClassMethod SaveTask() As%Status { Try { Do##class(%REST.Impl).%SetContentType("application/json") If '##class(%REST.Impl).%CheckAccepts("application/json") Do##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) Quit// Lecture du corps de l'appel http avec les données relatives à la personneset dynamicBody = {}.%FromJSON(%request.Content)

    <span class="hljs-keyword">set</span> task = <span class="hljs-keyword">##class</span>(EnsLib.Workflow.TaskResponse).<span class="hljs-built_in">%OpenId</span>(dynamicBody.<span class="hljs-built_in">%Get</span>(<span class="hljs-string">"id"</span>))
    <span class="hljs-keyword">set</span> sc = task.CompleteTask(dynamicBody.action)

    <span class="hljs-keyword">if</span> <span class="hljs-built_in">$$$ISOK</span>(sc) {	        
        <span class="hljs-keyword">Do</span> <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%REST.Impl</span>).<span class="hljs-built_in">%SetStatusCode</span>(<span class="hljs-string">"200"</span>)
        <span class="hljs-keyword">Do</span> <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%REST.Impl</span>).<span class="hljs-built_in">%WriteResponse</span>({<span class="hljs-string">"result"</span>: <span class="hljs-string">"success"</span>})         
	}	
    
} <span class="hljs-keyword">Catch</span> (ex) {
    <span class="hljs-keyword">Do</span> <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%REST.Impl</span>).<span class="hljs-built_in">%SetStatusCode</span>(<span class="hljs-string">"400"</span>)
    <span class="hljs-keyword">Do</span> <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%REST.Impl</span>).<span class="hljs-built_in">%WriteResponse</span>({<span class="hljs-string">"result"</span>: <span class="hljs-string">"error"</span>})
}

<span class="hljs-keyword">Quit</span> <span class="hljs-built_in">$$$OK</span>

}

}

Comme vous pouvez le voir, nous résoudrons toute la logique dont nous avons besoin à partir de notre WS, mais je ne recommande pas de procéder de cette manière, car nous perdons la traçabilité possible en envoyant les requêtes reçues à la production configurée dans l'espace de noms Namespace.

En jetant un coup d'œil à la section URLMap, nous verrons que nous avons 2 points de terminaison configurés dans notre WS:

  • getTasks: pour récupérer toutes les tâches en attente de l'utilisateur (si vous voyez le SQL utilisé, nous passons le nom d'utilisateur directement à partir de la variable générée lors de la connexion).
  • saveTask: pour recevoir la réponse de l'utilisateur à la tâche et la terminer en exécutant la méthode CompleteTaks de la classe EnsLib.Workflow.TaskResponse.

De la part d'IRIS, tout serait déjà configuré pour que l'application externe se connecte.

Test de l'application externe avec le moteur de Workflow

Tout d'abord nous allons simuler l'envoi d'un message de HL7 vers notre production en copiant le fichier /shared/hl7/message_1_1.hl7 vers le chemin /shared/in, examinons donc le message que nous envoyons:

MSH|^~\&|HIS|HULP|EMPI||20240402111716||ADT^A08|346831|P|2.5.1
EVN|A08|20240402111716
PID|||07751332X^^^MI^NI~900263^^^HULP^PI||LÓPEZ CABEZUELA^ÁLVARO^^^||19560121|F|||PASEO MARIO FERNÁNDEZ 2581 DERECHA^^MADRID^MADRID^28627^SPAIN||555819817^PRN^^ALVARO.LOPEZ@VODAFONE.COM|||||||||||||||||N|
PV1||N
OBX|1|NM|162986007^Pulso^SNM||72|^bpm|||||F|||20240402111716
OBX|2|NM|105723007^Temperatura^SNM||37|^Celsius|||||F|||20240402111716
OBX|3|NM|163030003^Presión sanguínea sistólica^SNM||142|^mmHg|||||F|||20240402111716
OBX|4|NM|163031004^Presión sanguínea diastólica^SNM||83|^mmHg|||||F|||20240402111716

Pour ceux d'entre vous qui ne se rappellent pas les articles précédents, nous avions défini dans notre BPL qu'une alerte serait générée lorsque la pression systolique dépassait 140 ou la pression diastolique dépassait 90, par conséquent, ce message devrait générer une tâche d'alerte pour notre patient avec DNI (Document National d'Identification) 07751332X et une autre tâche automatique qui restera ouverte jusqu'à ce qu'IRIS reçoive le nouveau message.

Vérifions notre application mobile:

Deux tâches sont générées : la première est définie comme manuelle et dépend de l'utilisateur ; la seconde est définie comme automatique et, pour qu'elle disparaisse, l'utilisateur doit effectuer une nouvelle lecture avec son tensiomètre . Si nous examinons l'appel HTTP, nous pouvons voir comment nous y avons inclus le JWT:

Si l'utilisateur clique sur le bouton Accepter de la tâche en attente, le flux restera en attente de la lecture du tensiomètre et ne passera pas aux étapes suivantes. Une fois que la tâche manuelle est acceptée et qu'une nouvelle lecture du tensiomètre est reçue, dans laquelle les limites marquées sont à nouveau dépassées, le système génère deux nouvelles tâches d'avertissement, l'une pour le patient et l'autre pour le médecin associé, afin de l'avertir d'une crise possible:

Parfait! Nous avons déjà mis en place notre système de notification des patients de manière simple et rapide.

Conclusion

Comme vous l'avez vu dans cette série d'articles, la fonctionnalité du moteur de Workflow ainsi que les capacités d'interopérabilité d'InterSystems IRIS offrent un potentiel incroyable pour la mise en œuvre de processus métier que peu d'autres solutions métier peuvent fournir. Il est vrai que certaines connaissances techniques peuvent être nécessaires pour en tirer le maximum, mais le jeu en vaut vraiment la chandelle.

0
0 60
Article Lorenzo Scalese · Juin 20, 2024 7m read

Dans notre article précédent, nous avons présenté les concepts généraux ainsi que le problème que nous voulions résoudre en utilisant le moteur de tâches intégré dans InterSystems IRIS. Dans l'article d'aujourd'hui, nous verrons comment configurer une production d'interopérabilité pour fournir une solution.

Configuration du moteur de workflow

Tout d'abord, nous allons définir les rôles des tâches à gérer. Dans notre exemple, nous allons définir deux types de tâches:

  • AutomaticBloodPressureRole: pour créer des tâches automatiques qui ne nécessitent aucune intervention de la part de l'utilisateur.
  • ManualBloodPressureRole:ManualBloodPressureRole: pour créer les tâches à valider manuellement par l'utilisateur.

Il ne faudra pas assigner des utilisateurs à ces rôles puisque nous le ferons plus tard, au fur et à mesure que nous recevrons des messages HL7 de différents patients.

Nous n'avons pas non plus besoin d'ajouter les utilisateurs d'IRIS au Workflow puisque nous le ferons par code à partir de la production.

Configuration de la production

Pour notre exemple, nous allons créer une production dans un NAMESPACE (espace de noms) créé ad-hoc et que nous avons appelé WORKFLOW (flux de travail). C'est dans cette production que nous inclurons les éléments métier dont nous avons besoin.

Commençons par expliquer les composants les plus simples:

Services métier

  • HL7_File_IN: responsable de la collecte des fichiers HL7 à partir d'un chemin d'accès spécifique au serveur qui simulera la réception de messages en provenance d'un dispositif médical.

Opérations métier

  • AutomaticBloodPressureRole: composant de la classe EnsLib.Workflow.Operation, avec le nom d'un des rôles définis que nous utiliserons pour générer les tâches qui n'impliqueront pas d'interaction directe avec l'utilisateur dans notre Workflow.
  • ManualBloodPressureRole: similaire à l'opération métier précédente, mais dans ce cas, les tâches que nous générons nécessiteront que l'utilisateur intervienne directement pour les achever.

Voyons maintenant en détail le processus opérationnel qui gérera le flux d'informations ainsi que la création et la gestion des tâches impliquées dans notre cycle.

Workflow.BP.BloodPressurePlan

Ce processus métier est de type BPL et sera responsable de la création des tâches liées aux soins du patient et de la saisie de la réponse, à la fois du patient et de l'appareil médical qui enverra les informations relatives aux niveaux de tension artérielle.

Début du processus:

Ici, le flux effectue les opérations suivantes:

  1. Contrôle de l'utilisateur: vérifie l'utilisateur reçu dans le message ADT^A08 et, s'il existe, poursuit le flux, sinon crée l'utilisateur dans IRIS et l'affecte aux rôles définis dans le flux de travail. Pour assigner des utilisateurs au rôle, elle lance la commande suivante :
    /// Création d'utilisateurs de Workflowset sc = ##class(EnsLib.Workflow.UserDefinition).CreateUser(username)
    /// Assignation du rôle à l'utilisateur du Workflowset sc = ##class(EnsLib.Workflow.RoleDefinition).AddUserToRole(role, username)
  2. Obtention d'une tâche automatique: Puisque dans ce flux nous allons gérer à la fois des tâches automatiques et manuelles, nous devons vérifier s'il y a des tâches automatiques en attente pour l'utilisateur concernant le patient reçu. Ce point est important car si des tâches automatiques sont en suspens, cela signifie que nous avons eu une lecture antérieure dont les valeurs dépassent les niveaux normaux. Nous effectuerons le contrôle directement dans la table TaskResponse où toutes les tâches créées sont sauvegardées, la requête sera la suivante:
    &sql(SELECTIDINTO :taskId FROM EnsLib_Workflow.TaskResponse WHERE TaskStatus_AssignedTo = :username AND TaskStatus_IsComplete = 0AND %RoleName = :role)
  3. Obtention du nombre d'OBX: nous récupérons le nombre de segments OBX à partir de notre message HL7.
  4. Contrôle des valeurs OBX: nous parcourons les segments OBX et n'extrayons que les données relatives à la tension artérielle.
  5. âches en attente?: Comme nous l'avons dit au point 2, nous vérifions si nous avons une tâche automatique en attente ou non.

Première lecture de la tension artérielle:

Dans cette partie du flux, nous traiterons les messages qui ne sont pas générés par une première lecture dépassant les niveaux maximums de tension artérielle définis.

  1. Contrôle de la tension: Nous vérifions si les niveaux de tension artérielle dépassent les limites préétablies ; si ce n'est pas le cas, le processus s'achève sans qu'il soit nécessaire de faire quoi que ce soit d'autre. Sinon, il sera nécessaire de créer les tâches nécessaires.
  2. Création d'une tâche manuelle: La tension artérielle a dépassé les niveaux de sécurité définis, il est donc nécessaire de créer une tâche manuelle qui informera le patient de la situation et lui indiquera qu'il doit effectuer une deuxième lecture. Voyons la configuration de cette activité.     
    1. Nous allons invoquer l'opération métier ManualBloodPressureRole qui sera responsable de la génération de la tâche en y passant un message de type EnsLib.Workflow.TaskRequest.
    2. Nous définirons les actions que le patient peut effectuer en les séparant par des virgules dans callrequest.%Actions, dans ce cas nous n'avons défini que l'action "Accepter".
    3. Nous configurons le message qui sera affiché au patient dans callrequest.%Message.
    4. Nous attribuons la tâche créée au patient en définissant son nom d'utilisateur dans callrequest.%UserName.
    5. Enfin, nous indiquerons un sujet ou un titre dans callequest.%Subject et une priorité (entre 1 et 3) dans callrequest.%Priority.
  3. Création d'une tâche automatique: comme pour la configuration de l'activité précédente, sauf que cette fois-ci, nous l'enverrons à l'opération métier AutomaticBloodPressureRole. Cette tâche sera celle qui restera en attente de la deuxième mesure de la tension artérielle du patient et qui fera en sorte que tout nouveau message reçu par la production pour ce patient ne génère pas de nouvelles tâches en obtenant un faux dans l'activité Pending task? ("Tâche en attente").
  4. Attente des tâches: Cette activité met le flux de tâches en attente jusqu'à ce que la tâche et le manuel soient achevés, c'est-à-dire jusqu'à ce que le patient supprime l'alarme créée par ManualBloodPressureRole et que la production IRIS reçoive un deuxième message du dispositif médical.
  5. Contrôle en cas d'avertissement: Nous aborderons cette activité une fois que les tâches précédentes en attente auront été achevées. À ce stade, nous vérifions si l'achèvement de la tâche automatique a reçu une nouvelle lecture avec des niveaux qui dépassent les valeurs maximales de tension artérielle configurées (la tâche sera achevée avec une action d'avertissement) ou non. Si la mesure est inférieure aux valeurs maximales, le processus s'achève. Si les mesures dépassent les valeurs maximales, vous passerez à l'activité suivante.
  6. Tâche d'avertissement pour le médecin: Une tâche manuelle est créée pour informer le médecin assigné au patient que ce dernier est peut-être en train de traverser une crise.
  7. Tâche d'avertissement pour le patient: nous informons le patient, au moyen d'une nouvelle tâche manuelle, que son médecin est au courant de la situation.

Deuxième lecture de la tension artérielle

Cette partie du flux ne sera accessible que lorsqu'une tâche automatique précédemment créée n'a pas été achevée.

  1. Contrôle de la tension: nous validons à nouveau le message reçu pour savoir si les valeurs de la tension artérielle dépassent la limite ou non.
  2. Clôturer la tâche normale: Si les niveaux ne dépassent pas les valeurs maximales, nous allons achever la tâche en attente avec la méthode CompleteTask fournie par la classe EnsLib.Workflow.TaskResponse que notre tâche instancie. Le paramètre utilisé indiquera l'action entreprise, dans ce cas nous indiquerons qu'il s'agit d'une fausse alarme en passant la valeur FalseAlarm, nous pourrions définir n'importe quel autre valeur à notre guise.
  3. Achevement de la tâche d'avertissement: Comme dans le cas précédent, sauf que dans ce cas l'action que nous transmettons au moteur de Workflow sera Avertissement et qu'il s'agira de la valeur que l'activité " Contrôle en cas d'avertissement " que nous avons vue précédemment vérifiera.

Comme vous pouvez le voir dans le diagramme suivant (il n'est pas littéral mais vous pouvez vous faire une idée de la manière dont il fonctionnera), notre flux de tâches fonctionnera avec deux "fils" ou "processus":

Le premier fil/processus restera ouvert au cas où il serait nécessaire d'attendre une seconde lecture et c'est le second fil/processus qui provoquera la réactivation du premier fil ouvert après réception de ladite seconde lecture, concluant ainsi le flux de tâches défini.

Conclusion

Comme vous pouvez le constater, la configuration d'un flux de tâches dans une BPL est assez simple et vous pouvez simuler des tâches automatiques et manuelles en toute facilité. Dans le prochain et dernier article, nous verrons comment les utilisateurs peuvent interagir avec leurs tâches à partir d'une application externe.

Merci à tous pour votre attention!

0
0 48
Article Lorenzo Scalese · Juin 18, 2024 6m read

Cela fait un certain temps que j'ai l'intention de faire une sorte de démonstration de concept avec la fonctionnalité Workflow (flux de travail), qui, comme beaucoup d'autres fonctionnalités disponibles dans IRIS, tend à passer inaperçue aux yeux de nos clients (et je fais ici mon mea culpa). C'est pourquoi j'ai décidé il y a quelques jours de développer un exemple de configuration et d'exploitation de cette fonctionnalité en la connectant à une interface utilisateur développée en Angular.

Pour ne pas faire un article trop long et le rendre plus accessible, je vais le diviser en 3 parties. Dans ce premier article, je présenterai la fonctionnalité de Workflow ainsi que l'exemple que nous allons résoudre. Le deuxième article détaillera la configuration et l'implémentation de la production qui sera responsable de la gestion du Workflow. Enfin, nous montrerons comment accéder aux informations disponibles dans notre Workflow via une application Web.

Moteur de Workflow dans InterSystems IRIS

Pour expliquer ce qu'est cette fonctionnalité de Workflow, rien de mieux que de copier la description qui en est faite dans la documentation d'IRIS.

Un système de gestion des flux de travail automatise la répartition des tâches entre les utilisateurs. L'automatisation de la distribution des tâches selon une stratégie prédéfinie rend l'attribution des tâches plus efficace et l'exécution des tâches plus responsable. Un exemple typique est celui d'une application de service d'assistance qui reçoit des rapports de problèmes de la part des clients, envoie ces rapports aux membres des organisations appropriées pour qu'ils prennent des mesures et, une fois le problème résolu, communique les résultats au client.

Le moteur de workflow dans InterSystems IRIS offre un niveau de fonctionnalité bien plus élevé que les systèmes de gestion de workflow traditionnels et autonomes.

Où trouvons - nous les fonctionnalités de Workflow dans notre IRIS? Très simple, depuis le Portail de Gestion:

Expliquons brièvement chacune des options disponibles avant d'entrer dans l'exemple de projet.

Les rôles de Workflow

Cette option fait référence aux rôles dans lesquels nous classerons les utilisateurs qui utiliseront la fonctionnalité. La définition du rôle est quelque peu confuse, je préfère la voir plutôt comme des types de tâches que nous pouvons créer à partir de notre production. Le nom attribué à ces rôles doit correspondre aux noms des Opérations Métier que nous déclarerons plus tard dans notre production pour créer les tâches associées à ces rôles.

Utilisateurs de Workflow

Les utilisateurs IRIS qui peuvent être assignés à différentes tâches seront associés à un ou plusieurs rôles. Les utilisateurs doivent être enregistrés auprès d'IRIS.

Les tâches de Workflow

Liste des tâches créées dans le système avec leurs informations associées. À partir de cette fenêtre, des tâches peuvent être attribuées aux différents utilisateurs correspondant au rôle de tâche.

Quelles sont ces tâches? C'est très simple, les tâches sont des instances de la classe EnsLib.Workflow.TaskRequest, l'objet de ce type sera envoyé depuis le Processus Métier dans lequel il a été généré vers une Opération Métier de la classe EnsLib.Workflow.Operation et avec le nom du rôle que nous avons précédemment créé. Cette action créera à son tour une instance de la classe EnsLib.Workflow.TaskResponse. L'Opération Métier ne renverra pas de réponse tant que la méthode CompleteTask de l'instance de classe TaskResponse n'aura pas été exécutée.

Liste de travail de Workflow

Semblable à la fonctionnalité précédente, il nous montre également une liste de tâches avec les informations qui leur sont associées.

Exemple de Workflow

Le projet que vous trouverez associé à cet article présente un exemple simplifié de solution à un problème typique des organisations de santé tel que le traitement des patients chroniques. Ces types de patients nécessitent un contrôle continu et une surveillance stricte pour lesquels, dans de nombreux cas, les professionnels impliqués dans les soins ne disposent pas des moyens les plus appropriés.

Dans l'exemple, nous allons voir comment nous pouvons surveiller les patients souffrant d'hypertension. Nous supposerons que les patients prennent leur tension artérielle quotidiennement avec un tensiomètre qui enverra la lecture au serveur où notre InterSystems IRIS est installé. Si la tension artérielle dépasse 140 pour la systolique et 90 pour la diastolique, le patient est informé qu'il faudra reprendre la tension artérielle après un certain temps et, si la tension dépasse à nouveau les limites, le patient et le médecin sont informés de la situation afin qu'ils puissent décider des mesures à prendre.

Pour ce faire, nous allons diviser ce flux de tâches en deux étapes:

Étape 1: Prise quotidienne de la tension artérielle.

  1. Réception dans la production de messages HL7 avec des mesures de la pression artérielle.
  2. Vérifier l'existence de l'utilisateur dans le système (et le créer s'il n'existe pas), puis enregistrer l'utilisateur dans les rôles impliqués dans le workflow.
  3. Extraction des données de tension artérielle et comparaison avec le critère d'alerte.
  4. Si les données dépassent les critères d'alerte:
    1. Création d'une tâche pour informer le patient de la situation et lui demander une nouvelle mesure de la tension artérielle.
    2. Création d'une tâche automatique pour gérer la deuxième lecture de la tension artérielle.
  5. Si les données ne dépassent pas les critères d'alerte, le processus est clôturé jusqu'à la réception de la prochaine lecture le lendemain.

Étape 2: Réception de la deuxième lecture après que la première lecture dépasse les valeurs d'alerte.

  1. Réception du message HL7 avec la deuxième lecture de données.
  2. En attente d'une récupération automatique des tâches.
  3. Si les niveaux de tension artérielle dépassent les critères d'alerte:
    1. La tâche automatique est clôturée en indiquant l'état d'alerte.
    2. Une tâche manuelle est créée pour le médecin qui signale la situation du patient.
    3. Une tâche manuelle est créée pour le patient, l'avertissant de la situation et du fait que son médecin a été informé.
  4. Si les niveaux ne dépassent pas les critères d'alerte:
    1. La tâche automatique est clôturée indiquant qu'il n'y a pas de danger.

Dans le prochain article, nous entrerons dans les détails de la conception de notre organigramme afin de refléter ce qui a été dit plus haut.

Interface utilisateur

Comme vous l'avez vu dans les captures d'écran ci-jointes, l'interface utilisateur fournie par InterSystems IRIS pour la gestion des tâches est, pour le moins, assez spartiate, mais l'un des avantages que nous avons avec IRIS est que nous pouvons créer notre propre API REST pour gérer notre flux de tâches d'une manière très simple, nous discuterons de ce point dans notre dernier article.

Pour offrir une interface conviviale aux utilisateurs, nous avons développé une petite application web en Angular en utilisant Angular Material qui nous permettra de créer une interface qui simule une application mobile.

Cette application mobile se connectera à InterSystems IRIS à l'aide de JSON Web Tokens (jetons Web JSON) et effectuera des requêtes HTTP vers une application web spécifique publiée dans IRIS, de manière à ce que les actions entreprises par les différents acteurs sur les tâches soient reflétées dans le flux de tâches défini.

Dans le chapitre suivant...

Après cette introduction à la fonctionnalité Workflow, dans notre prochain article nous montrerons comment implémenter le flux de tâches nécessaire pour traiter l'exemple que nous avons présenté en utilisant une BPL, nous configurerons l'ensemble de la production et nous verrons quelles sont les principales classes impliquées dans le Workflow (EnsLib.Workflow.TaskRequest et EnsLib.Workflow.TaskResponse).

Merci à tous pour votre attention!

0
0 61