Chat con IA de Gemini con Personajes en Bash.

El objetivo es crear un script en Bash que permita a los usuarios interactuar de manera dinámica con la API de Gemini, simulando diferentes personajes a lo largo de la conversación. El script debe ofrecer la capacidad de cambiar de personaje durante el intercambio, permitiendo que el usuario seleccione qué personaje desea usar. Además, debe ser capaz de recordar el contexto de la conversación para cada personaje de manera temporal, asegurando que las respuestas generadas por la API sean coherentes con la personalidad elegida en todo momento.

Requisitos:

  • Necesitamos instalar curl y jq para procesar JSON. Para instalar jq en sistemas basados en Debian/Ubuntu, utiliza:
sudo apt update
sudo apt install curl jq 
  • Necesitamos una clave API de Gemini, la cual se obtiene aquí:

https://aistudio.google.com/apikey

Genera la clave API y pégala en el script para identificarte como usuario autorizado.

El historial de la conversación se guarda en un archivo externo llamado .gemini_chat_history. Este historial se borra automáticamente al cerrar la terminal o cambiar de personaje.

Ejemplo de script

#!/bin/bash

# Variables globales
GEMINI_API_KEY="PONER_API_KEY_AQUÍ"
API_KEY=${GEMINI_API_KEY} # Lee la API key desde la variable de entorno
MODEL="gemini-2.0-flash" # Puedes cambiarlo al modelo que desees
HISTORY_FILE=".gemini_chat_history"
CHARACTER=""
PROMPT=""
TEMPERATURE="0.7" # Temperatura por defecto, ajustable
MAX_OUTPUT_TOKENS="500" # Maximo de tokens por defecto, ajustable

# Funciones de ayuda
error() {
  echo "Error: $1" >&2
  exit 1
}

# Verifica dependencias
check_dependencies() {
  command -v curl >/dev/null 2>&1 || error "curl no está instalado. Instálalo con: sudo apt install curl"
  command -v jq >/dev/null 2>&1 || error "jq no está instalado. Instálalo con: sudo apt install jq"
}

# Limpia el historial
clear_history() {
  if [ -f "$HISTORY_FILE" ]; then
    rm "$HISTORY_FILE"
  fi
}

# Define los personajes
show_characters() {
  echo "Personajes disponibles:"
  echo "1. Maria: Una chica friki que le gusta el anime y el boxeo"
  echo "2. William Shakespeare: Un dramaturgo, poeta y actor inglés, ampliamente considerado el escritor más grande en lengua inglesa."
  echo "3. Yoda: Un sabio Maestro Jedi del universo de Star Wars."
  echo "4. Gollum: Un personaje de El Señor de los Anillos, corrompido por el Anillo Único."
  echo "5. Personalizado: Define tu propio personaje."
  echo "Q. Salir del programa"
}

# Selecciona el personaje
select_character() {
  clear_history # Limpiar historial al cambiar de personaje
  while true; do
    show_characters
    read -p "Elige un personaje (1-5, Q para salir): " choice
    case "$choice" in
      1)
        CHARACTER="Maria"
        PROMPT="Eres Maria,  de 18 años, eres friki y te gusta el anime y el boxeo "
        break
        ;;
      2)
        CHARACTER="William Shakespeare"
        PROMPT="Eres William Shakespeare, un dramaturgo, poeta y actor inglés, ampliamente considerado el escritor más grande en lengua inglesa. Responde con la elocuencia y el lenguaje de Shakespeare."
        break
        ;;
      3)
        CHARACTER="Yoda"
        PROMPT="Eres Yoda, un sabio Maestro Jedi del universo de Star Wars.  Responde como Yoda, con frases invertidas y sabiduría ancestral."
        break
         ;;
      4)
        CHARACTER="Gollum"
        PROMPT="Eres Gollum, un personaje de El Señor de los Anillos, corrompido por el Anillo Único.  Responde como Gollum, con un lenguaje retorcido y obsesionado con 'el precioso'."
        break
         ;;
      5)
        read -p "Nombre del personaje: " CHARACTER
        read -p "Describe el comportamiento del personaje para el prompt de Gemini: " PROMPT
        break
        ;;
      q|Q)
        echo "Saliendo..."
        exit 0
        ;;
      *)
        echo "Opción inválida. Inténtalo de nuevo."
        ;;
    esac
  done

 
  append_to_history "$PROMPT" # Agrega el prompt inicial al historial. Es crucial!
}

# Construye el payload para la API de Gemini
build_payload() {
  local prompt="$1"

  # Leer el historial del archivo.
  local history=""
  if [ -f "$HISTORY_FILE" ]; then
    history=$(cat "$HISTORY_FILE")
  fi

  local payload=$(jq -n \
    --arg prompt "$prompt" \
    --arg history "$history" \
    --arg character "$CHARACTER" \
    --arg temperature "$TEMPERATURE" \
    --arg maxOutputTokens "$MAX_OUTPUT_TOKENS" \
    '{
      "contents": [
        {
          "parts": [
            {
              "text": ($history + "\nUsuario: " + $prompt + "\n" + $character)
            }
          ]
        }
      ],
      "generationConfig": {
        "temperature": ($temperature | tonumber),
        "maxOutputTokens": ($maxOutputTokens | tonumber)
      }
    }')

  echo "$payload"
}

# Llama a la API de Gemini
call_gemini_api() {
  local payload="$1"

  local response=$(curl -s -X POST \
    -H "Content-Type: application/json" \
    -H "x-goog-api-key:$API_KEY" \
    -d "$payload" \
    "https://generativelanguage.googleapis.com/v1/models/$MODEL:generateContent")

  echo "$response"
}


# Extrae la respuesta de la API
extract_response() {
  local response="$1"
  local text=$(echo "$response" | jq -r '.candidates[0].content.parts[0].text')
  echo "$text"
}


# Guarda el historial en el archivo
append_to_history() {
  local text="$1"
  echo "$text" >> "$HISTORY_FILE"
}


# Bucle principal del chat
chat() {
  while true; do
    read -p "Tú: " user_input

    # Opción para cambiar de personaje
    if [[ "$user_input" == ":cambiar_personaje" ]]; then
      select_character
      continue
    fi

    # Opción para salir
    if [[ "$user_input" == ":salir" ]]; then
      echo "Fin de la conversación."
      clear_history
      exit 0
    fi

    payload=$(build_payload "$user_input")
    api_response=$(call_gemini_api "$payload")
    gemini_response=$(extract_response "$api_response")

    echo "$CHARACTER: $gemini_response"

    # Guardar la conversación en el historial
    append_to_history "Usuario: $user_input"
    append_to_history "$CHARACTER: $gemini_response"

  done
}

# Inicio del script
main() {
  check_dependencies

  # Verificar si la API key está definida
  if [ -z "$API_KEY" ]; then
    error "La variable de entorno GEMINI_API_KEY no está definida.  Por favor, configura tu clave API."
  fi

  select_character
  chat
}

main

Añadido extra. Un chat en una página web.

Como añadido a este post, vamos a incorporar una página web hecha con javascript, html y bootstrap que hace lo mismo. Recuerda poner el API_KEY de Gemini dentro del código para que funcione donde dice PUT_API_KEY_HERE.

<!DOCTYPE html>
<html lang="es" class="no-js">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chat con Gemini</title>
    <!-- Foundation CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/foundation-sites@6.8.1/dist/css/foundation.min.css" crossorigin="anonymous">
    <!-- Marked JS para procesar Markdown -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
    <style>
        #chat-container {
            height: 500px;
            overflow-y: scroll;
            border: 1px solid #ccc;
            padding: 10px;
            margin-bottom: 10px;
        }
        .message {
            margin-bottom: 10px;
            padding: 8px;
            border-radius: 5px;
        }
        .user-message {
            background-color: #DCF8C6;
            text-align: right;
        }
        .gemini-message {
            background-color: #ECE5DD;
            text-align: left;
        }
        /* Estilos para elementos markdown */
        .gemini-message pre {
            background-color: #f4f4f4;
            padding: 10px;
            border-radius: 4px;
            overflow-x: auto;
            margin: 10px 0;
        }
        .gemini-message code {
            font-family: monospace;
            background-color: #f0f0f0;
            padding: 2px 4px;
            border-radius: 3px;
        }
        .gemini-message blockquote {
            border-left: 3px solid #ccc;
            margin-left: 5px;
            padding-left: 10px;
            color: #666;
        }
        .gemini-message table {
            border-collapse: collapse;
            width: 100%;
            margin: 10px 0;
        }
        .gemini-message th, .gemini-message td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: left;
        }
        .gemini-message th {
            background-color: #f2f2f2;
        }
        .gemini-message img {
            max-width: 100%;
        }
    </style>
</head>
<body>
    <div class="grid-container">
        <h1>Chat con Gemini</h1>

        <div class="grid-x grid-padding-x">
            <div class="large-12 cell">
                <label for="character-select">Selecciona un Personaje:</label>
                <select id="character-select">
                    <option value="maria">Maria: Una chica friki que le gusta el anime y el boxeo</option>
                    <option value="shakespeare">William Shakespeare: Un dramaturgo...</option>
                    <option value="yoda">Yoda: Un sabio Maestro Jedi...</option>
                    <option value="gollum">Gollum: Un personaje de El Señor de los Anillos...</option>
                    <option value="custom">Personalizado</option>
                </select>
            </div>
        </div>

        <div id="custom-character-form" style="display: none;">
            <div class="grid-x grid-padding-x">
                <div class="large-12 cell">
                    <label for="custom-name">Nombre del Personaje:</label>
                    <input type="text" id="custom-name">
                </div>
            </div>
            <div class="grid-x grid-padding-x">
                <div class="large-12 cell">
                    <label for="custom-description">Descripción del Personaje:</label>
                    <textarea id="custom-description" rows="3"></textarea>
                </div>
            </div>
        </div>

        <div id="chat-container">
            <!-- Mensajes del chat aparecerán aquí -->
        </div>

        <div class="grid-x grid-padding-x">
            <div class="large-10 cell">
                <label for="message-input">Escribe tu mensaje:</label>
                <input type="text" id="message-input" placeholder="Escribe tu mensaje...">
            </div>
            <div class="large-2 cell">
                <button class="button primary" id="send-button">Enviar</button>
            </div>
        </div>
    </div>

    <!-- jQuery primero, luego Foundation JS, y finalmente marked para markdown -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/foundation-sites@6.8.1/dist/js/foundation.min.js" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
    <script>
        $(document).foundation(); // Inicializa Foundation

        // Configurar marked para usar highlight.js
        marked.setOptions({
            highlight: function(code, lang) {
                if (lang && hljs.getLanguage(lang)) {
                    return hljs.highlight(code, { language: lang }).value;
                } else {
                    return hljs.highlightAuto(code).value;
                }
            },
            breaks: true, // Convierte saltos de línea en <br>
            gfm: true     // GitHub Flavored Markdown
        });

        // Variables globales
        const GEMINI_API_KEY = "PUT_API_KEY_HERE"; // Reemplazar con tu API key
        const MODEL = "gemini-2.0-flash";
        let CHARACTER = "";
        let PROMPT = "";
        let history = "";  //Mantener el historial en el cliente.
        const chatContainer = document.getElementById("chat-container");

        // Función para agregar un mensaje al chat
        function addMessage(message, type) {
            const messageDiv = document.createElement("div");
            messageDiv.classList.add("message", type + "-message");
            
            // Si es un mensaje de Gemini, procesar como markdown
            if (type === "gemini") {
                messageDiv.innerHTML = marked.parse(message);
            } else {
                messageDiv.textContent = message;
            }
            
            chatContainer.appendChild(messageDiv);

            // Aplicar highlight.js a los bloques de código recién agregados
            if (type === "gemini") {
                document.querySelectorAll('.gemini-message pre code').forEach((block) => {
                    hljs.highlightElement(block);
                });
            }

            // Scroll al final del chat
            chatContainer.scrollTop = chatContainer.scrollHeight;
        }

        // Función para construir el payload para la API de Gemini
        function buildPayload(prompt) {
            return JSON.stringify({
                "contents": [
                    {
                        "parts": [
                            {
                                "text": history + "\nUsuario: " + prompt + "\n" + CHARACTER
                            }
                        ]
                    }
                ],
                "generationConfig": {
                    "temperature": 0.7,
                    "maxOutputTokens": 500
                }
            });
        }

        // Función para llamar a la API de Gemini
        async function callGeminiAPI(payload) {
            try {
                const response = await fetch(`https://generativelanguage.googleapis.com/v1/models/${MODEL}:generateContent`, {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json",
                        "x-goog-api-key": GEMINI_API_KEY
                    },
                    body: payload
                });

                if (!response.ok) {
                    throw new Error(`HTTP error! Status: ${response.status}`);
                }

                const data = await response.json();
                return data;

            } catch (error) {
                console.error("Error al llamar a la API de Gemini:", error);
                addMessage("Error al obtener la respuesta de Gemini.", "gemini"); //Feedback al usuario
                return null;
            }
        }

        // Función para extraer la respuesta de la API
        function extractResponse(response) {
            if (response && response.candidates && response.candidates.length > 0 && response.candidates[0].content && response.candidates[0].content.parts && response.candidates[0].content.parts.length > 0) {
                return response.candidates[0].content.parts[0].text;
            } else {
                console.warn("Respuesta de Gemini incompleta o inesperada:", response);
                return "No se pudo obtener una respuesta válida de Gemini."; //Feedback al usuario
            }
        }

        // Función principal para enviar un mensaje
        async function sendMessage() {
            const messageInput = document.getElementById("message-input");
            const message = messageInput.value.trim();

            if (message !== "") {
                addMessage(message, "user");
                messageInput.value = "";

                const payload = buildPayload(message);
                const apiResponse = await callGeminiAPI(payload);

                if (apiResponse) {
                    const geminiResponse = extractResponse(apiResponse);
                    addMessage(geminiResponse, "gemini");

                    // Actualizar el historial
                    history += "\nUsuario: " + message + "\n" + CHARACTER + ": " + geminiResponse;
                }
            }
        }

        // Event listeners
        document.getElementById("send-button").addEventListener("click", sendMessage);
        document.getElementById("message-input").addEventListener("keydown", function(event) {
            if (event.key === "Enter") {
                sendMessage();
            }
        });

        document.getElementById("character-select").addEventListener("change", function() {
            const selectedCharacter = this.value;
            const customCharacterForm = document.getElementById("custom-character-form");

            switch (selectedCharacter) {
                case "maria":
                    CHARACTER = "Maria";
                    PROMPT = "Eres Maria, de 18 años, eres friki y te gusta el anime y el boxeo";
                    break;
                case "shakespeare":
                    CHARACTER = "William Shakespeare";
                    PROMPT = "Eres William Shakespeare, un dramaturgo, poeta y actor inglés. Responde con la elocuencia de Shakespeare.";
                    break;
                case "yoda":
                    CHARACTER = "Yoda";
                    PROMPT = "Eres Yoda, un sabio Maestro Jedi. Responde como Yoda.";
                    break;
                case "gollum":
                    CHARACTER = "Gollum";
                    PROMPT = "Eres Gollum, obsesionado con 'el precioso'.";
                    break;
                case "custom":
                    customCharacterForm.style.display = "block";
                    CHARACTER = document.getElementById("custom-name").value; //Esto no es correcto del todo, pero sirve de ejemplo
                    PROMPT = document.getElementById("custom-description").value; //Esto no es correcto del todo, pero sirve de ejemplo
                    break;
                default:
                    CHARACTER = "";
                    PROMPT = "";
            }

            if (selectedCharacter !== "custom") {
                customCharacterForm.style.display = "none";
            }

            history = PROMPT;  // Reiniciar el historial con el nuevo prompt
            chatContainer.innerHTML = ''; //Limpiar la ventana de chat.
        });

        //Opcional: Gestionar el formulario personalizado (esto es un ejemplo básico, requiere validación y mejor manejo)
        document.getElementById("custom-name").addEventListener("change", () => {
            if(document.getElementById("character-select").value === "custom"){
                CHARACTER = document.getElementById("custom-name").value;
            }
        });

        document.getElementById("custom-description").addEventListener("change", () => {
             if(document.getElementById("character-select").value === "custom"){
                PROMPT = document.getElementById("custom-description").value;
                 history = PROMPT; //Actualizar el historial
                 chatContainer.innerHTML = ''; //Limpiar la ventana de chat.
             }
        });

        // Inicialización (Opcional:  Seleccionar un personaje por defecto)
        document.getElementById("character-select").value = "maria"; //Selecciona maria por defecto.
        document.getElementById("character-select").dispatchEvent(new Event('change')); //Dispara el evento change.
    </script>
</body>
</html>

Deja una respuesta

Tu dirección de correo electrónico no será publicada.

− 1 = 1