Aller au contenu

Recherche et Transformation

AI generated translation.

Ce script est une évolution de la fonctionnalité « rechercher et remplacer » d’un éditeur de texte, où l’étape de « remplacement » a été remplacée par une transformation LLM.

Il peut être utile d’appliquer en batch des transformations de texte qui ne sont pas facilement réalisables avec des expressions régulières.

Par exemple, lorsque GenAIScript a ajouté la possibilité d’utiliser une chaîne de commande sous forme de chaîne de caractères dans le commande exec, nous avons dû convertir tout script utilisant

host.exec("cmd", ["arg0", "arg1", "arg2"])

en

host.exec(`cmd arg0 arg1 arg2`)`

Il est possible de faire correspondre cet appel de fonction avec une expression régulière

host\.exec\s*\([^,]+,\s*\[[^\]]+\]\s*\)

mais il n’est pas facile de formuler la chaîne de remplacement… à moins que vous ne puissiez la décrire en langage naturel :

Convert the call to a single string command shell in TypeScript

Voici quelques exemples de transformations où le LLM a correctement géré les variables.

  • concaténer les arguments d’un appel de fonction en une seule chaîne
const { stdout } = await host.exec("git", ["diff"])
const { stdout } = await host.exec(`git diff`)
  • concaténer les arguments et utiliser la syntaxe ${} pour interpoler les variables
const { stdout: commits } = await host.exec("git", [
"log",
"--author",
author,
"--until",
until,
"--format=oneline",
])
const { stdout: commits } = await host.exec(`git log --author ${author} --until ${until} --format=oneline`)

L’étape de recherche est réalisée avec workspace.grep qui permet de rechercher efficacement un motif dans des fichiers (c’est le même moteur de recherche qui alimente la recherche de Visual Studio Code).

const { pattern, globs } = env.vars
const patternRx = new RegExp(pattern, "g")
const { files } = await workspace.grep(patternRx, { globs })

La seconde étape consiste à appliquer l’expression régulière au contenu du fichier et à pré-calculer la transformation LLM de chaque correspondance en utilisant un prompt en ligne.

const { transform } = env.vars
...
const patches = {} // map of match -> transformed
for (const file of files) {
const { content } = await workspace.readText(file.filename)
for (const match of content.matchAll(patternRx)) {
const res = await runPrompt(
(ctx) => {
ctx.$`
## Task
Your task is to transform the MATCH with the following TRANSFORM.
Return the transformed text.
- do NOT add enclosing quotes.
## Context
`
ctx.def("MATCHED", match[0])
ctx.def("TRANSFORM", transform)
},
{ label: match[0], system: [], cache: "search-and-transform" }
)
...

Comme le LLM décide parfois d’entourer la réponse de guillemets, nous devons les supprimer.

...
const transformed = res.fences?.[0].content ?? res.text
patches[match[0]] = transformed

Enfin, avec les transformations pré-calculées, nous appliquons un remplacement regex final pour corriger l’ancien contenu du fichier avec les chaînes transformées.

const newContent = content.replace(
patternRx,
(match) => patches[match] ?? match
)
await workspace.writeText(file.filename, newContent)
}

Le script prend trois paramètres : un glob de fichiers, un motif à rechercher, et une transformation LLM à appliquer. Nous déclarons ces paramètres dans les métadonnées du script et les extrayons de l’objet env.vars.

script({ ...,
parameters: {
glob: {
type: "string",
description: "The glob pattern to filter files",
default: "*",
},
pattern: {
type: "string",
description: "The text pattern (regular expression) to search for",
},
transform: {
type: "string",
description: "The LLM transformation to apply to the match",
},
},
})
const { pattern, glob, transform } = env.vars
st.genai.mts
script({
title: "Search and transform",
description: "Search for a pattern in files and apply a LLM transformation the match",
parameters: {
glob: {
type: "string",
description: "The glob pattern to filter files",
},
pattern: {
type: "string",
description: "The text pattern (regular expression) to search for",
},
transform: {
type: "string",
description: "The LLM transformation to apply to the match",
},
},
});
let { pattern, glob, transform } = env.vars;
if (!glob) glob = (await host.input("Enter the glob pattern to filter files (default: *)")) || "*";
if (!pattern) pattern = await host.input("Enter the pattern to search for (regular expression)");
if (!pattern) cancel("pattern is missing");
const patternRx = new RegExp(pattern, "g");
if (!transform) transform = await host.input("Enter the LLM transformation to apply to the match");
if (!transform) cancel("transform is missing");
const { files } = await workspace.grep(patternRx, { glob });
// cached computed transformations
const patches = {};
for (const file of files) {
console.log(file.filename);
const { content } = await workspace.readText(file.filename);
// skip binary files
if (!content) continue;
// compute transforms
for (const match of content.matchAll(patternRx)) {
console.log(` ${match[0]}`);
if (patches[match[0]]) continue;
const res = await runPrompt(
(_) => {
_.$`
## Task
Your task is to transform the MATCH with the following TRANSFORM.
Return the transformed text.
- do NOT add enclosing quotes.
## Context
`;
_.def("MATCHED", match[0]);
_.def("TRANSFORM", transform, {
detectPromptInjection: "available",
});
},
{
label: match[0],
system: ["system.assistant", "system.safety_jailbreak", "system.safety_harmful_content"],
cache: "search-and-transform",
},
);
const transformed = res.fences?.[0].content ?? res.text;
if (transformed) patches[match[0]] = transformed;
console.log(` ${match[0]} -> ${transformed ?? "?"}`);
}
// apply transforms
const newContent = content.replace(patternRx, (match) => patches[match] ?? match);
// save results if file content is modified
if (content !== newContent) await workspace.writeText(file.filename, newContent);
}

Pour exécuter ce script, vous pouvez utiliser l’option --vars pour passer le motif et la transformation.

Fenêtre de terminal
genaiscript st --vars 'pattern=host\.exec\s*\([^,]+,\s*\[[^\]]+\]\s*\)' 'transform=Convert the call to a single string command shell in TypeScript'