Верстаем своего ИИ CLI агента на… GOLANG???

от автора

Все уже слышали про Gemini CLI, который позволяет взаимодействовать с мощной gemini 2.5 прямо из командной строки. Это удобно, открывает массу возможностей. Но что, если мы захотим не просто использовать готовое решение, а понять, как оно работает изнутри? А если у нас вообще нету VPN для сервисов гугла? Или, что еще интереснее, создать свой собственный, пусть и экспериментальный, аналог? Чем мы хуже? Давайте сверстаем свой вариант на… GOLANG?

Да, именно так. Мы не будем писать продакшн-готовый инструмент, который заменит собой все существующие CLI. Наша цель — эксперимент, погружение в процесс, понимание того, как можно подружить Go, консоль и большую языковую модель.

Моя идея проста: создать относительно не сложного CLI-агента, который будет слушать наши запросы, обращаться к AI за советом или командой, предлагать эту команду нам для выполнения, а затем, после нашего подтверждения, выполнять её и анализировать результат. Если команда завершится ошибкой, AI попытается понять, что пошло не так, и предложит решение. Это позволит нам не только получать ответы, но и автоматизировать рутинные задачи, не выходя из терминала.

Если вам интересен процесс и вы хотите следить за дальнейшими материалами, буду признателен за подписку на мой телеграм-канал. Там я публикую полезныe материалы по разработке, разборы сложных концепций, советы как быть продуктивным и конечно же отборные мемы: https://t.me/nullPointerDotEXE.

В качестве зависимостей будем использовать github.com/fatih/color для красивого вывода. Поэтому сразу сделайте go get github.com/fatih/color

Шаг 1. Объявляем переменные, структуры, константы и инициализируемся

var ( successColor = color.New(color.FgGreen).Add(color.Bold).SprintFunc() borderColor  = color.New(color.FgWhite).Add(color.Bold).SprintFunc() labelColor   = color.New(color.FgHiWhite).Add(color.Bold).SprintFunc() aiColor      = color.New(color.FgHiCyan).Add(color.Bold).SprintFunc() errorColor   = color.New(color.FgRed).Add(color.Bold).SprintFunc() ) 

Начнем с переменных. Это не более, чем просто функции для подсветки текста. Мы же пишем аналог gemini, ведь так? Ну вот и текст будем делать тоже красивым.

Далее у нас идут инструкции для нейросети. Модельки я использую бесплатные, но не самые слабые. Конкретно тут я использую qwen3 на 30b параметров. Api ключ я покажу как позже получить на openrouter.

const ( systemPromptBase            = "Вот системный контекст:\n" simpleChatPromptTemplate    = "Ты — AI-агент. Ответь на вопрос пользователя, основываясь на информации которая есть. НИКОГДА НЕ ИСПОЛЬЗУЙ СМАЙЛИКИ. вот история сообщений: %v" commandGenPromptTemplate    = "Ты — AI-агент. Твоя задача — генерировать команды для PowerShell на основе запроса пользователя.Отвечай только командой, без лишних слов, объяснений и markdown-форматирования. Только голая команда. Никогда не используй смайлики. Старайся генерить команды, которые не выводят очень длинный лог. И вот наши прошлые сообщения: %v" errorAnalysisPromptTemplate = "Проанализируй ошибку выполнения команды PowerShell и объясни простыми словами, что пошло не так. Исходный запрос: '%s'. Команда: '%s'. Ошибка: '%s'" summaryPromptTemplate       = "Кратко объясни результат выполнения команды, основываясь на первоначальном запросе пользователя. Исходный запрос: '%s'. Вывод команды: '%s'" )

Ну тут сами прочитаете тексты. 1 промпт нужен для предоставлении базовой информации о пк. Это можно убрать, но если вы планируете делать такого агента под mac os или linux, то предоставление базовой информации нейросетке для генерации правильных команд необходимо. 2 промпт для режима чата. 3 для агента. 4 для анализа ошибок и 5 для отчетов.

Идем далее

type Message struct { Role    string `json:"role"` Content string `json:"content"` }  type App struct { APIKey     string APIUrl     string Model      string MaxRetries int Client     *http.Client Reader     *bufio.Reader History    []Message }  type APIRequest struct { Model    string    `json:"model"` Messages []Message `json:"messages"` }  type APIResponse struct { Choices []Choice `json:"choices"` }  type Choice struct { Message ResponseMessage `json:"message"` }  type ResponseMessage struct { Content string `json:"content"` }

Структурки.

