Dotar al agente IA de Discourse con habilidades

Un patrón muy común en la industria que se utiliza en una amplia variedad de agentes son las Agent Skills (Habilidades de Agente)

Las habilidades resuelven un problema de contexto para los agentes.

En lugar de meter todas tus instrucciones e ideas en un único y gigantesco system prompt (mensaje del sistema), las habilidades de agente permiten una revelación progresiva.

Tu agente sabe lo que puede hacer y llama a una herramienta muy específica para averiguar cómo hacerlo.

Esto ofrece beneficios significativos en Discourse porque podemos mantener el catálogo de habilidades en temas y obtener un buen editor, historial y permisos.

Usando herramientas personalizadas en Discourse AI, podemos aproximar este sistema debido a dos características muy poderosas que proporcionan.

  • Las herramientas personalizadas permiten la modificación del system prompt.
  • Las herramientas personalizadas pueden leer temas y publicaciones en categorías específicas.

Nuestro Objetivo

Para aproximar las habilidades en Discourse, nos gustaría tener:

  1. Un tema por cada habilidad
  2. Un directorio automático de habilidades disponibles en el system prompt

Así que, por ejemplo, nos gustaría un tema con esta estructura (en formato raw):

---
name: doc-coauthoring
description: Guía a los usuarios a través de un flujo de trabajo estructurado para coescribir documentación. Úsalo cuando el usuario quiera escribir una propuesta, especificación técnica, registro de decisión o documento similar.
---

# Flujo de trabajo para coautoría de documentos

1. Recopila contexto del usuario.
2. Acuerda un esquema.
3. Redacta y refina cada sección.
4. Prueba el documento desde la perspectiva de un nuevo lector.

Para ser traducido a lo siguiente en el system prompt:

<available_skills>
  <skill>
    <name>doc-coauthoring</name>
    <description>Guía a los usuarios a través de un flujo de trabajo estructurado para coescribir documentación...</description>
    <location>https://example.com/t/skill-doc-coauthoring/123</location>
  </skill>
  ... más habilidades aquí ...
</available_skills>

Elegimos este formato particular porque muchos modelos de lenguaje ya están afinados para buscar esta estructura específica, lo cual puede mejorar la recuperación de información. Observa cómo el cuerpo de la habilidad real está ausente.

Luego, si el modelo detecta que un usuario está intentando coescribir un flujo de trabajo de documentos, llamará a la herramienta personalizada load_skill para obtener las instrucciones reales.

Con esto en su lugar, podemos definir una categoría dedicada en Discourse que contenga las habilidades que queremos que nuestros agentes utilicen y permitir que los miembros de la comunidad las iteren.

¿Cómo funciona?

Toda la lógica de load_skill está escrita en JavaScript, usando una herramienta personalizada.

discourse.filterTopics puede ser llamado para buscar temas en una categoría particular
discourse.getPost puede ser llamado para obtener el cuerpo de una publicación
customSystemMessage se usa para inyectar un system prompt personalizado

Con estos bloques de construcción

Primero definimos una nueva herramienta personalizada:

  • nombre: load_skill
  • parámetros:
    • “name”: “El nombre de la habilidad desde available_skills”

A continuación, enseñamos el análisis de habilidades:

var SKILLS_CATEGORY_SLUG = "agent-skills";
var SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;

function skillTopics() {
  var result = discourse.filterTopics({
    q: "category:" + SKILLS_CATEGORY_SLUG + " order:created",
    limit: 200,
  });

  return result && Array.isArray(result.topics) ? result.topics : [];
}

function topicOp(topic) {
  if (!topic || !topic.first_post_id) {
    return null;
  }

  return discourse.getPost(topic.first_post_id);
}

function frontmatter(raw) {
  if (typeof raw !== "string") {
    return null;
  }

  var match = raw.match(
    /^---[ \t]*\n([\s\S]*?)\n---(?:[ \t]*\n|$)/
  );

  return match ? match[1] : null;
}

function scalarField(yaml, field) {
  var match = yaml.match(
    new RegExp("^" + field + ":[ \\t]*(.+)[ \\t]*$", "m")
  );

  return match ? match[1].trim() : null;
}

