feat: Add report generation module for tracking metadata changes
- Implemented `report.nim` to create structured reports on metadata modifications. - Added functionality to merge reports and convert them to formatted strings. docs: Create prompt documentation for Markdown parser project - Added `prompt.md` detailing the requirements and functionalities for the Markdown parser. - Included specifications, usage examples, and testing guidelines. docs: Generate code review report for Markdown parser - Created `rapport_revue_code.md` outlining security vulnerabilities, code quality issues, and suggested improvements. - Provided a detailed analysis of the codebase with actionable recommendations. test: Add test data for Markdown parser - Included various Markdown files and a JPG image in `test_data` to simulate different scenarios. - Ensured that the parser can handle both valid and invalid metadata. chore: Add version management file - Created `version.nim` for automatic versioning of the Markdown parser. - Established constants for major, minor, patch, and build versions.
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
# config.nim
|
||||
# Module de gestion de la configuration
|
||||
|
||||
import std/[json, os, strutils, strformat]
|
||||
|
||||
type
|
||||
LLMModelConfig* = object
|
||||
name*: string
|
||||
systemRole*: string
|
||||
systemContent*: string
|
||||
userRole*: string
|
||||
userContentTemplate*: string
|
||||
isActive*: bool
|
||||
|
||||
AppConfig* = object
|
||||
apiUrl*: string
|
||||
apiMonitorUrl*: string
|
||||
models*: seq[LLMModelConfig]
|
||||
activeModelName*: string
|
||||
debugModeActive*: bool
|
||||
|
||||
proc getActiveModel*(config: AppConfig): LLMModelConfig =
|
||||
## Retourne le modèle actif de la configuration
|
||||
# D'abord essayer de trouver le modèle correspondant au activeModelName
|
||||
for model in config.models:
|
||||
if model.name == config.activeModelName:
|
||||
echo fmt"Utilisation du modèle spécifié dans activeModelName: {model.name}"
|
||||
return model
|
||||
|
||||
# Si le modèle actif n'est pas trouvé par nom, chercher un modèle avec isActive=true
|
||||
for model in config.models:
|
||||
if model.isActive:
|
||||
echo fmt"Utilisation du premier modèle marqué comme actif: {model.name}"
|
||||
return model
|
||||
|
||||
# Si aucun modèle actif n'est trouvé, retourner le premier modèle disponible
|
||||
if config.models.len > 0:
|
||||
echo fmt"Aucun modèle actif trouvé, utilisation du premier modèle disponible: {config.models[0].name}"
|
||||
return config.models[0]
|
||||
|
||||
# Si aucun modèle n'est configuré, retourner une configuration par défaut
|
||||
echo "Aucun modèle configuré, utilisation de la configuration par défaut"
|
||||
result = LLMModelConfig(
|
||||
name: "default",
|
||||
systemRole: "system",
|
||||
systemContent: "Vous êtes un assistant spécialisé dans l'analyse de contenu Markdown. Vous devez extraire ou générer le titre, la description et les tags pertinents pour le document, et les retourner dans un format YAML.",
|
||||
userRole: "user",
|
||||
userContentTemplate: """Analysez ce document Markdown et générez un titre, une description concise et des tags pertinents.
|
||||
Répondez UNIQUEMENT avec un bloc YAML qui commence et finit par '---', inscrire 5 à 10 tags, et contenant:
|
||||
- title: un titre pertinent
|
||||
- description: une description concise
|
||||
- tags: une liste de mots-clés pertinents (avec tirets)
|
||||
|
||||
Document à analyser:
|
||||
{content}""",
|
||||
isActive: true
|
||||
)
|
||||
|
||||
proc defaultConfig*(): AppConfig =
|
||||
## Crée une configuration par défaut
|
||||
result = AppConfig(
|
||||
apiUrl: "http://192.168.20.163:2222/v1/chat/completions",
|
||||
apiMonitorUrl: "http://192.168.20.163:2222/v1/models",
|
||||
activeModelName: "gemma-3-4b-it",
|
||||
debugModeActive: false
|
||||
)
|
||||
|
||||
# Ajouter le modèle gemma par défaut
|
||||
result.models.add(LLMModelConfig(
|
||||
name: "gemma-3-4b-it",
|
||||
systemRole: "system",
|
||||
systemContent: "Vous êtes un assistant spécialisé dans l'analyse de contenu Markdown. Vous devez extraire ou générer le titre, la description et les tags pertinents pour le document, et les retourner dans un format YAML.",
|
||||
userRole: "user",
|
||||
userContentTemplate: """Analysez ce document Markdown et générez un titre, une description concise et des tags pertinents.
|
||||
Répondez UNIQUEMENT avec un bloc YAML qui commence et finit par '---', inscrire 5 à 10 tags, et contenant:
|
||||
- title: un titre pertinent
|
||||
- description: une description concise
|
||||
- tags: une liste de mots-clés pertinents (avec tirets)
|
||||
|
||||
Document à analyser:
|
||||
{content}""",
|
||||
isActive: true
|
||||
))
|
||||
|
||||
proc loadConfig*(configFilePath: string): AppConfig =
|
||||
## Charge la configuration depuis un fichier JSON
|
||||
## Si le fichier n'existe pas, crée une configuration par défaut
|
||||
if not fileExists(configFilePath):
|
||||
result = defaultConfig()
|
||||
return
|
||||
|
||||
try:
|
||||
let jsonNode = parseFile(configFilePath)
|
||||
|
||||
# Initialiser result avec une configuration vide
|
||||
result = AppConfig()
|
||||
|
||||
# Charger l'URL de l'API
|
||||
if jsonNode.hasKey("apiUrl"):
|
||||
result.apiUrl = jsonNode["apiUrl"].getStr()
|
||||
else:
|
||||
result.apiUrl = "http://192.168.20.163:2222/v1/chat/completions"
|
||||
|
||||
# Charger l'URL de vérification des modèles
|
||||
if jsonNode.hasKey("apiMonitorUrl"):
|
||||
result.apiMonitorUrl = jsonNode["apiMonitorUrl"].getStr()
|
||||
else:
|
||||
result.apiMonitorUrl = "http://192.168.20.163:2222/v1/models"
|
||||
|
||||
# Charger le modèle actif
|
||||
if jsonNode.hasKey("activeModelName"):
|
||||
result.activeModelName = jsonNode["activeModelName"].getStr()
|
||||
|
||||
# Charger l'option de mode debug
|
||||
if jsonNode.hasKey("debugModeActive"):
|
||||
result.debugModeActive = jsonNode["debugModeActive"].getBool()
|
||||
else:
|
||||
result.debugModeActive = false
|
||||
|
||||
# Charger les modèles
|
||||
if jsonNode.hasKey("models") and jsonNode["models"].kind == JArray:
|
||||
for modelNode in jsonNode["models"]:
|
||||
var model = LLMModelConfig()
|
||||
|
||||
if modelNode.hasKey("name"):
|
||||
model.name = modelNode["name"].getStr()
|
||||
|
||||
if modelNode.hasKey("systemRole"):
|
||||
model.systemRole = modelNode["systemRole"].getStr()
|
||||
|
||||
if modelNode.hasKey("systemContent"):
|
||||
model.systemContent = modelNode["systemContent"].getStr()
|
||||
|
||||
if modelNode.hasKey("userRole"):
|
||||
model.userRole = modelNode["userRole"].getStr()
|
||||
|
||||
if modelNode.hasKey("userContentTemplate"):
|
||||
model.userContentTemplate = modelNode["userContentTemplate"].getStr()
|
||||
|
||||
if modelNode.hasKey("isActive"):
|
||||
model.isActive = modelNode["isActive"].getBool()
|
||||
if model.isActive and result.activeModelName == "":
|
||||
result.activeModelName = model.name
|
||||
|
||||
result.models.add(model)
|
||||
except:
|
||||
echo "Erreur lors du chargement de la configuration, utilisation des valeurs par défaut"
|
||||
result = defaultConfig()
|
||||
|
||||
# Si aucun modèle n'est présent, ajouter le modèle par défaut
|
||||
if result.models.len == 0:
|
||||
result = defaultConfig()
|
||||
|
||||
# Vérifier que le modèle actif existe
|
||||
var modelExists = false
|
||||
for model in result.models:
|
||||
if model.name == result.activeModelName:
|
||||
modelExists = true
|
||||
break
|
||||
|
||||
# Si le modèle actif n'existe pas, définir le premier modèle comme actif
|
||||
if not modelExists and result.models.len > 0:
|
||||
result.activeModelName = result.models[0].name
|
||||
|
||||
proc saveConfig*(config: AppConfig, configFilePath: string): bool =
|
||||
## Sauvegarde la configuration dans un fichier JSON
|
||||
try:
|
||||
var jsonNode = %* {
|
||||
"apiUrl": config.apiUrl,
|
||||
"apiMonitorUrl": config.apiMonitorUrl,
|
||||
"activeModelName": config.activeModelName,
|
||||
"debugModeActive": config.debugModeActive,
|
||||
"models": []
|
||||
}
|
||||
|
||||
for model in config.models:
|
||||
jsonNode["models"].add(%* {
|
||||
"name": model.name,
|
||||
"systemRole": model.systemRole,
|
||||
"systemContent": model.systemContent,
|
||||
"userRole": model.userRole,
|
||||
"userContentTemplate": model.userContentTemplate,
|
||||
"isActive": model.name == config.activeModelName
|
||||
})
|
||||
|
||||
# Créer le répertoire si nécessaire
|
||||
let configDir = configFilePath.splitPath().head
|
||||
if not dirExists(configDir):
|
||||
createDir(configDir)
|
||||
|
||||
writeFile(configFilePath, pretty(jsonNode))
|
||||
return true
|
||||
except:
|
||||
echo "Erreur lors de la sauvegarde de la configuration"
|
||||
return false
|
||||
|
||||
proc formatUserContent*(model: LLMModelConfig, content: string): string =
|
||||
## Formate le contenu utilisateur en remplaçant la variable {content}
|
||||
return model.userContentTemplate.replace("{content}", content)
|
||||
|
||||
# Variable globale pour contrôler le mode debug
|
||||
var globalDebugMode* = false
|
||||
|
||||
proc setGlobalDebugMode*(mode: bool) =
|
||||
## Définit le mode debug global
|
||||
globalDebugMode = mode
|
||||
|
||||
proc debugLog*(config: AppConfig, message: string) =
|
||||
## Affiche un message de debug uniquement si le mode debug est activé
|
||||
if config.debugModeActive:
|
||||
echo fmt"[DEBUG] {message}"
|
||||
|
||||
proc debugLog*(message: string) =
|
||||
## Affiche un message de debug uniquement si le mode debug global est activé
|
||||
if globalDebugMode:
|
||||
echo fmt"[DEBUG] {message}"
|
||||
@@ -0,0 +1,209 @@
|
||||
# fileutils.nim
|
||||
# Module responsable des opérations sur les fichiers et répertoires
|
||||
|
||||
import std/[os, strutils, strformat, options, tables]
|
||||
import zippy/ziparchives
|
||||
import metadata, report, config
|
||||
|
||||
|
||||
proc printOutput*(output: string, debug: bool = false, appConfig: Option[AppConfig] = none(AppConfig)) =
|
||||
## Affiche un message en sortie standard
|
||||
## Si debug est true, n'affiche le message que si debugModeActive est activé dans la configuration
|
||||
## Si debug est false, affiche toujours le message
|
||||
##
|
||||
## Paramètres:
|
||||
## - output: Le message à afficher
|
||||
## - debug: Si true, il s'agit d'un message de débogage
|
||||
## - appConfig: Configuration de l'application (optionnel)
|
||||
if not debug:
|
||||
# Message normal, toujours affiché
|
||||
echo output
|
||||
else:
|
||||
# Message de débogage, affiché uniquement si debugModeActive est true
|
||||
if appConfig.isSome and appConfig.get().debugModeActive:
|
||||
echo fmt"[DEBUG] {output}"
|
||||
|
||||
proc debugLog*(message: string, appConfig: AppConfig) =
|
||||
## Affiche un message de débogage si le mode debug est activé
|
||||
if appConfig.debugModeActive:
|
||||
echo fmt"[DEBUG] {message}"
|
||||
|
||||
proc walkDirRecFiles*(dir: string, ext: string): seq[string] =
|
||||
## Parcourt récursivement un répertoire et retourne tous les fichiers avec l'extension spécifiée
|
||||
result = @[]
|
||||
|
||||
for kind, path in walkDir(dir):
|
||||
if kind == pcDir:
|
||||
result.add(walkDirRecFiles(path, ext))
|
||||
elif kind == pcFile and path.endsWith(ext):
|
||||
result.add(path)
|
||||
|
||||
proc readMarkdownFile*(filePath: string): string =
|
||||
## Lit le contenu d'un fichier Markdown
|
||||
try:
|
||||
return readFile(filePath)
|
||||
except:
|
||||
echo fmt"Erreur lors de la lecture du fichier: {filePath}"
|
||||
return ""
|
||||
|
||||
proc writeMarkdownFile*(filePath: string, content: string): bool =
|
||||
## Écrit le contenu dans un fichier Markdown
|
||||
try:
|
||||
echo fmt"DEBUG: Tentative d'écriture dans le fichier: {filePath}"
|
||||
echo fmt"DEBUG: Contenu à écrire (premiers 100 caractères): {content[0..min(99, content.len-1)]}"
|
||||
|
||||
let parentDir = parentDir(filePath)
|
||||
if not dirExists(parentDir):
|
||||
echo fmt"DEBUG: Création du répertoire parent: {parentDir}"
|
||||
createDir(parentDir)
|
||||
|
||||
# Normalisation des retours à la ligne pour éviter les problèmes de \n littéraux
|
||||
let normalizedContent = content.replace("\n", "\r\n")
|
||||
|
||||
echo fmt"DEBUG: Écriture de {normalizedContent.len} caractères"
|
||||
writeFile(filePath, normalizedContent)
|
||||
echo fmt"DEBUG: Fichier écrit avec succès"
|
||||
return true
|
||||
except Exception as e:
|
||||
echo fmt"Erreur lors de l'écriture dans le fichier: {filePath}"
|
||||
echo fmt"DEBUG: Exception: {e.msg}"
|
||||
return false
|
||||
|
||||
proc copyFileWithDirectories*(sourcePath, targetPath: string, verbose: bool = false): bool =
|
||||
## Copie un fichier de sourcePath vers targetPath en créant les répertoires
|
||||
## intermédiaires si nécessaire
|
||||
##
|
||||
## Paramètres:
|
||||
## sourcePath: Chemin source du fichier
|
||||
## targetPath: Chemin destination du fichier
|
||||
## verbose: Affiche des informations supplémentaires
|
||||
##
|
||||
## Retourne:
|
||||
## true si la copie a réussi, false sinon
|
||||
try:
|
||||
if verbose:
|
||||
echo fmt"Copie du fichier: {sourcePath} -> {targetPath}"
|
||||
|
||||
let parentDir = parentDir(targetPath)
|
||||
if not dirExists(parentDir):
|
||||
createDir(parentDir)
|
||||
|
||||
copyFile(sourcePath, targetPath)
|
||||
return true
|
||||
except Exception as e:
|
||||
echo fmt"Erreur lors de la copie du fichier: {e.msg}"
|
||||
return false
|
||||
|
||||
proc zipDir*(sourceDir: string, zipFilePath: string = "", verbose: bool = false): bool =
|
||||
## Crée une archive zip du répertoire source dans un répertoire "backup"
|
||||
## Si zipFilePath est vide, utilise le nom du répertoire source pour le zip
|
||||
try:
|
||||
let
|
||||
sourceDirName = sourceDir.splitPath().tail
|
||||
parentDir = sourceDir.splitPath().head
|
||||
backupDir = parentDir / "backup"
|
||||
|
||||
# Créer le répertoire backup s'il n'existe pas
|
||||
if not dirExists(backupDir):
|
||||
if verbose:
|
||||
echo fmt"Création du répertoire backup: {backupDir}"
|
||||
createDir(backupDir)
|
||||
|
||||
# Déterminer le nom du fichier zip
|
||||
var actualZipPath: string
|
||||
if zipFilePath == "":
|
||||
# Si aucun nom n'est fourni, utiliser le nom du répertoire source
|
||||
actualZipPath = backupDir / fmt"{sourceDirName}.zip"
|
||||
else:
|
||||
# Si un nom est fourni, l'utiliser mais le placer dans le répertoire backup
|
||||
actualZipPath = backupDir / zipFilePath.extractFilename()
|
||||
|
||||
if verbose:
|
||||
echo fmt"Création d'une archive zip: {actualZipPath} à partir de {sourceDir}"
|
||||
|
||||
# Utilisons une approche plus simple basée sur la documentation de zippy
|
||||
# Extrayons d'abord tous les fichiers avec leurs chemins relatifs
|
||||
var fileEntries = initTable[string, string]()
|
||||
|
||||
# Fonction récursive pour ajouter les fichiers
|
||||
proc addFilesRecursively(dir: string, relativePath = "") =
|
||||
for kind, path in walkDir(dir):
|
||||
let relPath = if relativePath.len > 0: relativePath / path.extractFilename else: path.extractFilename
|
||||
|
||||
if kind == pcFile:
|
||||
# Ajouter le fichier à l'archive
|
||||
if verbose:
|
||||
echo fmt"Ajout du fichier: {path}"
|
||||
fileEntries[relPath] = readFile(path)
|
||||
elif kind == pcDir:
|
||||
# Récursivement parcourir les sous-répertoires
|
||||
addFilesRecursively(path, relPath)
|
||||
|
||||
# Commencer la compression depuis le répertoire source
|
||||
addFilesRecursively(sourceDir)
|
||||
|
||||
# Créer l'archive avec toutes les entrées
|
||||
let zipData = ziparchives.createZipArchive(fileEntries)
|
||||
|
||||
# Sauvegarder l'archive zip
|
||||
writeFile(actualZipPath, zipData)
|
||||
|
||||
if verbose:
|
||||
echo fmt"Archive zip créée avec succès: {actualZipPath}"
|
||||
|
||||
return true
|
||||
except Exception as e:
|
||||
echo fmt"Exception lors de la création du zip: {e.msg}"
|
||||
return false
|
||||
|
||||
proc processMarkdownFileBasic*(sourcePath, targetPath: string, verbose: bool = false): Report =
|
||||
## Traite un fichier Markdown : lit, analyse les métadonnées, complète si nécessaire, et écrit
|
||||
var report = newReport()
|
||||
|
||||
if verbose:
|
||||
echo fmt"Traitement du fichier: {sourcePath}"
|
||||
|
||||
let content = readMarkdownFile(sourcePath)
|
||||
if content == "":
|
||||
report.errors.add(fmt"Impossible de lire le fichier: {sourcePath}")
|
||||
return report
|
||||
|
||||
# Extraire les métadonnées existantes ou créer de nouvelles métadonnées
|
||||
var metadata: Metadata
|
||||
let metadataOpt = extractMetadataFromYaml(content)
|
||||
|
||||
# Utiliser correctement l'API Option
|
||||
if metadataOpt.isSome():
|
||||
metadata = metadataOpt.get()
|
||||
else:
|
||||
metadata = newMetadata()
|
||||
|
||||
# Compléter les métadonnées manquantes
|
||||
echo ">>> Avant updateMetadata: catégorie = " & metadata.category
|
||||
# Utiliser le targetPath pour déterminer la catégorie au lieu du sourcePath
|
||||
let modified = updateMetadata(metadata, content, targetPath, true, true)
|
||||
echo ">>> Après updateMetadata: catégorie = " & metadata.category
|
||||
|
||||
if verbose:
|
||||
if metadata.isNew:
|
||||
echo fmt"Métadonnées ajoutées pour: {sourcePath}"
|
||||
elif modified:
|
||||
echo fmt"Métadonnées mises à jour pour: {sourcePath}"
|
||||
else:
|
||||
echo fmt"Métadonnées déjà complètes pour: {sourcePath}"
|
||||
|
||||
# Ajouter les métadonnées au contenu
|
||||
let newContent = addMetadataToContent(metadata, content)
|
||||
|
||||
# Écrire le nouveau contenu dans le fichier cible
|
||||
if writeMarkdownFile(targetPath, newContent):
|
||||
if metadata.isNew:
|
||||
report.newMetadata.add(targetPath)
|
||||
elif modified:
|
||||
report.updatedMetadata.add(targetPath)
|
||||
else:
|
||||
report.unchanged.add(targetPath)
|
||||
else:
|
||||
report.errors.add(fmt"Impossible d'écrire dans le fichier: {targetPath}")
|
||||
|
||||
return report
|
||||
@@ -0,0 +1,692 @@
|
||||
# metadata.nim
|
||||
# Module responsable de l'extraction et de la manipulation des métadonnées YAML
|
||||
|
||||
import std/[strutils, strformat, options, times, os, httpclient, json, sequtils, tables, uri]
|
||||
import config
|
||||
|
||||
# Fonctions utilitaires
|
||||
proc isEmptyOrWhitespace*(s: string): bool =
|
||||
## Vérifie si une chaîne est vide ou contient uniquement des espaces blancs
|
||||
if s.len == 0:
|
||||
return true
|
||||
|
||||
for c in s:
|
||||
if not c.isSpaceAscii():
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
proc parseYamlValue(line: string): string =
|
||||
## Extrait la valeur d'une ligne YAML au format "clé: valeur"
|
||||
let parts = line.split(':', 1)
|
||||
if parts.len > 1:
|
||||
return parts[1].strip()
|
||||
return ""
|
||||
|
||||
proc isValidTag*(tag: string): bool =
|
||||
## Vérifie si un tag est valide (contient au moins un caractère alphanumérique)
|
||||
## Retourne false pour les tags qui ne contiennent que des caractères spéciaux comme "--"
|
||||
if tag.len == 0:
|
||||
return false
|
||||
|
||||
# Vérifier si le tag contient au moins une lettre ou un chiffre
|
||||
for c in tag:
|
||||
if c.isAlphaNumeric():
|
||||
return true
|
||||
|
||||
return false # Tag ne contient que des caractères spéciaux
|
||||
|
||||
proc cleanTagText*(tag: string): string =
|
||||
## Nettoie un tag en remplaçant les espaces par des underscores et en éliminant les caractères indésirables
|
||||
var resultString = tag.strip()
|
||||
# Supprimer les guillemets s'ils encadrent le tag
|
||||
if resultString.len >= 2 and resultString[0] == '"' and resultString[^1] == '"':
|
||||
resultString = resultString[1..^2]
|
||||
|
||||
# Supprimer les apostrophes s'ils encadrent le tag
|
||||
if resultString.len >= 2 and resultString[0] == '\'' and resultString[^1] == '\'':
|
||||
resultString = resultString[1..^2]
|
||||
|
||||
# Convertir en minuscules
|
||||
resultString = resultString.toLowerAscii()
|
||||
|
||||
# Remplacer les espaces par des underscores
|
||||
resultString = resultString.replace(" ", "_")
|
||||
|
||||
# Remplacer certains caractères spéciaux par des underscores
|
||||
for c in ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', '=', '{', '}', '[', ']', '|', '\\', ':', ';', '"', '\'', '<', '>', ',', '.', '?', '/']:
|
||||
resultString = resultString.replace($c, "_")
|
||||
|
||||
# Limiter les underscores consécutifs à un seul
|
||||
while "__" in resultString:
|
||||
resultString = resultString.replace("__", "_")
|
||||
|
||||
# Supprimer les underscores au début et à la fin
|
||||
resultString = resultString.strip(chars={'_'})
|
||||
|
||||
return resultString
|
||||
|
||||
proc parseYamlSeq(lines: seq[string], startIndex: int): tuple[tags: seq[string], endIndex: int] =
|
||||
## Parse une séquence YAML commençant par des tirets
|
||||
var
|
||||
tags: seq[string] = @[]
|
||||
i = startIndex
|
||||
|
||||
while i < lines.len:
|
||||
let line = lines[i].strip()
|
||||
if line.startsWith("-"):
|
||||
let tagText = line[1..^1].strip()
|
||||
let cleanedTag = cleanTagText(tagText)
|
||||
if isValidTag(cleanedTag): # Ne garder que les tags valides
|
||||
tags.add(cleanedTag)
|
||||
i += 1
|
||||
else:
|
||||
break
|
||||
|
||||
return (tags: tags, endIndex: i)
|
||||
|
||||
type
|
||||
AIAnalysisResult* = object
|
||||
title*: string
|
||||
description*: string
|
||||
tags*: seq[string]
|
||||
|
||||
Metadata* = object
|
||||
title*: string
|
||||
description*: string
|
||||
tags*: seq[string]
|
||||
creationDate*: string
|
||||
creationTime*: string
|
||||
modificationDate*: string
|
||||
modificationTime*: string
|
||||
auteur*: string
|
||||
url*: string
|
||||
lang*: string
|
||||
category*: string
|
||||
isNew*: bool # Indique si les métadonnées ont été générées ou étaient déjà présentes
|
||||
|
||||
proc newMetadata*(): Metadata =
|
||||
## Crée un nouvel objet Metadata vide
|
||||
result = Metadata(
|
||||
title: "",
|
||||
description: "",
|
||||
tags: @[],
|
||||
creationDate: "",
|
||||
creationTime: "",
|
||||
modificationDate: "",
|
||||
modificationTime: "",
|
||||
auteur: "Non spécifié",
|
||||
url: "",
|
||||
lang: "fr", # Langue par défaut
|
||||
category: "",
|
||||
isNew: true
|
||||
)
|
||||
|
||||
proc formatCurrentDateTime*(): tuple[date: string, time: string] =
|
||||
## Retourne la date et l'heure actuelles formatées selon le format requis
|
||||
let now = now()
|
||||
return (
|
||||
date: now.format("yyyy-MM-dd"),
|
||||
time: now.format("HH:mm:ss")
|
||||
)
|
||||
|
||||
proc formatFileTime*(time: Time): tuple[date: string, time: string] =
|
||||
## Formate un objet Time en tuple (date, heure) selon le format requis
|
||||
return (
|
||||
date: time.format("yyyy-MM-dd"),
|
||||
time: time.format("HH:mm:ss")
|
||||
)
|
||||
|
||||
proc getFileTimeInfo*(filePath: string): tuple[creation: tuple[date: string, time: string],
|
||||
modification: tuple[date: string, time: string]] =
|
||||
## Obtient les informations de date/heure de création et modification du fichier
|
||||
try:
|
||||
let
|
||||
fileInfo = getFileInfo(filePath)
|
||||
creationTime = fileInfo.creationTime
|
||||
modificationTime = fileInfo.lastWriteTime
|
||||
|
||||
return (
|
||||
creation: formatFileTime(creationTime),
|
||||
modification: formatFileTime(modificationTime)
|
||||
)
|
||||
except:
|
||||
let current = formatCurrentDateTime()
|
||||
return (
|
||||
creation: current,
|
||||
modification: current
|
||||
)
|
||||
|
||||
proc extractTitle*(content: string, filePath: string = ""): string =
|
||||
## Tente d'extraire un titre à partir du contenu Markdown
|
||||
## Regarde la première ligne commençant par # ou utilise le nom du fichier
|
||||
|
||||
let lines = content.splitLines()
|
||||
for line in lines:
|
||||
let trimmed = line.strip()
|
||||
if trimmed.startsWith("# "):
|
||||
return trimmed[2..^1]
|
||||
|
||||
# Si pas de titre trouvé, utiliser le nom du fichier sans extension
|
||||
if filePath != "":
|
||||
let fileName = extractFilename(filePath)
|
||||
if fileName.contains('.'):
|
||||
return fileName.split('.')[0]
|
||||
else:
|
||||
return fileName
|
||||
|
||||
return "Document Markdown"
|
||||
|
||||
proc extractYamlValue(yaml: string, key: string): string =
|
||||
## Extrait la valeur d'une clé spécifique d'un bloc YAML
|
||||
let lines = yaml.splitLines()
|
||||
|
||||
for line in lines:
|
||||
let trimmedLine = line.strip()
|
||||
# Cherche une ligne qui commence par la clé suivie de deux points
|
||||
if trimmedLine.startsWith(key & ":"):
|
||||
# Extrait la partie après les deux points
|
||||
let value = trimmedLine.substr(key.len + 1).strip()
|
||||
return value
|
||||
|
||||
return ""
|
||||
|
||||
proc extractYamlList(yaml: string, key: string): seq[string] =
|
||||
## Extrait une liste de valeurs pour une clé spécifique d'un bloc YAML
|
||||
var resultList: seq[string] = @[]
|
||||
let lines = yaml.splitLines()
|
||||
var i = 0
|
||||
|
||||
# Chercher la ligne avec la clé
|
||||
while i < lines.len:
|
||||
if lines[i].strip().startsWith(key & ":"):
|
||||
# Si la clé est suivie directement de valeurs sur la même ligne
|
||||
let restOfLine = lines[i].substr(key.len + 1).strip()
|
||||
if restOfLine.len > 0 and not restOfLine.startsWith("-"):
|
||||
# Format compact: key: [val1, val2, val3]
|
||||
if restOfLine.startsWith("[") and restOfLine.endsWith("]"):
|
||||
let items = restOfLine[1..^2].split(",")
|
||||
for item in items:
|
||||
let cleaned = item.strip()
|
||||
if cleaned.len > 0:
|
||||
resultList.add(cleaned)
|
||||
break
|
||||
|
||||
# Sinon, chercher les éléments de liste sur les lignes suivantes
|
||||
i += 1
|
||||
while i < lines.len:
|
||||
let line = lines[i].strip()
|
||||
if line.startsWith("-"):
|
||||
let value = line[1..^1].strip()
|
||||
if value.len > 0:
|
||||
resultList.add(value)
|
||||
else:
|
||||
if not line.isEmptyOrWhitespace():
|
||||
break
|
||||
i += 1
|
||||
break
|
||||
i += 1
|
||||
|
||||
return resultList
|
||||
|
||||
proc extractYamlBlock*(content: string): Option[string] =
|
||||
## Extrait un bloc YAML d'une chaîne de caractères, délimité par des marqueurs "---"
|
||||
let lines = content.splitLines()
|
||||
|
||||
# Vérifier si le contenu commence par un délimiteur YAML
|
||||
if lines.len < 3 or lines[0] != "---":
|
||||
return none(string) # Pas de section YAML
|
||||
|
||||
var endYamlIndex = -1
|
||||
|
||||
# Chercher la fin de la section YAML
|
||||
for j in 1..<lines.len:
|
||||
if lines[j] == "---":
|
||||
endYamlIndex = j
|
||||
break
|
||||
|
||||
if endYamlIndex == -1:
|
||||
return none(string) # Section YAML mal formatée
|
||||
|
||||
# Extraire le bloc YAML sans les délimiteurs
|
||||
let yamlBlock = lines[1..<endYamlIndex].join("\n")
|
||||
return some(yamlBlock)
|
||||
|
||||
proc extractMetadataFromYaml*(content: string): Option[Metadata] =
|
||||
## Tente d'extraire les métadonnées YAML du début d'un fichier Markdown
|
||||
|
||||
let lines = content.splitLines()
|
||||
if lines.len < 3 or lines[0] != "---":
|
||||
return none(Metadata) # Pas de section YAML
|
||||
|
||||
var
|
||||
endYamlIndex = -1
|
||||
metadata = newMetadata()
|
||||
i = 1
|
||||
|
||||
# Chercher la fin de la section YAML
|
||||
for j in 1..<lines.len:
|
||||
if lines[j] == "---":
|
||||
endYamlIndex = j
|
||||
break
|
||||
|
||||
if endYamlIndex == -1:
|
||||
return none(Metadata) # Section YAML mal formatée
|
||||
|
||||
metadata.isNew = false
|
||||
|
||||
# Analyser chaque ligne YAML
|
||||
i = 1
|
||||
while i < endYamlIndex:
|
||||
let line = lines[i].strip()
|
||||
|
||||
if line.startsWith("Titre:"):
|
||||
metadata.title = parseYamlValue(line)
|
||||
elif line.startsWith("Description:"):
|
||||
metadata.description = parseYamlValue(line)
|
||||
elif line.startsWith("tags:"):
|
||||
i += 1
|
||||
let res = parseYamlSeq(lines, i)
|
||||
metadata.tags = res.tags
|
||||
i = res.endIndex
|
||||
continue # On a déjà incrémenté i
|
||||
elif line.startsWith("Date de création:"):
|
||||
metadata.creationDate = parseYamlValue(line)
|
||||
elif line.startsWith("Heure de création:"):
|
||||
metadata.creationTime = parseYamlValue(line)
|
||||
elif line.startsWith("Date de modification:"):
|
||||
metadata.modificationDate = parseYamlValue(line)
|
||||
elif line.startsWith("Heure de modification:"):
|
||||
metadata.modificationTime = parseYamlValue(line)
|
||||
elif line.startsWith("Author:"):
|
||||
metadata.auteur = parseYamlValue(line)
|
||||
elif line.startsWith("URL:"):
|
||||
metadata.url = parseYamlValue(line)
|
||||
elif line.startsWith("Lang:"):
|
||||
metadata.lang = parseYamlValue(line)
|
||||
elif line.startsWith("Catégorie:"):
|
||||
metadata.category = parseYamlValue(line)
|
||||
|
||||
i += 1
|
||||
|
||||
return some(metadata)
|
||||
|
||||
proc analyzeWithAI*(content: string, configPath = "config.json"): AIAnalysisResult =
|
||||
## Analyse le contenu Markdown avec l'IA et retourne les métadonnées générées
|
||||
var
|
||||
appConfig: AppConfig
|
||||
activeModel: LLMModelConfig
|
||||
|
||||
try:
|
||||
appConfig = loadConfig(configPath)
|
||||
activeModel = appConfig.getActiveModel()
|
||||
|
||||
# Afficher les informations de debug si le mode debug est activé
|
||||
if appConfig.debugModeActive:
|
||||
echo fmt"[DEBUG] API URL: {appConfig.apiUrl}"
|
||||
echo fmt"[DEBUG] Modèle actif: {activeModel.name}"
|
||||
echo fmt"[DEBUG] activeModelName dans config: {appConfig.activeModelName}"
|
||||
except Exception as e:
|
||||
echo fmt"Erreur lors du chargement de la configuration: {e.msg}"
|
||||
return AIAnalysisResult(title: "", description: "", tags: @[])
|
||||
|
||||
let client = newHttpClient()
|
||||
client.headers = newHttpHeaders({
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
})
|
||||
|
||||
try:
|
||||
# Construction du corps de la requête
|
||||
var requestBody = %* {
|
||||
"model": activeModel.name,
|
||||
"messages": [
|
||||
{
|
||||
"role": activeModel.systemRole,
|
||||
"content": activeModel.systemContent
|
||||
},
|
||||
{
|
||||
"role": activeModel.userRole,
|
||||
"content": formatUserContent(activeModel, content)
|
||||
}
|
||||
],
|
||||
"max_tokens": 2048,
|
||||
"temperature": 0.7,
|
||||
"frequency_penalty": 0.0,
|
||||
"presence_penalty": 0.0,
|
||||
"stream": false,
|
||||
"stop": []
|
||||
}
|
||||
|
||||
# Afficher la requête complète en mode débogage
|
||||
if appConfig.debugModeActive:
|
||||
echo fmt"[DEBUG] Requête: {$requestBody}"
|
||||
echo fmt"[DEBUG] Envoi de la requête à: {appConfig.apiUrl}"
|
||||
|
||||
let
|
||||
response = client.post(appConfig.apiUrl, body = $requestBody)
|
||||
responseJson = parseJson(response.body)
|
||||
|
||||
if appConfig.debugModeActive:
|
||||
echo fmt"[DEBUG] Code de réponse: {response.code}"
|
||||
|
||||
if response.code == Http200:
|
||||
if appConfig.debugModeActive:
|
||||
echo fmt"[DEBUG] Réponse reçue avec succès"
|
||||
var content = responseJson["choices"][0]["message"]["content"].getStr()
|
||||
|
||||
# Nettoyage explicite des caractères d'échappement \n dans la réponse complète
|
||||
content = content.replace("\\n", "\n")
|
||||
|
||||
var
|
||||
title = ""
|
||||
description = ""
|
||||
tags: seq[string] = @[]
|
||||
|
||||
# Extraction du bloc YAML
|
||||
let yamlMatch = content.extractYamlBlock()
|
||||
if yamlMatch.isSome:
|
||||
let yaml = yamlMatch.get()
|
||||
|
||||
if yaml.len > 0:
|
||||
# Extraction des valeurs
|
||||
title = extractYamlValue(yaml, "title")
|
||||
description = extractYamlValue(yaml, "description")
|
||||
tags = extractYamlList(yaml, "tags")
|
||||
|
||||
if appConfig.debugModeActive:
|
||||
echo fmt"[DEBUG] Titre extrait: {title}"
|
||||
echo fmt"[DEBUG] Description extraite: {description}"
|
||||
echo fmt"[DEBUG] Tags extraits: {tags.len} tags"
|
||||
|
||||
return AIAnalysisResult(title: title, description: description, tags: tags)
|
||||
else:
|
||||
echo fmt"Erreur lors de l'appel à l'API: {response.code} - {response.body}"
|
||||
return AIAnalysisResult(title: "", description: "", tags: @[])
|
||||
except Exception as e:
|
||||
echo fmt"Exception lors de l'appel à l'API: {e.msg}"
|
||||
return AIAnalysisResult(title: "", description: "", tags: @[])
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
proc extractCategoryFromPath*(filePath: string): string =
|
||||
## Extrait une catégorie basée sur le chemin du fichier
|
||||
## Format: Parent/Enfant/SousEnfant
|
||||
if filePath.len == 0:
|
||||
return "Non classé"
|
||||
|
||||
# Normaliser le chemin en utilisant / plutôt que \
|
||||
var normalizedPath = filePath.replace('\\', '/')
|
||||
|
||||
# Obtenir le chemin relatif en supprimant les chemins absolus
|
||||
let
|
||||
driveLetterPos = normalizedPath.find(":/")
|
||||
pathStart = if driveLetterPos != -1: driveLetterPos + 2 else: 0
|
||||
relativePath = normalizedPath[pathStart..^1]
|
||||
|
||||
# Diviser le chemin en parties
|
||||
var parts = relativePath.split('/')
|
||||
|
||||
# Retirer le nom du fichier (dernier élément)
|
||||
if parts.len > 0 and parts[^1].contains('.'):
|
||||
discard parts.pop()
|
||||
|
||||
# Filtrer les parties vides et les noms de répertoires spéciaux
|
||||
parts = parts.filterIt(it.len > 0 and it != "." and it != "..")
|
||||
|
||||
# Si après filtrage le chemin est vide, renvoyer catégorie par défaut
|
||||
if parts.len == 0:
|
||||
return "Non classé"
|
||||
|
||||
# Rejoindre les parties avec un /
|
||||
result = parts.join("/")
|
||||
|
||||
proc updateMetadata*(metadata: var Metadata, content: string, filePath: string, useAI: bool = true, forceUpdateCategory: bool = true): bool =
|
||||
## Complète les métadonnées manquantes
|
||||
## Retourne vrai si des modifications ont été apportées
|
||||
##
|
||||
## Paramètres:
|
||||
## - metadata: Les métadonnées à mettre à jour
|
||||
## - content: Le contenu du fichier Markdown
|
||||
## - filePath: Chemin complet vers le fichier
|
||||
## - useAI: Utiliser l'IA pour compléter les métadonnées manquantes
|
||||
## - forceUpdateCategory: Si vrai, remplace toujours la catégorie par celle extraite du chemin
|
||||
var modified = false
|
||||
|
||||
# Utiliser l'IA pour compléter les métadonnées manquantes
|
||||
if useAI:
|
||||
if metadata.title == "" or metadata.description == "" or metadata.tags.len == 0:
|
||||
# Analyser le contenu avec l'IA
|
||||
let aiResult = analyzeWithAI(content)
|
||||
|
||||
# Compléter le titre s'il est manquant
|
||||
if metadata.title == "" and aiResult.title != "":
|
||||
metadata.title = aiResult.title
|
||||
modified = true
|
||||
|
||||
# Compléter la description si elle est manquante
|
||||
if metadata.description == "" and aiResult.description != "":
|
||||
metadata.description = aiResult.description
|
||||
modified = true
|
||||
|
||||
# Compléter les tags s'ils sont manquants
|
||||
if metadata.tags.len == 0 and aiResult.tags.len > 0:
|
||||
# Filtrer pour ne garder que les tags valides et nettoyer les espaces
|
||||
var cleanedTags: seq[string] = @[]
|
||||
for tag in aiResult.tags:
|
||||
let cleanedTag = cleanTagText(tag)
|
||||
if isValidTag(cleanedTag):
|
||||
cleanedTags.add(cleanedTag)
|
||||
|
||||
metadata.tags = cleanedTags
|
||||
if metadata.tags.len > 0:
|
||||
modified = true
|
||||
|
||||
# Méthodes traditionnelles si l'IA n'est pas utilisée ou n'a pas fourni de données
|
||||
if metadata.title == "":
|
||||
metadata.title = extractTitle(content, filePath)
|
||||
modified = true
|
||||
|
||||
# Obtenir la date de création réelle du fichier
|
||||
let
|
||||
fileExists = fileExists(filePath)
|
||||
currentDateTime = formatCurrentDateTime() # Date/heure actuelle
|
||||
|
||||
# Utiliser la date de création réelle du fichier si elle existe
|
||||
if metadata.creationDate == "" or metadata.creationTime == "":
|
||||
if fileExists:
|
||||
try:
|
||||
let creationTime = getCreationTime(filePath)
|
||||
let formattedCreationDate = format(creationTime, "yyyy-MM-dd")
|
||||
let formattedCreationTime = format(creationTime, "HH:mm:ss")
|
||||
|
||||
if metadata.creationDate == "":
|
||||
metadata.creationDate = formattedCreationDate
|
||||
modified = true
|
||||
|
||||
if metadata.creationTime == "":
|
||||
metadata.creationTime = formattedCreationTime
|
||||
modified = true
|
||||
except:
|
||||
# En cas d'erreur, utiliser la date/heure actuelle comme fallback
|
||||
if metadata.creationDate == "":
|
||||
metadata.creationDate = currentDateTime.date
|
||||
modified = true
|
||||
|
||||
if metadata.creationTime == "":
|
||||
metadata.creationTime = currentDateTime.time
|
||||
modified = true
|
||||
else:
|
||||
# Pour les nouveaux fichiers, utiliser la date/heure actuelle
|
||||
if metadata.creationDate == "":
|
||||
metadata.creationDate = currentDateTime.date
|
||||
modified = true
|
||||
|
||||
if metadata.creationTime == "":
|
||||
metadata.creationTime = currentDateTime.time
|
||||
modified = true
|
||||
|
||||
if metadata.auteur == "":
|
||||
metadata.auteur = "Non spécifié"
|
||||
modified = true
|
||||
|
||||
if metadata.lang == "":
|
||||
metadata.lang = "fr"
|
||||
modified = true
|
||||
|
||||
if forceUpdateCategory or metadata.category == "" or metadata.category == "Non classé":
|
||||
# Utiliser la structure des répertoires comme catégorie
|
||||
let pathCategory = extractCategoryFromPath(filePath)
|
||||
echo fmt">>> extractCategoryFromPath pour {filePath} a retourné: {pathCategory}"
|
||||
# Toujours mettre à jour la catégorie si forceUpdateCategory est true
|
||||
echo fmt">>> Mise à jour de la catégorie: {metadata.category} -> {pathCategory}"
|
||||
metadata.category = pathCategory
|
||||
modified = true
|
||||
|
||||
# Nettoyer les tags existants pour éliminer les tags non valides
|
||||
if metadata.tags.len > 0:
|
||||
let originalTags = metadata.tags # Garder une copie des tags originaux
|
||||
var cleanedTags: seq[string] = @[]
|
||||
|
||||
for tag in metadata.tags:
|
||||
let cleanedTag = cleanTagText(tag)
|
||||
if isValidTag(cleanedTag):
|
||||
cleanedTags.add(cleanedTag)
|
||||
|
||||
# Vérifier si les tags ont été modifiés (nombre différent ou contenu différent)
|
||||
if originalTags.len != cleanedTags.len or originalTags != cleanedTags:
|
||||
modified = true
|
||||
|
||||
metadata.tags = cleanedTags
|
||||
|
||||
# Si des modifications ont été apportées, mettre à jour la date et l'heure de modification
|
||||
# pour refléter le moment actuel du traitement
|
||||
if modified:
|
||||
metadata.modificationDate = currentDateTime.date
|
||||
metadata.modificationTime = currentDateTime.time
|
||||
else:
|
||||
# Si aucune modification n'a été apportée, vérifier si la date/heure de modification est définie
|
||||
# Si non, utiliser la date de dernière modification du fichier ou la date actuelle
|
||||
if metadata.modificationDate == "" or metadata.modificationTime == "":
|
||||
if fileExists:
|
||||
try:
|
||||
let modificationTime = getLastModificationTime(filePath)
|
||||
let formattedModDate = format(modificationTime, "yyyy-MM-dd")
|
||||
let formattedModTime = format(modificationTime, "HH:mm:ss")
|
||||
|
||||
if metadata.modificationDate == "":
|
||||
metadata.modificationDate = formattedModDate
|
||||
modified = true # Marquer comme modifié même si on utilise juste la date du fichier
|
||||
|
||||
if metadata.modificationTime == "":
|
||||
metadata.modificationTime = formattedModTime
|
||||
modified = true # Marquer comme modifié même si on utilise juste l'heure du fichier
|
||||
except:
|
||||
# En cas d'erreur, utiliser la date/heure actuelle
|
||||
if metadata.modificationDate == "":
|
||||
metadata.modificationDate = currentDateTime.date
|
||||
modified = true
|
||||
|
||||
if metadata.modificationTime == "":
|
||||
metadata.modificationTime = currentDateTime.time
|
||||
modified = true
|
||||
else:
|
||||
# Pour les nouveaux fichiers, utiliser la date/heure actuelle
|
||||
if metadata.modificationDate == "":
|
||||
metadata.modificationDate = currentDateTime.date
|
||||
modified = true
|
||||
|
||||
if metadata.modificationTime == "":
|
||||
metadata.modificationTime = currentDateTime.time
|
||||
modified = true
|
||||
|
||||
return modified
|
||||
|
||||
proc metadataToYaml*(metadata: Metadata): string =
|
||||
## Convertit les métadonnées en YAML formaté
|
||||
|
||||
# Créer les lignes YAML individuellement sans l'opérateur &=
|
||||
var lines: seq[string] = @["---"]
|
||||
|
||||
lines.add(fmt"Titre: {metadata.title}")
|
||||
lines.add(fmt"Description: {metadata.description}")
|
||||
|
||||
lines.add("tags:")
|
||||
for tag in metadata.tags:
|
||||
lines.add(fmt" - {tag}")
|
||||
|
||||
lines.add(fmt"Date de création: {metadata.creationDate}")
|
||||
lines.add(fmt"Heure de création: {metadata.creationTime}")
|
||||
lines.add(fmt"Date de modification: {metadata.modificationDate}")
|
||||
lines.add(fmt"Heure de modification: {metadata.modificationTime}")
|
||||
lines.add(fmt"Auteur: {metadata.auteur}")
|
||||
lines.add(fmt"URL: {metadata.url}")
|
||||
lines.add(fmt"Lang: {metadata.lang}")
|
||||
lines.add(fmt"Catégorie: {metadata.category}")
|
||||
|
||||
lines.add("---")
|
||||
|
||||
# Joindre toutes les lignes avec des retours à la ligne
|
||||
return lines.join("\n")
|
||||
|
||||
proc getContentWithoutYaml*(content: string): string =
|
||||
## Retourne le contenu sans la section YAML initiale, s'il y en a une
|
||||
let lines = content.splitLines()
|
||||
|
||||
if lines.len >= 2 and lines[0] == "---":
|
||||
# Chercher la ligne de fin de la section YAML
|
||||
for i in 1..<lines.len:
|
||||
if lines[i] == "---":
|
||||
# Retourner le contenu après la section YAML
|
||||
return lines[i+1..^1].join("\n")
|
||||
|
||||
# Pas de section YAML, retourner le contenu tel quel
|
||||
return content
|
||||
|
||||
proc addMetadataToContent*(metadata: Metadata, content: string): string =
|
||||
## Ajoute les métadonnées formatées en YAML au début du contenu
|
||||
## Supprime d'abord toute section YAML existante
|
||||
let cleanContent = getContentWithoutYaml(content)
|
||||
|
||||
return metadataToYaml(metadata) & "\n" & cleanContent
|
||||
|
||||
proc isModelAvailable*(config: AppConfig): bool =
|
||||
## Vérifie si le modèle actif est disponible sur le serveur LM Studio
|
||||
var client = newHttpClient()
|
||||
defer: client.close()
|
||||
client.headers = newHttpHeaders({
|
||||
"Accept": "application/json"
|
||||
})
|
||||
|
||||
try:
|
||||
if config.debugModeActive:
|
||||
echo fmt"[DEBUG] Vérification de la disponibilité des modèles sur: {config.apiMonitorUrl}"
|
||||
|
||||
let
|
||||
response = client.get(config.apiMonitorUrl)
|
||||
responseJson = parseJson(response.body)
|
||||
|
||||
if response.code == Http200:
|
||||
# Vérifier si le modèle actif figure dans la liste des modèles disponibles
|
||||
var activeModel = config.activeModelName
|
||||
|
||||
# Vérifier si la réponse contient une clé "data" avec un tableau de modèles
|
||||
if responseJson.hasKey("data") and responseJson["data"].kind == JArray:
|
||||
let modelsArray = responseJson["data"]
|
||||
for modelObj in modelsArray:
|
||||
if modelObj.hasKey("id") and modelObj["id"].getStr() == activeModel:
|
||||
echo fmt"Modèle '{activeModel}' disponible sur le serveur"
|
||||
return true
|
||||
|
||||
# Si nous arrivons ici, le modèle n'a pas été trouvé
|
||||
echo fmt"Modèle '{activeModel}' non disponible sur le serveur."
|
||||
return false
|
||||
else:
|
||||
echo "Format de réponse inattendu de l'API."
|
||||
return false
|
||||
else:
|
||||
echo fmt"Erreur lors de la connexion à l'API: {response.code}"
|
||||
return false
|
||||
except Exception as e:
|
||||
echo fmt"Exception lors de la vérification du modèle: {e.msg}"
|
||||
return false
|
||||
@@ -0,0 +1,69 @@
|
||||
# report.nim
|
||||
# Module responsable de la génération des rapports sur les modifications effectuées
|
||||
|
||||
import std/[strformat, strutils]
|
||||
|
||||
type
|
||||
FileReport* = object
|
||||
sourcePath*: string
|
||||
targetPath*: string
|
||||
status*: string
|
||||
success*: bool
|
||||
|
||||
Report* = object
|
||||
newMetadata*: seq[string] # Fichiers où des métadonnées ont été ajoutées
|
||||
updatedMetadata*: seq[string] # Fichiers où des métadonnées ont été mises à jour
|
||||
unchanged*: seq[string] # Fichiers inchangés
|
||||
errors*: seq[string] # Erreurs rencontrées
|
||||
fileReports*: seq[FileReport] # Rapports de fichiers individuels
|
||||
|
||||
proc newReport*(): Report =
|
||||
## Crée un nouveau rapport vide
|
||||
result = Report(
|
||||
newMetadata: @[],
|
||||
updatedMetadata: @[],
|
||||
unchanged: @[],
|
||||
errors: @[],
|
||||
fileReports: @[]
|
||||
)
|
||||
|
||||
proc merge*(self: var Report, other: Report) =
|
||||
## Fusionne deux rapports
|
||||
self.newMetadata.add(other.newMetadata)
|
||||
self.updatedMetadata.add(other.updatedMetadata)
|
||||
self.unchanged.add(other.unchanged)
|
||||
self.errors.add(other.errors)
|
||||
self.fileReports.add(other.fileReports)
|
||||
|
||||
proc toString*(report: Report): string =
|
||||
## Convertit le rapport en chaîne de caractères formatée
|
||||
let totalProcessed = report.newMetadata.len + report.updatedMetadata.len + report.unchanged.len
|
||||
|
||||
var lines: seq[string] = @[]
|
||||
|
||||
lines.add("=== Résumé du traitement ===")
|
||||
lines.add(fmt"Total des fichiers traités: {totalProcessed}")
|
||||
lines.add(fmt"Métadonnées ajoutées: {report.newMetadata.len}")
|
||||
lines.add(fmt"Métadonnées mises à jour: {report.updatedMetadata.len}")
|
||||
lines.add(fmt"Fichiers inchangés: {report.unchanged.len}")
|
||||
lines.add(fmt"Erreurs: {report.errors.len}")
|
||||
|
||||
if report.newMetadata.len > 0:
|
||||
lines.add("")
|
||||
lines.add("=== Fichiers avec métadonnées ajoutées ===")
|
||||
for file in report.newMetadata:
|
||||
lines.add(fmt"- {file}")
|
||||
|
||||
if report.updatedMetadata.len > 0:
|
||||
lines.add("")
|
||||
lines.add("=== Fichiers avec métadonnées mises à jour ===")
|
||||
for file in report.updatedMetadata:
|
||||
lines.add(fmt"- {file}")
|
||||
|
||||
if report.errors.len > 0:
|
||||
lines.add("")
|
||||
lines.add("=== Erreurs ===")
|
||||
for error in report.errors:
|
||||
lines.add(fmt"- {error}")
|
||||
|
||||
return lines.join("\n")
|
||||
Reference in New Issue
Block a user