Пример с Copilot, который мы рассмотрим в этой статье, носит более демонстрационный, нежели какой-то практический характер. Однако вполне возможно, что какие-то из наработок вы сможете в будущем подстроить под себя и применить при решении реальных задач.
Итак, ставим для себя следующую задачу:
1. Хотим, чтобы у нас была возможность прямо в тексте модуля написать текстовое описание некой логики, а при нажатии специального сочетания клавиш это описание трансформировалось бы в реальный код.
2. Хотим, чтобы у нас была возможность при нажатии специальной горячей клавиши получать от ИИ анализ выделенного участка кода.
Решать эту задачу будем с помощью следующих инструментов:
- AutoHotkey - это скриптовый язык программирования, предназначенный специально для назначения горячих клавиш, а также эмуляции нажатия кнопок клавиатуры и мыши. Предварительно скачиваем и устанавливаем с официального сайта версию 2.0.
- OneScript - благодаря нему всю основную логику, такую как отправка HTTP-запросов и обработка ответов, мы сможем написать на всем нам хорошо знакомом и понятном 1С. Тоже скачиваем и устанавливаем.
- Gemini - в качестве ИИ модели можно было бы выбрать и ChatGPT, но Gemini оказался предпочтительнее, просто потому что у него есть бесплатный тариф. Переходим в Google AI Studio и генерируем API Key.
Создаем пустой каталог и открываем его с помощью Visual Studio Code, где и будем вести разработку.
Назначаем горячие клавиши с помощью AutoHotkey
Первое, что нужно сделать, это отловить нажатие горячих клавиш. Для этого создаем файл с расширением .ahk. Например, 1c-designer-copilot.ahk.
По умолчанию, Visual Studio Code не умеет работать с файлами расширения .ahk, поэтому чтобы получать синтактические подсказки, а также подсветку кода устанавливаем специальное расширение AutoHotkey Plus Plus.
Предполагается, что для генерации кода пользователь должен сначала написать текстовое описание кода, который он хочет получить, а затем должен это описание выделить и нажать специальное сочетание горячих клавиш. При анализе кода он также выделяет участок кода и нажимает уже на другое сочетание клавиш. Для себе определяем так, что за генерацию кода будет отвечать сочетание Ctrl+1, а за анализ Ctrl+2. Для этого создаем две процедуры.
^1:: {
...
}
^2:: {
...
}
Здесь символ "^" означает Ctrl. С полным списком обозначений клавиш можно ознакомиться в документации.
Что происходит дальше? Дальше в этих обработчиках мы должны прописать следующую логику:
1. Нам нужно скопировать выделенные тексты. Т.е. симулировать нажатие Ctrl+C.
2. Дальше нужно запустить нужный скрипт OneScript: СкриптСозданияКода.os для генерации кода и СкриптАнализаКода.os для его анализа. Подробнее о них чуть позже.
3. Эти скрипты берут на себя задачу взаимодействия с Gemini и возвращают полученный ответ обратно на сторону AutoHotkey.
4. Далее, если мы вызвали генерацию кода, то выделенное описание заменяем на полученный от Gemini код, т.е. симулируем нажатие Ctrl+V.
4. Если же мы вызывали анализ кода, то открываем этот анализ в виде HTML-документа.
Но вот проблема: одних только симуляций нажатия Ctrl+C и Ctrl+V нам окажется недостаточно. Почему? Дело в том, что OneScript из под коробки не умеет работать с буфером обмена. Поэтому пришлось пойти обходным путем, а именно создать промежуточный текстовый файл, в который мы запишем скопированное значение. Иными словами, AutoHotkey и OneScript будут общаться между собой через этот текстовый файл. AutoHotkey будет записывать в этот файл скопированное пользователем значение, а OneScript записывать туда значение ответа. В моем случае этот файл я назвал clipboard.txt и поместил его в папку temp внутри проекта.
Чтобы все таки получить возможность работы с буфером обмена в OneScript можно воспользоваться сторонней библиотекой.
Конечный результат выглядит так:
#Requires AutoHotkey v2.0
SaveTextToFile(Path, Content) {
File := FileOpen(A_ScriptDir Path, "w", "UTF-8")
File.Write(Content)
File.Close()
}
FileContent(Path) {
File := FileOpen(A_ScriptDir Path, "r", "UTF-8")
Content := File.Read(unset)
File.Close()
return Content
}
^1:: {
ClipboardFilePath := "\temp\clipboard.txt"
A_Clipboard := ""
Send("^c")
ClipWait(1, 1)
Promt := A_Clipboard
SaveTextToFile(ClipboardFilePath, Promt)
ScriptPath := A_ScriptDir "\src\Модули\СкриптСозданияКода.os"
Disk := "/c"
RunWait("cmd " Disk " oscript " ScriptPath)
A_Clipboard := FileContent(ClipboardFilePath)
Send("^v")
}
^2:: {
A_Clipboard := ""
Send("^c")
ClipWait(1, 1)
Content := A_Clipboard
SaveTextToFile("\temp\clipboard.txt", Content)
ScriptPath := A_ScriptDir "\src\Модули\СкриптАнализаКода.os"
Disk := "/c"
RunWait("cmd " Disk " oscript " ScriptPath)
}
Несколько моментов, которые здесь нужно пояснить:
1. A_Clipboard - это специальный объект AutoHotkey, представляющий собой содержимое буфера обмена.
2. С помощью методов вроде Send("^c") и Send("^v") мы симулируем нажатие на клавиши Ctrl+C и Ctrl+V соответственно. Обратите внимание, что дальше за ними следует вызов ClipWait(1, 1). Этот встроенный метод позволяет дождаться окончания вызова предыдущей команды, иначе код переходил бы к следующей строке не дожидаясь фактического помещения данных в буфер.
3. Метод RunWait используется для запуска скриптов OneScript с ожиданием их завершения.
4. Вспомогательные функции SaveTextToFile и FileContent созданы специально для взаимодействия с промежуточным файлом clipboard.txt.
Тут же в скобках заметим, что скрипты .ahk можно легко скомпилировать в .exe с помощью специальной формы, доступной после установки.
Пишем скрипты на OneScript
Перед тем как приступить к работе над написанием скриптов, установим еще одно расширение VS code, а именно "Language 1C (BSL)", которое включит подсказки и подсветку синтаксиса уже для файлов с расширением .os. Также если планируете делать отладку своего кода на OneScript, то можете установить еще и расширение OneScript Debug (BSL).
Как уже оговаривалось выше, проект будет содержать в себе два отдельных скрипта на OneScript, запускаемых из под AutoHotkey - "СкриптСозданияКода.os" и "СкриптАнализаКода.os". Но перед тем как приступить к разбору содержимого этих скриптов, взглянем немного на то, как в итоге будет выглядеть структура папок проекта в целом.
Тут наибольший интерес прямо сейчас для нас представляет содержимое папки "src", в которую помещено все, что связано с OneScript. Внутри папки "src" расположена папка "Модули", а уже в ней размещены те самые скрипты. Но помимо этого папка "src" также содержит в себе подпапки "utils" и "gemini". Внутри папки "utils" есть еще одна папка "Модули", с файлами "РаботаСJSON.os" и "РаботаСФайлами.os", а внутри "gemini" папка "Классы" с одним единственным файлом "Gemini.os".
Такая структура не прихоть, а следование конвенции OneScript. В общем случае ее можно выразить так: на верхнем уровне располагается папка, имя которой отражает суть реализуемого функционала, а далее, уровнем ниже, идут подпапки "Классы" и/или "Модули", каждая из которых в свою очередь содержит файлы с расширением .os. Тут нужно быть несколько осторожным, потому что при неправильной организации файлов проекта можно наткнуться на ошибки вроде "Обнаружены циклические зависимости" или "Конструктор не найден". Подробнее о структуре можно прочитать здесь.
Теперь, собственно, о том, чем отличается содержимое папок "Модули" от папок "Классы".
Модули в терминологии классического 1С ближе всего к понятию общих модулей. В них расположены процедуры и функции выполняющие непосредственную логику приложения. В нашем случае, помимо основных скриптов, у нас есть еще модули "РаботаСJSON.os" и "РаботаСФайлами.os".
Классы же это особая сущность, которой нет в платформе 1С (хотя кто-то может сравнить с программной инициализацией обработки). В OneScript они позволяют нам создавать свои собственные конструкции с помощью ключевого слова "Новый". Например, точно так же, как мы создаем, допустим, новую структуру, мы можем создать и новый экземпляр некоего своего собственного класса. Методы и свойства этого класса мы определяем самостоятельно.
Возвращаясь к рассматриваемому примеру, можем чуть подробнее остановиться на классе "Gemini":
APIKey = "123";
Gemini = Новый Gemini(APIKey);
Gemini.СформироватьКодПоОписанию("Напиши функцию возведения в степень");
Тут все просто и понятно: мы создали экземпляр класса Gemini и сразу же при его инициализации определили используемый им ключ API. А затем вызвали имеющийся у этого класса метод "СформироватьКодПоОписанию".
А теперь рассмотрим этот же класс под капотом, т.е. содержимое файла "Gemini.os":
Перем ТокенAPI Экспорт;
Процедура ПриСозданииОбъекта(ЗначениеТокена)
ТокенAPI = ЗначениеТокена;
КонецПроцедуры
Функция СформироватьКодПоОписанию(Промт) Экспорт
//...
КонецФункции
Как видим, свойства класса объявляются в виде переменных модуля. В случае класса Gemini у него есть только одно свойство "ТокенAPI". Далее при инициализации нового экземпляра класса у нас отработало событие "ПриСозданииОбъекта", т.е. это именно та процедура, которая вызывается при исполнении строчки "Новый Gemini(APIKey)". А уже далее с помощь экспортной функции объявлен метод класса "СформироватьКодПоОписанию".
В итоговом варианте класс Gemini.os выглядит следующим образом:
#Использовать "../../utils"
// BSLLS:ExportVariables-off
Перем ТокенAPI;
// BSLLS:ExportVariables-on
Процедура ПриСозданииОбъекта(ЗначениеТокена)
ТокенAPI = ЗначениеТокена;
КонецПроцедуры
Функция АнализироватьКод(Код) Экспорт
Сообщить("Анализируем код...");
Промт = СтрШаблон("Объясни, что делает этот код 1С: %1", Код);
ПараметрыЗапросаHTTP = НоваяСтруктураПараметровЗапросаHTTP();
ПараметрыЗапросаHTTP.АдресРесурса = СтрШаблон("/v1beta/models/gemini-1.5-flash:generateContent?key=%1", ТокенAPI);
ПараметрыЗапросаHTTP.HTTPМетод = "POST";
ПараметрыЗапросаHTTP.Заголовки.Вставить("Content-Type", "application/json");
ПараметрыЗапросаHTTP.ТелоЗапроса = ТелоЗапросаКGemini(Промт);
Возврат ВыполнитьЗапросHTTP(ПараметрыЗапросаHTTP);
КонецФункции
Функция СформироватьКодПоОписанию(Промт) Экспорт
Сообщить("Пишем код...");
Промт = СтрШаблон("Напиши код 1С по описанию. Никакого другого текста - только код.
|%1", Промт);
ПараметрыЗапросаHTTP = НоваяСтруктураПараметровЗапросаHTTP();
ПараметрыЗапросаHTTP.АдресРесурса = СтрШаблон("/v1beta/models/gemini-1.5-flash:generateContent?key=%1", ТокенAPI);
ПараметрыЗапросаHTTP.HTTPМетод = "POST";
ПараметрыЗапросаHTTP.Заголовки.Вставить("Content-Type", "application/json");
ПараметрыЗапросаHTTP.ТелоЗапроса = ТелоЗапросаКGemini(Промт);
Возврат ВыполнитьЗапросHTTP(ПараметрыЗапросаHTTP);
КонецФункции
Функция ВыполнитьЗапросHTTP(ПараметрыЗапроса)
Результат = НоваяСтруктураРезультатаЗапроса();
HTTPЗапрос = Новый HTTPЗапрос;
HTTPЗапрос.Заголовки = ПараметрыЗапроса.Заголовки;
HTTPЗапрос.АдресРесурса = ПараметрыЗапроса.АдресРесурса;
HTTPЗапрос.УстановитьТелоИзСтроки(ПараметрыЗапроса.ТелоЗапроса);
Таймаут = 30;
АдресРесурса = "https://generativelanguage.googleapis.com";
HTTPСоединение = Новый HTTPСоединение(АдресРесурса, , , , , Таймаут);
СтатусУспешно = 200;
Попытка
HTTPОтвет = HTTPСоединение.ВызватьHTTPМетод(ПараметрыЗапроса.HTTPМетод, HTTPЗапрос);
Если HTTPОтвет.КодСостояния = СтатусУспешно Тогда
JSON = HTTPОтвет.ПолучитьТелоКакСтроку();
ДанныеОтвета = РаботаСJSON.ЗначениеИзJSON(JSON, Истина);
Результат.Ответ = ИзвлечьТекстИзОтветаGemini(ДанныеОтвета);
Иначе
JSONОшибки = HTTPОтвет.ПолучитьТелоКакСтроку();
Результат.Ответ = СтрШаблон("Произошла ошибка: %1", JSONОшибки);
КонецЕсли;
Исключение
Результат.ТекстОшибки = СтрШаблон("Произошла ошибка: %1", ОписаниеОшибки());
КонецПопытки;
Возврат Результат;
КонецФункции
Функция ТелоЗапросаКGemini(ТекстЗапроса)
СтруктураЗапроса = Новый Структура("text", ТекстЗапроса);
СписокСоставных = Новый Массив;
СписокСоставных.Добавить(СтруктураЗапроса);
Контент = Новый Структура("parts", СписокСоставных);
СписокКонтента = Новый Массив;
СписокКонтента.добавить(Контент);
ДанныеЗапроса = Новый Структура("contents", СписокКонтента);
Возврат РаботаСJSON.JSONИзЗначения(ДанныеЗапроса);
КонецФункции
Функция ИзвлечьТекстИзОтветаGemini(ДанныеОтвета)
Возврат ДанныеОтвета["candidates"][0]["content"]["parts"][0]["text"];
КонецФункции
Функция НоваяСтруктураПараметровЗапросаHTTP()
ПараметровЗапросаHTTP = Новый Структура;
ПараметровЗапросаHTTP.Вставить("АдресРесурса", "");
ПараметровЗапросаHTTP.Вставить("HTTPМетод", "GET");
ПараметровЗапросаHTTP.Вставить("Заголовки", Новый Соответствие);
ПараметровЗапросаHTTP.Вставить("ТелоЗапроса", "");
Возврат ПараметровЗапросаHTTP;
КонецФункции
Функция НоваяСтруктураРезультатаЗапроса()
Результат = Новый Структура;
Результат.Вставить("Ответ", "");
Результат.Вставить("ТекстОшибки", "");
Возврат Результат;
КонецФункции
Последнее, на что нужно обратить внимание, это директива #Использовать в самом начале модуля. Дело в том, что в общем случае каждый из файлов .os ничего не знает о содержимом других таких же файлов. Поэтому чтобы воспользоваться функционалом, реализованном в другом файле, нужно предварительно объявить его использование посредством данной конструкции.
Генерация кода по запросу
Теперь рассмотрим содержимое скрипта, отвечающего за запрос к Gemini по генерации кода и последующую обработку полученного ответа.
Напомню, что из-за существующих ограничений вместо буфера обмена придется считать содержимое файла "clipboard.txt".
Также стоит обратить внимание, что классу Gemini нужно передать значение токена API. Явным образом прописывать токен в коде точно не стоит, поэтому вместо этого мы вынесем его в отдельный файл "env.json", а сам этот файл добавим в .gitignore проекта:
{
"geminiApiKey": "API_KEY"
}
В качестве хорошей альтернативы можно объявить переменную среды, благо OneScript поддерживает все необходимые для этого методы.
Итоговый текст нашего скрипта будет выглядеть следующим образом:
#Использовать "../gemini"
#Использовать "../utils"
JSON = РаботаСФайлами.СодержимоеТекстовогоДокумента(ТекущийСценарий().Каталог + "\..\..\env.json");
env = РаботаСJSON.ЗначениеИзJSON(JSON);
ПутьКФайлу = ТекущийСценарий().Каталог + "\..\..\temp\clipboard.txt";
Промт = РаботаСФайлами.СодержимоеТекстовогоДокумента(ПутьКФайлу);
Gemini = Новый Gemini(env.geminiApiKey);
Результат = Gemini.СформироватьКодПоОписанию(Промт);
РаботаСФайлами.ЗаписатьТекстовыйДокумент(ПутьКФайлу, Результат.Ответ);
Как вы можете видеть, полученный от Gemini ответ записывается в последствии все в тот же файл "clipboard.txt". После отработки этого кода продолжится исполнение скрипта AutoHotkey, который прочитает этот файл, поместит его содержимое в буфер обмена и симулирует нажатие клавиш Ctrl+V.
Итак, дважды кликаем на файл "1c-designer-copilot.ahk", чтобы включить распознавание нажатия горячих клавиш, и можно проверять результат:
Нда...Код, конечно, оставляет желать лучшего. Но, как бы то ни было, главное это то, что наши скрипты отработали так, как и было задумано. Возможно, в будущем Gemini будет лучше разбираться в коде на 1С, а пока рассмотрим скрипт по анализу кода.
Анализ кода
Принципиально содержимое второго скрипта отличается от первого только в той его части, которая отвечает за обработку полученного ответа. Если в предыдущем примере мы просто помещали текст ответа в буфер обмена, то ответ анализа кода мы поместим в HTML-документ, который затем сразу и запустим. Шаблон этого документа заранее подготовлен и помещен в каталог temp. Наша задача состоит только в том, чтобы в нужный участок этого документа, а именно между специально размещенными комментариями "//++ Mardown" и "//-- Mardown" вставить ответ от Gemini. Еще один важный момент: все ответы от Gemini мы получаем в формате Markdown, поэтому при загрузке HTML-документа для сохранения форматирования текста ответа запускается функция на JavaScript "convertMarkdown", преобразующая содержимое переменной markdown в HTML.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Анализ кода</title>
<script type="text/javascript">
function convertMarkdown() {
//++ Mardown
const markdown = "Привет, Мир!";
//-- Mardown
const html = markdown
.replace(/^### (.+)$/gm, "<h3>$1</h3>")
.replace(/^## (.+)$/gm, "<h2>$1</h2>")
.replace(/^# (.+)$/gm, "<h1>$1</h1>")
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
.replace(/\*(.+?)\*/g, "<em>$1</em>")
.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')
.replace(/\n/g, "<br>");
document.getElementById("output").innerHTML = html;
}
window.onload = convertMarkdown;
</script>
</head>
<body>
<div id="output"></div>
</body>
</html>
Текст самого скрипта выглядит так:
#Использовать "../gemini"
#Использовать "../utils"
JSON = РаботаСФайлами.СодержимоеТекстовогоДокумента(ТекущийСценарий().Каталог + "\..\..\env.json");
env = РаботаСJSON.ЗначениеИзJSON(JSON);
КодКАнализу = РаботаСФайлами.СодержимоеТекстовогоДокумента(ТекущийСценарий().Каталог + "\..\..\temp\clipboard.txt");
Gemini = Новый Gemini(env.geminiApiKey);
Результат = Gemini.АнализироватьКод(КодКАнализу);
ПутьКФайлуHTML = ТекущийСценарий().Каталог + "\..\..\temp\code-explanation.html";
СодержимоеHTML = РаботаСФайлами.СодержимоеТекстовогоДокумента(ПутьКФайлуHTML);
КомментарийОткрытияMarkdown = "//++ Mardown";
КомментарийЗакрытияMarkdown = "//-- Mardown";
ПозицияНачалаMarkdown = СтрНайти(СодержимоеHTML, КомментарийОткрытияMarkdown);
ПозицияЗавершенияMarkdown = СтрНайти(СодержимоеHTML, КомментарийЗакрытияMarkdown);
ДлинаКомментария = СтрДлина(КомментарийОткрытияMarkdown);
ЧислоСимволовКЗамене = ПозицияЗавершенияMarkdown - ДлинаКомментария - ПозицияНачалаMarkdown;
ТекстОтвета = Результат.Ответ;
ТекстОтвета = СтрЗаменить(ТекстОтвета, Символы.ПС, "\n");
ТекстОтвета = СтрЗаменить(ТекстОтвета, "'", """");
ТекстКЗамене = Сред(СодержимоеHTML, ПозицияНачалаMarkdown + ДлинаКомментария, ЧислоСимволовКЗамене);
ТекстКподстановке = СтрШаблон("
|const markdown = '%1';
|", ТекстОтвета);
СодержимоеHTML = СтрЗаменить(СодержимоеHTML, ТекстКЗамене, ТекстКподстановке);
РаботаСФайлами.ЗаписатьТекстовыйДокумент(ПутьКФайлуHTML, СодержимоеHTML);
ЗапуститьПриложение(ПутьКФайлуHTML);
Проверяем:
Уже интереснее. С анализом кода Gemini справился явно лучше, чем с его генерацией.
Заключение
Практическая польза от такой доработки, конечно, будет невелика. Особенно сейчас, когда уровень работы популярных LLM с 1С оставляет желать лучшего. Тем не менее, как и говорилось в начале этой статьи, такой цели мы и не преследовали. Для нас было важно опробовать новые для нас инструменты. Вполне возможно, что какие-то из приведенных здесь наработок вы сможете использовать по-своему уже в реальных проектах.
Если вам понравилась статья, то не забудьте поставить плюс, а также звездочку репозиторию на GitHub.
Спасибо за внимание.
Протестировано на платформе 8.3.25.1501