Наделение агента Discourse AI навыками

В индустрии широко распространен паттерн, используемый в самых разных агентах — Agent Skills (Навыки агентов).

Навыки решают проблему контекста для агентов.

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

Ваш агент знает, что он может делать, и вызывает очень специфичный инструмент, чтобы узнать, как это сделать.

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

Используя пользовательские инструменты в Discourse AI, мы можем приблизиться к этой системе благодаря двум очень мощным функциям, которые они предоставляют.

  • Пользовательские инструменты позволяют изменять системный промпт
  • Пользовательские инструменты могут читать темы и сообщения в определенных категориях.

Наша цель

Чтобы реализовать навыки в Discourse, мы хотим:

  1. Одна тема на каждый навык
  2. Автоматический каталог доступных навыков в системном промпте

Так что, например, мы хотим, чтобы тема выглядела так (сырой текст):

---
name: doc-coauthoring
description: Guide users through a structured workflow for co-authoring documentation. Use when the user wants to write a proposal, technical specification, decision record, or similar document.
---

# Document co-authoring workflow

1. Gather context from the user.
2. Agree on an outline.
3. Draft and refine each section.
4. Test the document from a new reader's perspective.

Чтобы быть преобразованным в следующее в системном промпте:

<available_skills>
  <skill>
    <name>doc-coauthoring</name>
    <description>Guide users through a structured workflow for co-authoring documentation...</description>
    <location>https://example.com/t/skill-doc-coauthoring/123</location>
  </skill>
  ... more skills here ...
</available_skills>

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

Затем, если модель обнаруживает, что пользователь пытается создать рабочий процесс совместного написания документа, она вызовет пользовательский инструмент load_skill, чтобы получить фактические инструкции.

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

Как это работает?

Вся логика load_skill написана на JavaScript, используя пользовательский инструмент.

discourse.filterTopics можно вызвать, чтобы искать темы в определенной категории
discourse.getPost можно вызвать, чтобы получить тело сообщения
customSystemMessage используется для внедрения пользовательского системного промпта

С этими строительными блоками

Сначала мы определяем новый пользовательский инструмент:

  • name: load_skill
  • params:
    • “name”: “The name of the skill from available_skills”

Далее мы обучаем разбору навыков:

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;
}

Убедитесь, что SKILLS_CATEGORY_SLUG установлен на фактический slug категории, содержащей ваши навыки.

Далее мы используем customSystemMessage для внедрения системного промпта

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

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

  var lines = [
    "Skills provide specialized instructions and workflows for specific tasks.",
    "Use the load_skill tool to load a skill when a task matches its description.",
    "<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");
}

Наконец, мы создаем загрузчик навыков, который переходит от slug к содержимому навыка.

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

  if (typeof requestedName !== "string") {
    return "No skill name was provided.";
  }

  requestedName = requestedName.trim();

  if (!SKILL_NAME_PATTERN.test(requestedName)) {
    return "Skill names must use lowercase letters, numbers, and hyphens.";
  }

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

  if (matches.length === 0) {
    return "No available skill has the name " + requestedName + ".";
  }

  if (matches.length > 1) {
    return "More than one skill has the name " + requestedName + ".";
  }

  var skill = matches[0];

  return [
    '<skill_content name="' + xmlEscape(skill.name) + '">',
    "# Skill: " + skill.name,
    "",
    skill.content.trim(),
    "",
    "Source topic for this skill: " + skill.location,
    "</skill_content>",
  ].join("\n");
}

Полный рабочий JSON-файл агента + пользовательского инструмента доступен здесь, вы можете импортировать его в свой экземпляр и изменять по своему усмотрению:

category-skills-agent.json (5.9 KB)

Видим это в действии

Ограничения

В текущей демонстрации все еще есть N+1 вызов, загружающий каждое сообщение с навыком при каждом ходе пользователя. В масштабе это может быть нежелательно.

Дизайн может быть изменен так, чтобы пост каталога навыков автоматически генерировался с помощью рабочего процесса, или, возможно, filterTopics можно было бы расширить, чтобы разрешить вызов с запросом префикса.

Кроме того, навыки агентов поддерживают ресурсы и скрипты, ресурсы могут быть приближены, позволяя инструменту “чтение” и связываясь с конкретными темами из вашего навыка. Выполнение shell-команд явно выходит за рамки Discourse.

Надеемся, вы найдете это полезным

1 лайк