первая структура для сохранения истории. Все остальные для взаимодействия с API нейросети. А вот app для инициализации приложения. Городить DI, конфигурацию и прочий оверхед в маленьком проекте я не буду. Сейчас у нас максимально лайтовая CLI’ка на каждый день.

Структурку App необходимо проинициализировать.

func NewApp() *App {  return &App{ APIKey:     "апи ключ", APIUrl:     "https://openrouter.ai/api/v1/chat/completions", Model:      "qwen/qwen3-30b-a3b:free", MaxRetries: 5, Client: &http.Client{ Timeout: 60 * time.Second, //генерация может быть не самой быстрой }, Reader: bufio.NewReader(os.Stdin), History: []Message{ { Role:    "system", Content: systemPromptBase + getSystemInfo(), }, }, } }

Но запомните: НИКОГДА НЕ ХАРДКОРДИТЕ АПИ КЛЮЧИ! ВОТ ВООБЩЕ НЕ НАДО ТАК ДЕЛАТЬ!! В данном случае мы пишем CLI чисто для эксперемента, а не в продакшн. Поэтому загружать конфиг с env я не вижу смысла тут. Но если будете расширять, то ОБЯЗАТЕЛЬНО ВЫНОСИТЕ КОНФИГУРАЦИЮ И САМОЕ ГЛАВНОЕ .ENV.

Функция getSystemInfo() возвращает базовую информацию о пк юзера.

func getSystemInfo() string { osName := runtime.GOOS currentUser, err := user.Current() username := "unknown" if err == nil { username = currentUser.Username } cwd, err := os.Getwd() if err != nil { cwd = "unknown" } return fmt.Sprintf( "System Context:\n- OS: %s\n- Shell: PowerShell\n- User: %s\n- CWD: %s\n", osName, username, cwd, ) }

Итак, давайте получим API ключ. Если у вас локальная модель или другая, то можете пропускать смело шаг 2.

Шаг 2. Получаем API ключ для нейросети на openrouter

Перейдите на сайт опенроутера и войдите любым удобным способ

После этого наведите курсор на свою иконку. Там будет вкладка «keys». Вот это вот вам туда вот.

Далее просто создаете ключ, копируете и вставляете.

Сложно? Не думаю.

Шаг 3. Создаем функцию генерации контента.

Cердце нашей утилиты, без которого оно не сможет функционировать хоть как-то. Функция максимально типичная и простая. Она делает запрос к API и возвращает ответ. Это ядро логики взаимодействия с внешним API генерации текста.

func (a *App) GenerateContent(messages []Message) (string, error) { // Формируем тело запроса с моделью и сообщениями reqBody := APIRequest{ Model:    a.Model, Messages: messages, }  // Сериализуем тело запроса в JSON bodyBytes, err := json.Marshal(reqBody) if err != nil { return "", fmt.Errorf("ошибка кодирования JSON: %w", err) }  // Создаём HTTP POST-запрос к API req, err := http.NewRequest("POST", a.APIUrl, bytes.NewBuffer(bodyBytes)) if err != nil { return "", fmt.Errorf("ошибка создания запроса: %w", err) }  // Устанавливаем заголовки: тип контента и авторизация req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+a.APIKey)  // Отправляем запрос и получаем ответ resp, err := a.Client.Do(req) if err != nil { return "", fmt.Errorf("ошибка выполнения запроса: %w", err) } defer resp.Body.Close()  // Читаем тело ответа respBytes, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("ошибка чтения ответа: %w", err) }  // Проверяем успешность запроса по статус-коду if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("API вернул ошибку (статус %d): %s", resp.StatusCode, string(respBytes)) }  // Десериализуем JSON-ответ в структуру var result APIResponse if err := json.Unmarshal(respBytes, &result); err != nil { return "", fmt.Errorf("ошибка декодирования ответа: %w", err) }  // Возвращаем контент первого ответа, если он есть if len(result.Choices) > 0 && result.Choices[0].Message.Content != "" { return result.Choices[0].Message.Content, nil }  // Если контент не получен — возвращаем ошибку return "", fmt.Errorf("API не вернул контент в ответе") }

Шаг 4. Выполняем команды от нейросети.

Напишем функцию, которая будет преобразовывать слова в действие.

func executeCommand(cmdStr string) (string, error) { cmd := exec.Command("powershell", "-NoProfile", "-Command", cmdStr) output, err := cmd.CombinedOutput() // Возвращаем вывод в любом случае, т.к. там может быть текст ошибки return strings.ToValidUTF8(string(output), ""), err }

Функция executeCommand выполняет указанную строку команды через PowerShell, собирает весь вывод (включая ошибки), преобразует его в корректную UTF-8 строку и возвращает вместе с ошибкой (если она возникла). Даже если команда завершилась с ошибкой, текст её вывода всё равно возвращается — это важно для отображения сообщений об ошибках пользователю. Если в выводе будет кириллица, то символы будут битые. Поэтому мы переводим все в UTF8.

В дополнение к этой команде идет функция для очистки. Зачем? Я отвечу зачем. Хоть мы в промпте явно указываем, что генерировать команду надо сразу, но от фундаментальных проблем нейросети мы не лишены. Нейросеть банально может забыть инструкцию из-за длины контекста. Поэтому мы перестраховываемся.

func cleanCommand(cmdStr string) string { cmdStr = strings.TrimPrefix(cmdStr, "```powershell") cmdStr = strings.TrimPrefix(cmdStr, "```bash") cmdStr = strings.TrimPrefix(cmdStr, "```") cmdStr = strings.TrimSuffix(cmdStr, "```") cmdStr = strings.TrimPrefix(cmdStr, "Команда: ") cmdStr = strings.TrimPrefix(cmdStr, "Command: ")  // Более надежное извлечение команды из `powershell -command "..."` if strings.HasPrefix(strings.ToLower(cmdStr), "powershell -command ") { firstQuote := strings.Index(cmdStr, "\"") lastQuote := strings.LastIndex(cmdStr, "\"") if firstQuote != -1 && lastQuote > firstQuote { cmdStr = cmdStr[firstQuote+1 : lastQuote] } } return strings.TrimSpace(cmdStr) }

Функция cleanCommand очищает строку команды от лишнего форматирования и обёрток, которые могли быть добавлены нейросетью. Она удаляет префиксы вроде powershell`, bash, "Команда: ", "Command: ", а также обрезает завершающие ««.

Если строка начинается с конструкции powershell -command "...", то функция аккуратно извлекает содержимое внутри кавычек. В итоге возвращается чистая команда, готовая к выполнению.

Шаг 5. Пишем вспомогательные функции для красивого вывода

Тут я прям подробно заострять внимание не буду. В целом можно обойтись и без этих функций, но тк мы делаем свою gemini CLI, которая работает в России, то без них не обойтись.

Начнем с функции вывода огромной надписи CLI AGENT.

func printHeader() { var asciiHeader = []string{ "  ███████╗██╗     ██╗     █████╗  ██████╗  █████╗ ███████╗███╗   ██╗████████╗", "  ██╔════╝██║     ██║    ██╔══██╗██╔════╝ ██╔══██╗██╔════╝████╗  ██║╚══██╔══╝", "  ██║     ██║     ██║    ███████║██║  ███╗███████║█████╗  ██╔██╗ ██║   ██║   ", "  ██║     ██║     ██║    ██╔══██║██║   ██║██╔══██║██╔══╝  ██║╚██╗██║   ██║   ", "  ███████╗███████╗██║    ██║  ██║╚██████╔╝██║  ██║███████╗██║ ╚████║   ██║   ", "  ╚══════╝╚══════╝╚═╝    ╚═╝  ╚═╝ ╚═════╝ ╚═╝  ╚═╝╚══════╝╚═╝  ╚═══╝   ╚═╝   ", "                                                                            ", "                             (by oyminirole)                                ", } colors := []*color.Color{ color.New(color.FgHiCyan), color.New(color.FgCyan), color.New(color.FgHiBlue), color.New(color.FgBlue), color.New(color.FgHiMagenta), color.New(color.FgMagenta), }  for _, line := range asciiHeader { lineLength := len(line) step := float64(len(colors)) / float64(lineLength)  for i, char := range line { colorIndex := int(float64(i) * step) if colorIndex >= len(colors) { colorIndex = len(colors) - 1 } colors[colorIndex].Printf("%c", char) } fmt.Println() }  color.New(color.FgHiCyan).Add(color.Bold).Println("\nИнформация:") fmt.Println(" • Используйте перед запросом \"!\" для получения ответов на вопросы, или просто введите запрос. Пример: !Как создать папку?") fmt.Println(" • Для режима агента просто введите запрос без !. Агент будет исполнять команды и предоставлять результаты с отчетами.") fmt.Println(" • Для выхода из программы нажмите Ctrl+C или закройте окно терминала.") fmt.Println(strings.Repeat("─", 70)) }

Функция printHeader выводит в терминал красивый ASCII-баннер с градиентной цветовой заливкой, а затем — краткую справку по использованию CLI-интерфейса.

Сначала она построчно печатает логотип с псевдонимом (oyminirole это я кста), используя плавный градиент из шести цветов. Затем выводит инструкции для пользователя: как задавать вопросы (!), как использовать режим агента и как выйти из программы. Всё оформлено в стиле интерактивной CLI-помощи.

Куда же без спинера.

func startSpinner(text string) chan bool { stop := make(chan bool) go func() { frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇"} i := 0 for { select { case <-stop: fmt.Print("\r\033[K") //выполняет очистку текущей строки в терминале return default: fmt.Print("\r\033[K") //выполняет очистку текущей строки в терминале fmt.Print(text + " ") color.New(color.FgHiCyan).Add(color.Bold).Printf(frames[i%len(frames)]) time.Sleep(100 * time.Millisecond) i++ } } }() return stop }

Функция startSpinner запускает спиннер (анимацию загрузки) в отдельной горутине, чтобы визуально показать, что идёт фоновый процесс (например, генерация ответа).

Спиннер крутится в консоли, отображая символы из массива frames (символы типа , и т.д.). Каждые 100 мс обновляется кадр, пока в канал stop не придёт сигнал — тогда спиннер останавливается и строка очищается.

Возвращаемый канал chan bool используется, чтобы остановить анимацию извне.

Теперь напишем функции для отображения красивых ответов

func printResultBox(commandOutput, aiSummary string, makeAnalize bool) { width := 80  fmt.Printf(" %s\n", successColor("╭─[ Результат ]"+strings.Repeat("─", width-16))) fmt.Printf(" %s %s\n", borderColor("│"), labelColor("Вывод команды:")) for _, line := range strings.Split(strings.TrimSpace(commandOutput), "\n") { fmt.Printf(" %s   %s\n", borderColor("│"), line) } if makeAnalize && aiSummary != "" { fmt.Printf(" %s %s\n", borderColor("│"), borderColor(strings.Repeat("·", width-4))) fmt.Printf(" %s %s\n", borderColor("│"), aiColor("Анализ AI:")) for _, line := range strings.Split(strings.TrimSpace(aiSummary), "\n") { fmt.Printf(" %s   %s\n", borderColor("│"), aiColor(line)) } } fmt.Printf(" %s\n", successColor("╰"+strings.Repeat("─", width-2))) }  func printErrorBox(errorOutput, aiAnalysis string) { width := 80  fmt.Printf(" %s\n", errorColor("╭─[ ✗ Ошибка ]"+strings.Repeat("─", width-16))) fmt.Printf(" %s %s\n", borderColor("│"), labelColor("Лог ошибки:")) for _, line := range strings.Split(strings.TrimSpace(errorOutput), "\n") { fmt.Printf(" %s   %s\n", borderColor("│"), errorColor(line)) } if aiAnalysis != "" { fmt.Printf(" %s %s\n", borderColor("│"), borderColor(strings.Repeat("·", width-4))) fmt.Printf(" %s %s\n", borderColor("│"), aiColor("Анализ AI:")) for _, line := range strings.Split(strings.TrimSpace(aiAnalysis), "\n") { fmt.Printf(" %s   %s\n", borderColor("│"), aiColor(line)) } } fmt.Printf(" %s\n", errorColor("╰"+strings.Repeat("─", width-2))) }

Знаю, что выглядит очень страшно, но ничего страшного они не делают. Запоминать вам это не надо. Эти функции просто выводят красиво текст в консоль.

Шаг 6. Пишем основную логику

Начнем с наименее страшной функции.

// handleSimpleChat обрабатывает запросы в режиме простого чата. func (a *App) handleSimpleChat(userInput string) { spinner := startSpinner("Генерация...")   prompt := []Message{ {Role: "system", Content: fmt.Sprintf(simpleChatPromptTemplate, a.History)}, {Role: "user", Content: strings.TrimPrefix(userInput, "!")}, }  response, err := a.GenerateContent(prompt) if err != nil { printErrorBox(fmt.Sprintf("Ошибка генерации ответа:\n%v", err), "") return } spinner <- true printResultBox(response, "", false)  }

Функция handleSimpleChat обрабатывает пользовательский ввод в режиме простого чата (вопрос-ответ) и выводит результат.

Что делает по шагам:

  1. Запускает спиннер с подписью «Генерация…», чтобы показать, что идёт обработка.

  2. Формирует промпт для нейросети:

    • системное сообщение с шаблоном simpleChatPromptTemplate, в который подставляется история общения (a.History);

    • пользовательское сообщение (ввод без ! в начале).

  3. Вызывает GenerateContent для получения ответа от AI.

  4. Обрабатывает ошибку, если она возникла — выводит её в виде красной рамки.

  5. Останавливает спиннер, если всё прошло успешно.

  6. Печатает результат в красивой рамке (printResultBox).

Итог: простой чат-режим, в котором мы получаем ответ на свой вопрос с анимацией ожидания и красивым выводом. Ничего сложного.

Идем дальше. Вот теперь мы подходим к самой логике агента. Но для начала напишем простенькую функцию для подтверждения команд.

func (a *App) askForConfirmation(command string) bool { fmt.Print("Подтвердить команду? [y/n]: ", aiColor(command), "\n> ")  confirmInput, _ := a.Reader.ReadString('\n') confirmInput = strings.ToLower(strings.TrimSpace(confirmInput)) fmt.Print("\033[2A\033[J") return confirmInput == "y" || confirmInput == "yes" }

Функция askForConfirmation запрашивает у пользователя подтверждение на выполнение команды и возвращает true, если ответ — положительный. Так же она очищает 2 строки вверх (\033[2A) и удаляет их содержимое (\033[J), чтобы убрать следы подтверждения из терминала.

Теперь сам агент.

func (a *App) handleCommandMode(userInput string) { currentTurnHistory := make([]Message, len(a.History)) copy(currentTurnHistory, a.History) currentTurnHistory = append(currentTurnHistory, Message{Role: "user", Content: userInput})  var lastError string for i := 0; i < a.MaxRetries; i++ {  spinnerGen := startSpinner("Генерация команды...") prompt := []Message{ {Role: "system", Content: fmt.Sprintf(commandGenPromptTemplate, currentTurnHistory)}, {Role: "user", Content: userInput}, } command, err := a.GenerateContent(prompt) spinnerGen <- true  if err != nil { printErrorBox(fmt.Sprintf("Ошибка генерации команды:\n%v", err), "") return } command = cleanCommand(command)  // 2. Подтверждение от пользователя if !a.askForConfirmation(command) { log.Println("Команда отменена пользователем.") return } currentTurnHistory = append(currentTurnHistory, Message{Role: "assistant", Content: command})  // 3. Выполнение команды spinnerExec := startSpinner("Выполнение...")  output, err := executeCommand(command)       spinnerExec <- true  if err != nil {  currentTurnHistory = append(currentTurnHistory, Message{Role: "console error", Content: output}) lastError = output time.Sleep(time.Second) continue // Переходим к следующей попытке }  // 4. Успешное выполнение и подведение итогов spinnerSummary := startSpinner("Готовлю отчет...") summaryPrompt := []Message{{Role: "user", Content: fmt.Sprintf(summaryPromptTemplate, userInput, output)}} aiSummary, err := a.GenerateContent(summaryPrompt) if err != nil { log.Println("Ошибка генерации отчета: ", errorColor(err)) } spinnerSummary <- true  printResultBox(output, aiSummary, true) currentTurnHistory = append(currentTurnHistory, Message{Role: "AI", Content: aiSummary})  a.History = currentTurnHistory // Сохраняем успешный диалог в основную историю return                         // Успешно завершили, выходим из функции }  // 5. Обработка, если все попытки провалились spinnerAnalysis := startSpinner("Анализирую ошибки...")  analysisPrompt := []Message{{Role: "user", Content: fmt.Sprintf(errorAnalysisPromptTemplate, userInput, "N/A", lastError)}} aiAnalysis, _ := a.GenerateContent(analysisPrompt) spinnerAnalysis <- true  printErrorBox(fmt.Sprintf("Не удалось выполнить задачу после %d попыток.", a.MaxRetries), aiAnalysis) }

Согласен, пугает. Метод handleCommandMode реализует режим агента, в котором нейросеть генерирует команду по пользовательскому запросу, предлагает её на подтверждение, выполняет, а затем формирует краткий отчёт или анализирует ошибки при неудаче.

Пошаговая логика

  1. Создаётся копия истории сообщений текущего чата, чтобы работать в рамках одной сессии, не влияя на глобальную историю. Это сделано чтобы не забивать контекст.

  2. Запуск до MaxRetries попыток:

    • Генерация команды: нейросети передаётся промпт на основе истории и текущего ввода. Показывается спиннер.

    • Подтверждение: у пользователя спрашивается, можно ли выполнять команду. При отказе — функция завершает работу.

    • Выполнение команды: запускается в PowerShell, с анимацией выполнения. Если произошла ошибка — результат сохраняется, делается пауза и запускается следующая попытка.

  3. Если команда выполнена успешно:

    • Генерируется краткий AI-отчёт по результатам выполнения.

    • Вывод команды и отчёт красиво показываются через printResultBox.

    • Вся история этого раунда сохраняется в общую историю приложения.

  4. Если все попытки провалились:

    • Генерируется анализ ошибки (AI анализирует, почему не удалось).

    • Показывается красная рамка с числом неудачных попыток и анализом.

Назначение в CLI

Этот метод — ключевой элемент CLI-агента. Он превращает текстовый ввод пользователя в конкретную команду, контролирует выполнение, перехватывает ошибки и формирует осмысленные отчёты, обеспечивая высокий уровень автономности и интерактивности.

Шаг 7. Собираем все в единое целое.

Все самое страшное теперь позади. Давайте соберем наше приложение.

func isCommandPrefixed(input string) bool { return strings.HasPrefix(strings.ToLower(strings.TrimSpace(input)), "!") }  func (a *App) Run() { printHeader() //да, это наш красивый заголовок  for { fmt.Println("╭─" + strings.Repeat("─", 66)) color.New(color.FgHiWhite).Print("│ > Введите запрос: ") userInput, _ := a.Reader.ReadString('\n') color.New(color.FgHiWhite).Println("╰" + strings.Repeat("─", 65)) userInput = strings.TrimSpace(userInput)  if userInput == "" { continue }  if isCommandPrefixed(userInput) { a.handleSimpleChat(userInput) } else { a.handleCommandMode(userInput) } } }  func main() { app := NewApp() defer app.Client.CloseIdleConnections() // Закрываем соединения при завершении app.Run() }

Обработка пользовательского ввода

Каждый цикл начинается с вывода декоративной рамки и приглашения ко вводу ( > Введите запрос:). Введённая строка очищается от пробелов и анализируется:

  • Если строка начинается с восклицательного знака ! — система воспринимает её как вопрос или сообщение и передаёт на обработку в handleSimpleChat. Это режим диалога с нейросетью без выполнения команд.

  • Если строка не начинается с ! — считается, что пользователь хочет выполнить команду, и запуск происходит через handleCommandMode. В этом режиме нейросеть сначала генерирует команду, затем спрашивает подтверждение, выполняет её и формирует краткий отчёт.

Такой подход позволяет чётко разделять безопасные текстовые запросы и потенциально опасные системные действия.

Главный цикл приложения

Функция Run запускает бесконечный цикл обработки пользовательского ввода, обеспечивая непрерывную работу CLI-интерфейса. Перед этим выводится ASCII-заголовок. Каждое сообщение анализируется и передаётся в соответствующий режим, в зависимости от его структуры.

Запуск и завершение

Точка входа программы — функция main — создаёт экземпляр App с преднастроенными параметрами (HTTP-клиент, история, ввод и т.д.) и запускает основной цикл. При завершении работы соединения HTTP-клиента закрываются корректно через defer, что особенно важно при частом использовании API.

Результат

Итогом является интуитивно понятный CLI-интерфейс, где:

  • !Как создать папку? → возвращает ответ ИИ;

  • создай мне папку на рабочем столе → превращается в команду, исполняется, а затем поясняется.

Шаг 7. Восхищаемся результатом.

Поздравляю, мы получили максимально простую и красивую CLI’ку, которую спокойно можно переписывать как душе удобно, добавляя новый функционал. Причем с неплохими фишками. У нас нейросеть проводит саморефлексию и чинит ошибки. Что может быть лучше?

Ну и еще примерчик:

Команды можно выполнять и более сложные. Спокойно может поднять докер контейнеры, создать файлы и написать в них код, проанализировать тонну информации о вашем пк, найти всякие операции, которые жрут память и тд.

На мой взгляд приложение вышло крайне полезным и интересным.

Сори, что не выложил полный код на гитхаб. Позже добавлю, все распишу, так как ну вообще нету времени в данный момент у меня. Но поделиться интересным тоже хочется. На гитхаб уже опубликую более полноценную версию с env и конфигами, а так же чуть допилю функционал. Но в целом все основные моменты я вам показал.

По традиции жду ваших комментариев. Гудлак!


ссылка на оригинал статьи https://habr.com/ru/articles/925318/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *