====== 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|Back
|Destructor|Destroy| |X|Back
|Procédure|Logeas| |X|Back
|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 **[[https://stephan-bester.medium.com/getting-started-with-mormot-and-lazarus-free-pascal-c7bc7e98a866|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 **[[https://js.devexpress.com/Demos/WidgetsGallery/|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 **[[https://compodoc.app/guides/getting-started.html|Compodoc]]** et sur notre **[[https://wiki.logeas.fr/doku.php|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 : [[https://www.looping-mcd.fr/|Looping]]\\
{{:mcd.png?|}}
===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 {
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\\
-> 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\\
-> 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 =====
{{ :presentation_nono_v1.pdf |Présentation Nono V1}}