function xmlEscape(value) {
  return String(value)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/\"/g, "&quot;")
    .replace(/'/g, "&apos;");
}

function skillRecord(topic) {
  var post = topicOp(topic);
  var yaml = post && frontmatter(post.raw);

  if (!yaml) {
    return null;
  }

  var name = scalarField(yaml, "name");
  var description = scalarField(yaml, "description");

  if (
    !name ||
    !description ||
    !SKILL_NAME_PATTERN.test(name) ||
    name.length > 64 ||
    description.length > 1024
  ) {
    return null;
  }

  return {
    name: name,
    description: description,
    location: discourse.baseUrl + topic.url,
    content: post.raw,
  };
}

function availableSkills() {
  var records = [];
  var topics = skillTopics();

  for (var i = 0; i < topics.length; i++) {
    var record = skillRecord(topics[i]);
    if (record) {
      records.push(record);
    }
  }

  records.sort(function (a, b) {
    return a.name.localeCompare(b.name);
  });

  return records;
}

Asegúrate de establecer SKILLS_CATEGORY_SLUG con el slug real de la categoría que contiene tus habilidades.

A continuación, usamos customSystemMessage para inyectar el system prompt

function customSystemMessage() {
  var skills = availableSkills();

  if (skills.length === 0) {
    return null;
  }

  var lines = [
    "Las habilidades proporcionan instrucciones y flujos de trabajo especializados para tareas específicas.",
    "Usa la herramienta load_skill para cargar una habilidad cuando una tarea coincida con su descripción.",
    "<available_skills>",
  ];

  skills.forEach(function (skill) {
    lines.push("  <skill>");
    lines.push("    <name>" + xmlEscape(skill.name) + "</name>");
    lines.push(
      "    <description>" +
        xmlEscape(skill.description) +
        "</description>"
    );
    lines.push(
      "    <location>" + xmlEscape(skill.location) + "</location>"
    );
    lines.push("  </skill>");
  });

  lines.push("</available_skills>");
  return lines.join("\n");
}

Finalmente, construimos el cargador de habilidades que va desde el slug hasta el contenido de la habilidad.

function invoke(parameters) {
  var requestedName = parameters && parameters.name;

  if (typeof requestedName !== "string") {
    return "No se proporcionó ningún nombre de habilidad.";
  }

  requestedName = requestedName.trim();

  if (!SKILL_NAME_PATTERN.test(requestedName)) {
    return "Los nombres de las habilidades deben usar letras minúsculas, números y guiones.";
  }

  var matches = availableSkills().filter(function (skill) {
    return skill.name === requestedName;
  });

  if (matches.length === 0) {
    return "No hay ninguna habilidad disponible con el nombre " + requestedName + ".";
  }

  if (matches.length > 1) {
    return "Más de una habilidad tiene el nombre " + requestedName + ".";
  }

  var skill = matches[0];

  return [
    '<skill_content name="' + xmlEscape(skill.name) + '">',
    "# Habilidad: " + skill.name,
    "",
    skill.content.trim(),
    "",
    "Tema de origen para esta habilidad: " + skill.location,
    "</skill_content>",
  ].join("\n");
}

Un agente completo funcional + json de herramienta personalizada está disponible aquí, puedes importarlo a tu instancia y modificarlo según consideres oportuno:

category-skills-agent.json (5.9 KB)

Viéndolo en acción

Limitaciones

La demostración actual todavía tiene una llamada N+1 que carga cada publicación con una habilidad en cada turno de usuario. A gran escala, esto puede no ser aconsejable.

El diseño podría modificarse para que un catálogo de habilidades se genere automáticamente usando un flujo de trabajo o quizás filterTopics podría ampliarse para permitir una llamada que solicite un prefijo.

Además, las habilidades de agente admiten recursos y scripts; los recursos podrían aproximarse permitiendo la herramienta “leer” y vinculando a temas específicos desde tu habilidad. La ejecución de shell claramente está fuera del alcance de Discourse.

Espero que encuentres esto útil

1 me gusta