Dando habilidades de agente de IA ao Discourse

Um padrão muito comum na indústria, utilizado em uma ampla variedade de agentes, são as Agent Skills (Habilidades de Agente).

As habilidades resolvem um problema de contexto para os agentes.

Em vez de encher um único prompt de sistema gigante com todas as suas instruções e ideias, as habilidades do agente permitem a revelação progressiva.

Seu agente sabe o que pode fazer e chama uma ferramenta muito específica para descobrir como fazê-lo.

Isso oferece benefícios significativos no Discourse porque podemos manter o catálogo de habilidades em tópicos e obter um editor agradável, histórico e permissões.

Usando ferramentas personalizadas no Discourse AI, podemos aproximar esse sistema devido a dois recursos muito poderosos que elas fornecem.

  • As ferramentas personalizadas permitem a modificação do prompt de sistema
  • As ferramentas personalizadas podem ler tópicos e postagens em categorias específicas.

Nosso Objetivo

Para aproximar as habilidades no Discourse, gostaríamos de:

  1. Um tópico por habilidade
  2. Um diretório automático de habilidades disponíveis no prompt de sistema

Então, por exemplo, gostaríamos de um tópico com este formato (cru):

---
name: doc-coauthoring
description: Guia os usuários através de um fluxo de trabalho estruturado para coautoria de documentação. Use quando o usuário quiser escrever uma proposta, especificação técnica, registro de decisão ou documento semelhante.
---

# Fluxo de trabalho de coautoria de documentos

1. Reúna o contexto do usuário.
2. Combine um esboço.
3. Esboce e refine cada seção.
4. Teste o documento da perspectiva de um novo leitor.

Para ser traduzido no prompt de sistema da seguinte forma:

<available_skills>
  <skill>
    <name>doc-coauthoring</name>
    <description>Guia os usuários através de um fluxo de trabalho estruturado para coautoria de documentação...</description>
    <location>https://example.com/t/skill-doc-coauthoring/123</location>
  </skill>
  ... mais habilidades aqui ...
</available_skills>

Escolhemos este formato específico porque muitos modelos de linguagem já são ajustados para procurar esta forma específica, o que pode melhorar a recuperação. Observe como o corpo da habilidade real está ausente.

Então, se o modelo detectar que um usuário está tentando coautorar um fluxo de trabalho de documento, ele chamará a ferramenta personalizada load_skill para obter as instruções reais.

Com isso em vigor, podemos definir uma categoria dedicada no Discourse que contenha habilidades que queremos que nossos agentes usem e permitir que membros da comunidade as iterem.

Como funciona?

Toda a lógica de load_skill é autorada em JavaScript, usando uma ferramenta personalizada.

discourse.filterTopics pode ser chamado para procurar tópicos em uma categoria específica
discourse.getPost pode ser chamado para obter o corpo de uma postagem
customSystemMessage é usado para injetar um prompt de sistema personalizado

Com esses blocos de construção

Primeiro, definimos uma nova ferramenta personalizada:

  • name: load_skill
  • params:
    • “name”: “O nome da habilidade de available_skills”

Em seguida, ensinamos a análise 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;
}

Certifique-se de definir SKILLS_CATEGORY_SLUG para o slug real da categoria que contém suas habilidades.

Em seguida, usamos customSystemMessage para injetar o prompt de sistema

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

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

  var lines = [
    "As habilidades fornecem instruções e fluxos de trabalho especializados para tarefas específicas.",
    "Use a ferramenta load_skill para carregar uma habilidade quando uma tarefa corresponder à sua descrição.",
    "<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, construímos o carregador de habilidades que vai do slug para o conteúdo da habilidade.

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

  if (typeof requestedName !== "string") {
    return "Nenhum nome de habilidade foi fornecido.";
  }

  requestedName = requestedName.trim();

  if (!SKILL_NAME_PATTERN.test(requestedName)) {
    return "Os nomes das habilidades devem usar letras minúsculas, números e hífens.";
  }

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

  if (matches.length === 0) {
    return "Nenhuma habilidade disponível tem o nome " + requestedName + ".";
  }

  if (matches.length > 1) {
    return "Mais de uma habilidade tem o nome " + requestedName + ".";
  }

  var skill = matches[0];

  return [
    '<skill_content name="' + xmlEscape(skill.name) + '">',
    "# Habilidade: " + skill.name,
    "",
    skill.content.trim(),
    "",
    "Tópico de origem para esta habilidade: " + skill.location,
    "</skill_content>",
  ].join("\n");
}

Um agente completo + json de ferramenta personalizada está disponível aqui, você pode importar para sua instância e alterar conforme achar adequado:

category-skills-agent.json (5.9 KB)

Vendo isso em ação

Limitações

A demonstração atual ainda tem uma chamada N+1 carregando cada postagem com uma habilidade em cada turno do usuário. Em escala, isso pode não ser aconselhável.

O design pode ser alterado para que um post de catálogo de habilidades seja gerado automaticamente usando um fluxo de trabalho ou talvez filterTopics possa ser expandido para permitir uma chamada pedindo prefixo.

Além disso, as habilidades do agente suportam recursos e scripts, os recursos podem ser aproximados permitindo a ferramenta “ler” e vinculando a tópicos específicos da sua habilidade. A execução de shell está claramente fora do escopo do Discourse.

Espero que você ache isso útil

1 curtida