Projet "Nono le petit robot"
Documentation technique
Cahier des charges
Présentation du contexte
Logeas Informatique est une société coopérative et participative (SCOP) spécialisée sur le marché du conseil en systèmes et des logiciels informatiques, à destination des associations et des petites et moyennes entreprises.
Logeas Informatique commercialise une solution de gestion d’organisation. Le logiciel LoGeAs, facile d’utilisation, permet la prise en compte des exigences légales et réglementaires (comptabilité, gestion des fichiers, reçus fiscaux, etc.).
LoGeAs est un logiciel sur mesure développé à partir de la plateforme RAD Delphi.
LoGeAs est en cours de réécriture afin de passer en FullWeb. Pour cette nouvelle version du logiciel, nous souhaitons ajouter une petit API liée à notre base de connaissance afin que nos utilisateurs puissent avoir des réponses sans passer par l'assistance (mail/téléphone).
Objectif
Avoir une API intégrer à la future version de LoGeAs.
Avoir deux interface : Utilisateurs et Assistance.
Avoir une base de connaissance référençant les demandes (et réponses) récurrentes reçues à l'assistance.
Donner un accès plus facile à la documentation au travers de :
- Note d’aide à l’imputation
- Notes succinctes
- Renvoi vers la documentation
(Dans un deuxième temps permettre de contacter l’assistance si la base de connaissance ne propose pas une réponse satisfaisante)
Interface utilisateurs
L'utilisateur pourra poser des questions (ex : “Comment éditer un reçu fiscal ?”) et obtenir un choix de réponses liées à sa question. Cela lui fera gagner du temps et le nombre de demandes récurrentes envoyées à l'assistance sera diminué.
Interface assistance
L'équipe chargée de l'assistance pourra ajouter des réponses à notre base de connaissance via cette API. Elle pourra également modifier les réponses existantes en cas de besoin. Y ajouter des mot clés etc…
Périmètre
Cette API sera disponible à tous les utilisateurs de notre application initiale.
Fonctionnement
Fonctions accessibles sans identifications
- Suite à la saisie d’une suite de mot :
- Proposer une suite de réponses hiérarchisées (imputation, notes, renvois)
- Classées par ordre de pertinence (dans une premier temps on prendra comme ordre le nombre de mot clef qui « match » avec la demande)
- Les réponses seront présentées sous forme d’une suite de titre cliquable (sauf s’il n’y a pas de texte et de lien associé)
- Si la demande ne donne pas de résultat et/ou si l’utilisateur dit que la réponse ne lui suffit pas, un mail sera envoyé avec les informations disponibles à l’assistance pour évolution de la base
- Les mots clefs tapés mais non présent dans la base y seront ajoutés
- A chaque demande un compteur sur les mots clefs identifiés sera incrémenté
- A chaque ouverture d’une réponse, un compteur sur le doc sera incrémenté
- Aucune gestion de droit n’est demandé
Fonctions accessibles avec identifications
- Dans la première version il ne sera pas fait de gestion d’identification, la seul saisie d’un mot de passe « en dur » permettra d’accéder à la partie « édition »
- Il sera alors possible :
- Créer/Rechercher un document
- Le modifier
- Le lier/délier avec des mots clefs
- Le lier avec un lien
Ressources humaines
La création de cette API est réalisée par :
Collaborateurs | Fonction | Niveau en Dev |
---|---|---|
Nicolas MARCHAND | Chef de projet / co-gérant de Logeas Informatique | +++++++ |
Alexia LABARTHE | Apprentie Dev, anciennement en CDI à l'assistance du logiciel LoGeAs | Débutante en apprentissage |
Mathys PAQUEREAU | Apprenti Dev | Débutant en apprentissage |
Cahier fonctionnel
Fonctions et procédures que l'on a développé
Type | Fonctionnalité | Utilité | Accessible par | Niveau |
---|---|---|---|---|
Fonction | MotsToListeID | Converti une question (liste de mot) en la liste de leurs Ids Rend la liste des ID des mots de la question | X | Back |
Fonction | GetMotClefReponse | Récupère la liste des MotClef liés à l'ID de la Réponse sélectionnée Rend la liste des ID des MotClefs | X | Back |
Fonction | GetJsonFromSQL | Rend le résultat de la requête SQL au format JSON | X | Back |
Fonction | NotifyBeforeURI | X | Back | |
Fonction | AddOrUpdate | X | Back | |
Fonction | Ajoute | X | Back | |
Fonction | AjouteMotVide | X | Back | |
Fonction | GetNonoModel | X | Back | |
Fonction | MUppercase | X | Back | |
Fonction | AjouteDoc | X | Back | |
Constructor | Create | X | ||
Destructor | Destroy | X | ||
Procédure | Logeas | X | ||
Procédure | Recherche | Récupère la Question posée par l'utilisateur Rend la liste des réponses liées aux MotsClefs de la Question posée par l'utilisateur | Tout le monde | Back/Front |
Procédure | EnvoiEmail | Tout le monde | Back | |
Procédure | IncrementReponse | Ajoute +1 au champ NbUse dans la table MotClef | X | Back |
Procédure | GetRep | X | Back/Front | |
Procédure | SaveRep | Récupère les éléments modifiés d'une réponse et/ou d'un/des éléments Rend un JSON comprenant les éléments modifiés | Assistance | Back/Front |
Procédure | SaveRepBig | Identique à SaveRep, mais prends en compte le volume des éléments Récupère les éléments modifiés d'une réponse et/ou d'un/des éléments Rend un JSON comprenant les éléments modifiés | Assistance | Back/Front |
Procédure | SetMotClef | Converti une saisie (liste de mot) en une liste de leurs Ids Rend la liste des ID des MotClefs | Assistance | Back/Front |
Procédure | GetMotClef | Récupère la liste des MotClef liés à l'ID de la Réponse sélectionnée Rend la liste des ID des MotClefs | Tout le monde | Back/Front |
Cahier technique
Aspects techniques
Back
Le back sera réalisé en Free Pascal Lazarus, complété de l’ORM mORMot déjà utilisé par LoGeAs Web
Lazarus est un éditeur de code multiplateforme compatible Delphi pour le développement rapide d'applications.
Lazarus utilise Free Pascal comme langage qui est un dialecte Object Pascal. Il est constamment développé pour intégrer de nouvelles fonctionnalités que l'on peut attendre des langages de programmation modernes.
L’ORM mORMot est un Open SourceServeur clientORMSOAFramework MVC pour Delphi 6 jusqu'à la dernière version Delphi disponible. Les principales caractéristiques de mORMot sont donc :
- ORM/ODM : persistance des objets sur presque toutes les bases de données (SQL ou NoSQL) ;
- SOA : organisez votre logique métier enservices REST ;
- Clients : consommez vos données ou services depuis n'importe quelle plateforme, via des classes ORM ou des interfaces SOA ;
- Web MVC : publiez votre processus ORM/SOA en responsiveDes applications Web.
Avec un accès local ou distant, via un Client-Serveur auto-configurableConception REPOS.
Front
Le front sera réalisé en Angular (version 15) avec la bibliothèque DevExtreme
Angular est un framework open source JavaScript développé par Google. Il est utilisé pour développer des applications web et mobile. Avec cette technologie, on réalise des interfaces de type monopage ou “one page” qui fonctionnent sans rechargement de la page web.
DevExtreme est une bibliothèque de plus de 70 composants réactifs et tactiles pour les applications Angular.
La suite DevExtreme comprend une grille de données, des graphiques interactifs, des éditeurs de données, des composants de navigation et d'interface utilisateur polyvalents.
BDD
La base de donnée sera sous SQLite
SQLite est un système de gestion de base de données relationnelle, il regroupe en un seul fichier toutes les tables stockées dans la base de données.
Documentation
Les documents seront au format HTML stocké dans la base de donnée
La documentation se fait par Compodoc et sur notre WiKi Logeas
Compodoc est un outil open source de documentation pour les applications Angular. Il génère une documentation statique de notre application.
Mise en place / Encodage
Schématisation BDD (Base De Données)
MCD
Schématisation de la base de donnée avec le logiciel looping en libre accès et totalement gratuit : Looping
MLD textuelle
Notre base SQLite a été créée en utilisant l'ORM mORMot. Par simplification je n'ai pas fait apparaitre le champ ID de chaque table dans le MLD, celui-ci étant inclus par la classe de base de toute les tables générées par cet ORM.
{ TSQLReponse } TTypeReponse = (ttr_Assistance,ttr_Tous,ttr_Compte,ttr_ModelEcriture); TSQLReponse = class(TSQLRecord) private fIDTypeBase: TID; fNbUse: Integer; fText: RawUTF8; fTitre: RawUTF8; fTypeReponse: TTypeReponse; published property Titre: RawUTF8 read fTitre write fTitre; property Text: RawUTF8 read fText write fText; property IDTypeBase:TID read fIDTypeBase write fIDTypeBase; property NbUse: Integer read fNbUse write fNbUse; property TypeReponse: TTypeReponse read fTypeReponse write fTypeReponse; public class function AddOrUpdate(Rest:TSQLRest; var Json:RawUTF8):Integer; {$IFDEF LOGEAS_WEB}class function AjouteDoc(Rest:TSQLRest; Titre,Text,Createur:RawUTF8; IdTypeBase:TID):TID;{$ENDIF} end;
{ TSQLMotClef } TSQLMotClef = class(TSQLRecord) private fIDGroupe: TID; fMot: RawUTF8; fNbUse: Integer; fSansSens: Boolean; fSoundex: RawUTF8; published property IDGroupe:TID read fIDGroupe write fIDGroupe; property Mot: RawUTF8 read fMot write fMot; property Soundex: RawUTF8 read fSoundex write fSoundex; property NbUse: Integer read fNbUse write fNbUse; property SansSens:Boolean read fSansSens write fSansSens; public class function Ajoute(Rest:TSQLRest; aMot:String; IsMotVide:Boolean=False):TID; {$IFNDEF FPC}class procedure AjouteMotVide(Rest:TSQLRest); {$ENDIF} class function MotsToListeID(Rest:TSQLRest; Chaine:String; AjouteIDSiInconnu:Boolean=False; AjouteSurServeur:Boolean=True): String; class function GetMotClefReponse(Rest: TSQLRest; IDRep:TID):String; end;
{ TSQLLien } TSQLLien = class(TSQLRecord) private fIDMotClef: TSQLMotClef; fIDReponse: TSQLReponse; published property IDReponse:TSQLReponse read fIDReponse write fIDReponse; property IDMotClef: TSQLMotClef read fIDMotClef write fIDMotClef; public class function Ajoute(Rest:TSQLRest; IDRep,IDMot:TID):TID; end;
Exemple de procédure
Regardons plus en détail la procédure Recherche
Dans le Back
Déclaration de notre procédure avec un exemple
L’exemple nous indique quel URL on va devoir envoyer au serveur pour utiliser cette fonctionnalité.
//procedure de recherche Question -> Reponses procedure Recherche(Ctxt: TSQLRestServerURIContext); //http://localhost:8087/root/Recherche?IdTypeBase=0&Question=Comment%20imputer%20les%20salaires%3F&TypeReponse=1
Mise en place de la procédure Recherche
- Récupère la Question posée par l'utilisateur
- Si la Question fait moins de 2 caractères → elle n'est pas prise en concidération
- Si la Question est =* → on rend toutes les réponses de la base de données
- Si la Question est =531 → on rend la réponse correspondant à l'ID 531
- Si la Question contient un IdTypeBase → on filtre les Reponses par l'IdTypeBase donné
- @param (String) URL contenant une requête SQL
- @returns Rend la liste des réponses liées aux MotsClefs de la Question posée par l'utilisateur
procedure TNonoServer.Recherche(Ctxt: TSQLRestServerURIContext); Var Question:RawUTF8; IdTypeBase:Integer; TypeReponse:TTypeReponse; ListeIDMot,ListeIDSoundex:String; Res:String; SQL,SQLWhere:RawUTF8; begin Question:=Ctxt.InputUTF8['Question']; IdTypeBase:=Ctxt.InputInt['IdTypeBase']; TypeReponse:=TTypeReponse(Ctxt.InputInt['TypeReponse']); if length(question)<2 then begin Ctxt.Returns('{}'); exit; end; if Question[1]='=' then begin Question := Trim(Copy(Question,2,MaxInt)); if Question[1]='*' then begin Res:=GetJsonFromSQL('SELECT * FROM Reponse'); end else Res:=GetJsonFromSQL(formatUTF8('SELECT * FROM Reponse Where ID=?',[],[Question])); end else begin ListeIDMot:=TSQLMotClef.MotsToListeID(self,Question,False,False); ListeIDSoundex:=MotsToListeIDSoundex(Question); SQL:=FormatUTF8( 'SELECT Max(Pertinence) as Pertinence, ID, Titre, IDTypeBase, TypeReponse FROM ( '+ ' SELECT Count(*) as Pertinence, L.IdReponse as ID, R.Titre, R.IdTypeBase, R.TypeReponse '+ ' FROM Lien L, Reponse R '+ ' WHERE (L.IDMotClef in %) and (R.ID=L.IdReponse) '+ ' GROUP BY IdReponse '+ 'UNION '+ ' SELECT Count(*)*.5 as Pertinence, L.IdReponse as ID, R.Titre, R.IdTypeBase, R.TypeReponse '+ ' FROM Lien L, Reponse R '+ ' WHERE (L.IDMotClef in %) and (R.ID=L.IdReponse) '+ ' GROUP BY IdReponse '+ ')',[ListeIDMot,ListeIDSoundex],[]); SQLWhere:=''; if IdTypeBase>0 then SQLWhere:=SQLWhere+FormatUTF8('((IdTypeBase=?) or (IdTypeBase=?) or (IdTypeBase is null)) ',[],[0,IdTypeBase]) else SQLWhere:='(1=1)'; case TypeReponse of ttr_Assistance:; ttr_Tous: SQLWhere:=FormatUTF8('% and (TypeReponse<>?)',[SQLWhere],[Ord(ttr_Assistance)]); ttr_Compte: SQLWhere:=FormatUTF8('% and (TypeReponse=?)',[SQLWhere],[Ord(ttr_Compte)]); ttr_ModelEcriture: SQLWhere:=FormatUTF8('% and (TypeReponse=?)',[SQLWhere],[Ord(ttr_ModelEcriture)]); end; IF SQLWhere<>'' then SQL:=SQL+' WHERE '+SQLWhere; SQL:=SQL+' GROUP BY ID ORDER BY Pertinence DESC'; Res:=GetJsonFromSQL(SQL); end; Ctxt.Returns(Res); end;
Utilise les fonctions suivantes :
GetJsonFromSQL
- Rend le résultat de la requête SQL au format JSON
- @param (RawUTF8) Requête SQL
- @returns Rend le résultat de la requête SQL au format JSON
function TNonoServer.GetJsonFromSQL(SQL:RawUTF8): RawUTF8; var Table:TSQLTableJSON; Doc:Variant; begin Table:=Self.ExecuteList([],SQL); Try Result:=''; If Table.RowCount=0 then Result:='{}' else begin Table.ToDocVariant(doc,True); result:=VariantToUTF8(Doc); end; finally { Quoi qu'il arrive entre le Try et le finally, je libère la mémoire et je passe entre le finally et le end } Table.free; end; end;
MotsToListeID
- Converti une question (liste de mot) en la liste de leurs IDs
- Si le paramétre “AjouteSurServeur” est vrai et que l'un des mots de la question n'existe pas dans la base il est ajouté,
- si en plus le paramètre “AjouteIDSiInconnu” est vrai l'ID du mot créé est ajouté à la liste de sortie
- NB: Les IDs rendu sont ceux du groupe de mot (IDGRoupe)
- @param (String) Question posé par l'utilisateur
- @param (boolean) AjouteIDSiInconnu si vrai ET AjouteSurServeur est vrai ajoute l'ID du mot inconnu à la sortie
- @param (boolean) AjouteSurServeur si vrai ajoute les mots inconnus dans la base
- @returns Rend la liste des ID des mots de la question
class function TSQLMotClef.MotsToListeID(Rest: TSQLRest; Chaine: String; AjouteIDSiInconnu: Boolean; AjouteSurServeur: Boolean): String; var Mots:TStringArray; aMot:TSQLMotClef; aID:TID; MotSt:String; i:Integer; begin { La Question est découpée dans un tableau de liste de mots en tenant compte des règles de séparations } Chaine:=MUppercase(Chaine); Mots:=Chaine.Split([' ',';',',','.','!','?','(',')','[',']','{','}']); Result:=''; { On boucle sur chaque mot non vide découpé à l'étape précédente pour le traiter } for i:=0 to High(Mots) do if Mots[i]<>'' then begin MotSt:=Mots[i]; aMot:=TSQLMotClef.create(Rest,FormatUTF8('Mot like ?',[],[MotSt])); Try If aMot.ID=0 then begin If AjouteSurServeur then begin aID:=TSQLMotClef.Ajoute(Rest,MotSt); if AjouteIDSiInconnu then Result:=Result+IntToStr(aID)+','; end; end else begin If not aMot.SansSens then Result:=Result+IntToStr(aMot.ID)+','; end; finally { Quoi qu'il arrive entre le Try et le finally, on libère la mémoire et on passe entre le finally et le end } aMot.free; end; end; Result:=' ('+Copy(Result,1,Length(Result)-1)+')'; writeln('MotsToListeID '+Chaine+' -> '+Result); Setlength(Mots,0); end;
Dans le Front
- Création du projet sous Angular
- Ajout de la bibliothèque Dev-Extreme
Création du fichier nono.service.ts
→ On déclare la propriété urlBase qui est l’URL d’accès à notre serveur
//URL en exploitataion //public urlBase:string ='https://bases.logeas.fr/root/'; //URL locale public urlBase:string ='http://localhost:8087/root/';
→ On déclare la méthode Recherche
- Appelle le service REST éponyme du back
- @example Recherche('Comment saisir la rémunération d'un salarié ?')
- @param {string} Question Question posé par l'utilisateur
- @returns un tableau contenant la liste des réponses à proposé à l'utilisateur sous la forme TSQLReponses avec un paramétre supplémentaire “pertinence”
public Recherche(IdTypeBase:number, Question:string): Observable<any> { let URL = this.urlBase + 'Recherche?IdTypeBase='+IdTypeBase+'&Question='+encodeURIComponent(Question); console.log('URL : ',URL); return this.http.get(URL) .pipe( retry(2), catchError(err => this.getError(err)) ); }
→ On gère les erreurs potentielles avec la méthode getError
getError(error: any) { let message = ''; if (error.error instanceof ErrorEvent) { // gère les erreurs côté client message = `Error: ${error.error.message}`; } else { // gère les erreurs côté serveur message = `Error Code: ${error.status}\nMessage: ${error.message}`; } console.log('getError ', message); return throwError(error); }
→ Implémentation et utilisation de la méthode recherche du back
Dans le fichier app.component.ts
→ Import des modules nécessaires
import { Component } from '@angular/core'; import { NonoService, TSQLReponse, TSQLReponses } from 'src/nono.service'; import notify from 'devextreme/ui/notify'; import { OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router';
→ Composant de la librairie DevExtreme du côté HTML
<dx-text-box [(value)]="Question" placeholder="Saisissez votre demande" valueChangeEvent="keyup" (onValueChanged)="onCheckboxValueChanged($event)" [maxLength]="40"> </dx-text-box>
→ Côté TS du composant text-box et de son évènement onValueChanged
// Récupère la valeur de Question pour retourner une Réponse onCheckboxValueChanged(e:any) { this.nono.Recherche(this.IdTypeBase, this.Question).subscribe({ next: (res: any) => {console.log('ICI ',res); this.Reponses=res}, error: (error) => {console.log('ERR ICI ',error);},}); }
→ Affichage des réponses récoltés
→Composant grille de DevExtreme pour les réponses
<!-- ZONE DE LA GRILLE OU SONT LES REPONSES --> <dx-data-grid id="gridContainer" [dataSource]="Reponses" keyExpr="ID" [showBorders]="true" [focusedRowEnabled]="true" [(autoNavigateToFocusedRow)]="autoNavigateToFocusedRow" (onFocusedRowChanged)="onFocusedRowChanged($event)"> <dxi-column dataField="Titre"></dxi-column> </dx-data-grid> <!-- FIN ZONE DE LA GRILLE OU SONT LES REPONSES -->
→ Côté TS du composant data-grid et de son évènement onFocusedRowChanged
// Récupère le texte de la réponse sélectionnée onFocusedRowChanged(e:any) { this.ligneCourante = e.rowIndex; this.ReponseCourante = this.Reponses[this.ligneCourante]; console.log("Avant changement",e.rowIndex) this.nono.GetMotClef(this.ReponseCourante.ID).subscribe({ next: (res: any) => {console.log('ICI ',res); this.motsclefs = res.Liste}, error: (error) => {console.log('ERR ICI ',error);}, }) console.log("Mots Clefs :", this.motsclefs) }
→ Variable et classe
→ Déclaration des variables dans le fichier TS
title = 'nono-angular'; Question = ''; Reponses! : TSQLReponses; ReponseCourante!:TSQLReponse; motsclefs = ''; autoNavigateToFocusedRow = true; ligneCourante: any; isAssistance : boolean = false IdTypeBase : number = 0;
→ Déclaration des classes TSQLReponses et TSQLReponse dans le fichier TS
// Déclaration de la classe TSQLReponses -> Tableau de TSQLReponse export type TSQLReponses = [TSQLReponse]; // Déclaration de la classe TSQLReponse export class TSQLReponse { ID:number = 0; Titre: string = ''; Text: string = ''; IdTypeBase: number = 0; NbUse: number = 0; Createur: string = ''; };