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 :

(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
Fonctions accessibles avec identifications

Ressources humaines

La création de cette API est réalisée par :

CollaborateursFonctionNiveau en Dev
Nicolas MARCHANDChef de projet / co-gérant de Logeas Informatique+++++++
Alexia LABARTHEApprentie Dev, anciennement en CDI à l'assistance du logiciel LoGeAsDébutante en apprentissage
Mathys PAQUEREAUApprenti DevDébutant en apprentissage

Cahier fonctionnel

Fonctions et procédures que l'on a développé

TypeFonctionnalitéUtilitéAccessible parNiveau
FonctionMotsToListeIDConverti une question (liste de mot) en la liste de leurs Ids
Rend la liste des ID des mots de la question
XBack
FonctionGetMotClefReponseRécupère la liste des MotClef liés à l'ID de la Réponse sélectionnée
Rend la liste des ID des MotClefs
XBack
FonctionGetJsonFromSQLRend le résultat de la requête SQL au format JSONXBack
FonctionNotifyBeforeURI XBack
FonctionAddOrUpdate XBack
FonctionAjoute XBack
FonctionAjouteMotVide XBack
FonctionGetNonoModel XBack
FonctionMUppercase XBack
FonctionAjouteDoc XBack
ConstructorCreate X
DestructorDestroy X
ProcédureLogeas X
ProcédureRechercheRé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 mondeBack/Front
ProcédureEnvoiEmail Tout le mondeBack
ProcédureIncrementReponseAjoute +1 au champ NbUse dans la table MotClefXBack
ProcédureGetRep XBack/Front
ProcédureSaveRepRé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
AssistanceBack/Front
ProcédureSaveRepBigIdentique à 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
AssistanceBack/Front
ProcédureSetMotClefConverti une saisie (liste de mot) en une liste de leurs Ids
Rend la liste des ID des MotClefs
AssistanceBack/Front
ProcédureGetMotClefRé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 mondeBack/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 :

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

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

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

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 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

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 = '';
};

Discours

Présentation Nono V1