Aller au contenu principal

ChatGPT peut il m'assister ? - Partie 2

· 6 minutes de lecture
Stéphane ROBERT
Consultant DevOps

Dans le précédent billet, nous avons pu voir que chatGPT n'est pas si magique que cela. En effet, il ne suit pas forcément les bonnes pratiques et surtout ne propose pas toujours les solutions adaptées. La plus grosse difficulté est de trouver quelles informations lui fournir pour qu'il produise du contenu proche d'une solution acceptable.

Dans ce billet, je vais chercher des solutions pour fournir à chatGPT un template correspondant à mon besoin avec les options que j'attends et respectant l'utilisation des FQCN.

Comment stocker les templates ?

L'idée est de décrire les taches avec des phrases proches du langage naturel. Ex: Copy a file /path/to/test to the path /path/to/ with mode 0666 owner test group tests.

J'ai constitué une base de données regroupant tous les modules Ansible indexés sur leur phrase de description. Pour constituer cette base, j'utilise mon outil qui génère les snippets vscode.

Exemple de template :

{
    "execute command on remote node create remove lock": {
        "tasks": [
            {
                "name": "Execute commands on targets",
                "ansible.builtin.command": {
                    "chdir": "/tmp/test",
                    "cmd": "/usr/bin/make_database.sh db_user db_name && touch /var/run/test.lock",
                    "creates": "/var/run/test.lock",
                    "removes": "/var/run/test.lock"
                }
            }
        ]
    },
}

Et non ce n'est pas une base de données, mais un simple fichier json. Pour info mon fichier contient 650 templates regroupant les collections ansible les plus courantes.

Comment identifier le bon template ?

Maintenant que j'ai constitué ma base de templates, je dois trouver une solution permettant de trouver une similarité entre la phrase décrivant la tache et celle des modules.

Je me suis tourné vers Gensim, une bibliothèque logicielle Python de topic modelling.

#!/usr/bin/env python3

import os
import re
import sys
from rich import print_json
from gensim import corpora, models, similarities
import jieba
import json

with open("snippets.json") as json_file:
    snippets = json.load(json_file)
keyword = sys.argv[1]
analysed_snippets = [jieba.lcut(snippet.lower()) for snippet in snippets]
dictionary = corpora.Dictionary(analysed_snippets)
feature_cnt = len(dictionary.token2id)
corpus = [dictionary.doc2bow(snippet) for snippet in analysed_snippets]
tfidf = models.TfidfModel(corpus)
kw_vector = dictionary.doc2bow(jieba.lcut(keyword))
index = similarities.SparseMatrixSimilarity(tfidf[corpus], num_features=feature_cnt)
sim = list(index[tfidf[kw_vector]])
index = sim.index(max(sim))
snippet = snippets[list(snippets)[index]]
print_json (json.dumps(snippet))

On lance notre premier test en ciblant le module ansible ci-dessus.

./test.py "execute commande /usr/bin/start.sh and creates lock /var/run/test.lock"
Building prefix dict from the default dictionary ...
Loading model from cache /tmp/jieba.cache
Loading model cost 0.700 seconds.
Prefix dict has been built successfully.
{
  "tasks": [
    {
      "name": "Execute commands on targets",
      "ansible.builtin.command": {
        "chdir": "/tmp/test",
        "cmd": "/usr/bin/make_database.sh db_user db_name",
        "creates": "/var/run/test.lock",
        "removes": "/var/run/test.lock"
      }
    }
  ]
}

Bingo c'est le résultat attendu. Le plus long va être de corriger les descriptions de tous mes modules.

On enrichit notre demande à ChatGPT

Maintenant le code minimal pour poser une question à chatGPT :

#!/usr/bin/env python3

import os
import re
import sys
from rich import print_json, print
from gensim import corpora, models, similarities
import jieba
import json
import ruamel.yaml
from rich.console import Console
from rich.text import Text
from rich.markdown import Markdown
import openai


with open("snippets.json") as json_file:
    snippets = json.load(json_file)
