Gpt Intern 🇫🇷

Le “stagiaire qui t’envoie jamais chier”

La formule est d’un pote, philosophe de la deuxième période décadente française ; pour vivre cette expérience (un collègue dans la pièce à côté à qui tu peux demander à peu près n’importe quoi même plusieurs fois), j’ai commencé par coder un client Emacs GPT très simple, GPT4Emacs.

Le truc est que gpt4Emacs ne posait qu’une question, éventuellement amendée d’un bout de texte (la sélection région) et qu’il n’est compatible qu’avec un seul modèle (davinci-003) parce qu’en fait 😎 je l’ai écrit sans même vraiment lire la doc de l’API - par ailleurs assez bordélique ; je devrais sérieusement arreter de faire ça, promis ?

Aussi, il se trouve qu’officiellement, selon les règles M/ELPA ☝🏻🧐 le nom d’un module Emacs ne peut pas comporter “emacs” dans son nom. Makes sense, them’s the rules, RTFM et autant écrire carrément un autre module, enter GPT Intern.

Le meilleur client pour chatter GPT sérieusement, laisse-moi te mecspliquer:

Nuits d’ivresse

Depuis qu’OpenAI a sorti GPT-3.5, je passe des nuits entières à cracher du café dans sa face pour lui arracher du sens ; j’ai utilisé trois interfaces : le client web, la ligne de commande (curl) et Cursor. Ce dernier accède à GPT-4 en plus, je ne sais même pas comment il fait en pratique. J’ai pas dormi pendant deux semaines, basiquement.

Le principe de Cursor (un clone de VSCode avec un plugin IA) est de lire le fichier courant et de répondre en fonction de celui-ci, dans une logique conversationnelle, au sens où chaque question-réponse est fonction des questions-réponses précédentes ; quand tu veux une nouvelle conversation, tu cliques sur (Ctrl-l) et il faut le savoir, oui, VSCode n’est pas du tout le parangon ergonomique qu’on pourrait s’imaginer. Je suis très peu déçu ; après des années à tester distraitement les dernières coqueluches des potes, Eclipse, QT Creator, Atom, Sublime Truc (baille) ou whatever, je suis un peu blasé.

Customisable

Well of course: M-x customize-group RET gpt-intern RET.

gpt-intern-customizegpt-intern-customize

De gpt4emacs à GPT Intern

Ce petit module de moins de 200 lignes (licence comprise) marche selon la pure logique Emacs input-parse-buffer-output, tu poses tes questions, elles s’affichent dans le buffer. Le buffer courant n’est pas lu comme dans Cursor (où tu sais pas vraiment dans quel mesure il lit ledit fichier courant, quelle partie, et si le fichier dépasse la limite de token usage ? Hm.) mais uniquement si tu le veux, en activant une région. En sélectionnant un truc, ça veut dire.

De base, une session ressemble à ça:

Q: Ready to code?
R: I am! What language do you have in mind?
Q: ES7?
R: Ok, what do you want to do in JavaScript ES7?
...

GPT répond, ses réponses aussi sont dans le buffer ; à chaque question, (basiquement) tout le buffer est envoyé, en requêtes proprement formatées. Si un truc est sélectionné il est préfixé à la question, et aussi imprimé dans le buffer. Le buffer (en lecture seule, propre) est accessible tout le temps, et tu gardes ta conversation pendant toute la session, tu peux même la sauver. Nouveau buffer, nouvelle conversation.

Fonctions

Il nous a fallu quelques jours heures minutes de réflexion… => Emacs travaille sur du texte, GPT aussi, moi aussi.

La requète

D’abord on va construire une requete avec url-retrieve (async) structurée comme le veut l’API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(defun gpt-intern--query-api (messages)
"Sends a POST request to the GPT API with the given MESSAGES."
(let* ((url-request-method "POST")
(url-request-extra-headers `(("Content-Type" . "application/json; charset=utf-8")
("Authorization" . ,(format "Bearer %s" gpt-intern-api-key))))
(request-body `(("model" . ,gpt-intern-model)
("temperature" . ,gpt-intern-temperature)
("messages" . ,messages))) ;; Use the reversed messages directly
(url-request-data (encode-coding-string (json-encode request-body) 'utf-8))
(coding-system-for-read 'utf-8))
(message "Request Body: %s" (json-encode request-body))
(url-retrieve gpt-intern-api-url
(lambda (status)
(gpt-intern--parse-response status)))))

Ensuite, une fois tout ça prèt, on pose la question ; la conversation s’ouvre sur le coté (ou en bas selon la gémométrie de ton écran) de façon non-intrusive, c’est un buffer view-mode standard que tu peux quitter avec q.

C’est la fonction principale: on vérifie si quelque chose est sélectionné (ou si, dit plus précisément, une région est active) pour l’ajouter avant la réponse de l’utilisateur au prompt “Ask: “ ; mais de toute façon, à ce moment (suis un peu) on lance gpt-intern--parse-buffer pour lire tout le buffer de conversation et mettre cette derniere, correctement formattée, dans la requête suivante. Le wokflow est respecté.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(defun gpt-intern-prompt (&optional beg end query)
"Prompt for a question, prepend selected region if any, and send to GPT API.
When called interactively with a region, BEG and END specify the region bounds."
(interactive (if (use-region-p) (list (region-beginning) (region-end) nil) (list nil nil nil)))
(with-local-quit
(let* ((buffer (get-buffer-create gpt-intern-buffer))
(region-text (when beg (buffer-substring-no-properties beg end)))
(user-input (or query (read-string "Ask: "))))
(with-current-buffer buffer
(rename-buffer gpt-intern-buffer)
(set-buffer-file-coding-system 'utf-8))
(pop-to-buffer buffer) ;; Ici :)
(message "Parsed messages in prompt: %s" (gpt-intern--parse-buffer))
(gpt-intern--display-gpt-chat-buffer "user" user-input)
(message "Current messages: %s" (json-encode messages))
(gpt-intern--query-api (nreverse messages)))))

La réponse

Ensuite on parse la réponse de l’API ; cette fonction est un callback, vu que url-request (dans cette forme anyway) est asynchrone:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(defun gpt-intern--parse-response (status)
"Parse the HTTP response from the GPT API.
Set the JSON parsing settings.
Try to parse the JSON response."
(if (eq (car status) :error)
(message "Error: %s" (cdr status))
(goto-char url-http-end-of-headers)
(let ((json-object-type 'plist)
(json-array-type 'list)
(json-key-type 'keyword))
(condition-case err
(let* ((response (json-read))
(choices (plist-get response :choices))
(first-choice (car choices))
(message (plist-get first-choice :message))
(content (plist-get message :content))
(usage (plist-get response :usage))
(prompt-tokens (plist-get usage :prompt_tokens))
(completion-tokens (plist-get usage :completion_tokens))
(total-tokens (plist-get usage :total_tokens)))
(setq-default global-mode-string (format "Prompt: %s, Completion: %s, Total: %s" prompt-tokens completion-tokens total-tokens)) ;; Set the message in the global-mode-string
(message "Prompt: %s, Completion: %s, Total: %s" prompt-tokens completion-tokens total-tokens)
(gpt-intern--display-gpt-chat-buffer "assistant" content))
(error
(message "Error parsing JSON response: %s" (error-message-string err)))))))

Le buffer de conversation

Puis on affiche les données (et aussi le “Token usage” (prompt, completion, total) dans la modeline, en fonction des préférences) dans le GPT Buffer, c’est là que tout se passe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(defun gpt-intern--display-gpt-chat-buffer (role message)
"Append the message to the gpt-intern-buffer, prefixed with 'Q:' or 'R:'.
The point is placed at the bottom of the buffer after inserting the message."
(push (create-message role message) messages)
(with-current-buffer (get-buffer-create gpt-intern-buffer)
(view-mode -1)
(goto-char (point-max))
(let ((start (point)))
(insert (if (string= role "user") "Q: " "R: ") message "\n\n")
(let ((end (point)))
(put-text-property start end 'face (if (string= role "user") 'gpt-intern-question-face 'gpt-intern-answer-face))))
(set-window-point (get-buffer-window) (point-max))
(view-mode 1)
(unless (string= role "user") ;; Only prompt for a new question after displaying an assistant's message
(gpt-intern-prompt)))) ; Et la conversation continue.

Et la conversation continue. Toute la nuit :|