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:
2026-04-19 12:56:55 -04:00
commit 18ee8a1cfd
26 changed files with 3255 additions and 0 deletions
+216
View File
@@ -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}"
+209
View File
@@ -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
+692
View File
@@ -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
+69
View File
@@ -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")