keyword = sys.argv[1]
analysed_snippets = [jieba.lcut(snippet.lower()) for snippet in snippets]
dictionary = corpora.Dictionary(analysed_snippets)
feature_cnt = len(dictionary.token2id)
corpus = [dictionary.doc2bow(snippet) for snippet in analysed_snippets]
tfidf = models.TfidfModel(corpus)
kw_vector = dictionary.doc2bow(jieba.lcut(keyword))
index = similarities.SparseMatrixSimilarity(tfidf[corpus], num_features=feature_cnt)
sim = list(index[tfidf[kw_vector]])
index = sim.index(max(sim))
snippet = snippets[list(snippets)[index]]
openai.api_key = os.getenv("OPENAI_KEY")
system_message = "You are an ansible expert. Use FQCN. No comments. Json:" % (snippet)
system_user = "You must write a ansible task ansible en with all template options to %s #template 1 %s. No comments. Json:" % (sys.argv[1],snippet)
print("Demande envoyé à ChatGPT: \nsystem : %s\nuser: %s" % (system_message, system_user))
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0301",
    messages=[
        {
            "role": "system",
            "content": system_message,
        },
        {"role": "user", "content": system_user},
    ],
    temperature = 0.5
)
chatgpt_response = response["choices"][0]["message"]["content"]
chatgpt_response = re.sub(r'\s(?={^\{\}}*}})', "_", chatgpt_response)
tokens = response['usage']['total_tokens']
console = Console()
print("ChatGPT réponds:")
yaml = ruamel.yaml.YAML()
yaml.indent(sequence=4, offset=2)
try:
    yaml.dump(json.loads(chatgpt_response)["tasks"], sys.stdout)
except:
    print(chatgpt_response)
print("Tokens utilisés %i" % tokens)

On lance notre premier test :

./test.py "execute commande /usr/bin/start.sh and creates lock /var/run/test.lock"
Building prefix dict from the default dictionary ...
Loading model from cache /tmp/jieba.cache
Loading model cost 0.700 seconds.
Prefix dict has been built successfully.
Demande envoyé à ChatGPT:
system : You are an ansible expert. Use FQCN. No comments. Json:
user: You must write a ansible task ansible en with all template options to execute commande /usr/bin/start.sh and creates lock /var/run/test.lock #template 1 {'tasks': [{'name': 'Execute commands on targets',
'ansible.builtin.command': {'chdir': '/tmp/test', 'cmd': '/usr/bin/make_database.sh db_user db_name', 'creates': '/var/run/test.lock', 'removes': '/var/run/test.lock'}}]}. No comments. Json:
ChatGPT réponds:
  - name: Execute commands on targets
    ansible.builtin.command:
      chdir: /tmp/test
      cmd: /usr/bin/start.sh
      creates: /var/run/test.lock
Tokens utilisés 190

Nickel, il respecte les FQCN, fourni les options attendues.

Un autre exemple :

./test.py "Copy file on remote from source /path/to/test to /path/to/ mode 0666 owner test group tests"
Building prefix dict from the default dictionary ...
Loading model from cache /tmp/jieba.cache
Loading model cost 0.687 seconds.
Prefix dict has been built successfully.
Demande envoyé à ChatGPT:
system : You are an ansible expert. Use FQCN. No comments. Json:
user: You must write a ansible task ansible en with all template options to Copy file on remote from source /path/to/test to /path/to/ mode 0666 owner test group tests #template 1 {'tasks': [{'name': 'Copy files to remote
locations', 'ansible.builtin.copy': {'backup': False, 'dest': '/tmp/test', 'follow': False, 'force': True, 'group': 'group', 'mode': '0755', 'owner': 'user', 'remote_src': False, 'src': '/tmp/test', 'unsafe_writes': False}}]}. No
comments. Json:
ChatGPT réponds:
  - name: Copy files to remote locations
    ansible.builtin.copy:
      backup: false
      dest: /path/to/
      follow: false
      force: true
      group: tests
      mode: '0666'
      owner: test
      remote_src: false
      src: /path/to/test
      unsafe_writes: false
Tokens utilisés 262

La suite

Bon, j'obtiens des résultats satisfaisants. On va passer au développement d'une CLI prenant soit une tache, soit une liste de tâches via un fichier.

Ce fichier est une suite de taches et de blocks Ansible. J'utiliserai ce format :

- task: Install package htop, nginx and net-tools with generic module
- task: Copy file from local file /tmp/toto to remote /tmp/titi set mode 0666 owner bob group www
  register: test
- name: A block
  when: test.rc == 0
  block:
    - task: wait for port 6300 on localhost timeout 25
  rescue:
    - task: Execute command to start /opt/application/start.sh and creates /var/run/test.lock
- task: Download file from https://tmp.io/test/ set mode 0640 and force true

J'ai déjà bien avancé et je publierai la cli d'ici à quelques jours. Le temps de corriger quelques bugs et renforcer le fonctionnement. A bientôt...