Packages

AI integration layer for the Jido ecosystem - Actions, Workflows, and LLM orchestration

Current section

Files

Jump to
jido_ai lib examples tools skill_writer.ex
Raw

lib/examples/tools/skill_writer.ex

defmodule Jido.AI.Examples.Tools.ValidateSkillName do
@moduledoc "Validates a proposed skill name against agentskills.io rules."
use Jido.Action,
name: "validate_skill_name",
description: "Validates a skill name matches the pattern: lowercase alphanumeric with hyphens, max 64 chars.",
schema: [
name: [type: :string, required: true, doc: "The proposed skill name to validate"]
]
@name_regex ~r/^[a-z0-9]+(-[a-z0-9]+)*$/
@max_length 64
@impl true
def run(params, _context) do
name = params.name
cond do
String.length(name) == 0 ->
{:ok, %{valid: false, name: name, error: "Name cannot be empty"}}
String.length(name) > @max_length ->
{:ok, %{valid: false, name: name, error: "Name exceeds #{@max_length} characters"}}
not Regex.match?(@name_regex, name) ->
{:ok,
%{
valid: false,
name: name,
error: "Name must be lowercase alphanumeric with hyphens (e.g., 'my-skill-name')"
}}
true ->
{:ok, %{valid: true, name: name, suggestion: nil}}
end
end
end
defmodule Jido.AI.Examples.Tools.WriteModuleSkill do
@moduledoc "Generates Elixir module code for a Jido.AI.Skill."
use Jido.Action,
name: "write_module_skill",
description: "Generates Elixir module source code for a skill using `use Jido.AI.Skill`.",
schema: [
module_name: [
type: :string,
required: true,
doc: "Full module name, e.g., 'MyApp.Skills.CodeReview'"
],
name: [type: :string, required: true, doc: "Skill name (lowercase-hyphenated)"],
description: [type: :string, required: true, doc: "Skill description (1-1024 chars)"],
license: [type: :string, required: false, doc: "License identifier, e.g., 'MIT'"],
allowed_tools: [type: {:list, :string}, required: false, doc: "List of allowed tool names"],
actions: [type: {:list, :string}, required: false, doc: "List of action module names"],
tags: [type: {:list, :string}, required: false, doc: "List of tags"],
body: [type: :string, required: true, doc: "Skill body content (markdown)"]
]
@impl true
def run(params, _context) do
module_name = params.module_name
name = params.name
description = params.description
license = Map.get(params, :license)
allowed_tools = Map.get(params, :allowed_tools, [])
actions = Map.get(params, :actions, [])
tags = Map.get(params, :tags, [])
body = params.body
code = generate_module_code(module_name, name, description, license, allowed_tools, actions, tags, body)
{:ok, %{format: "elixir_module", module_name: module_name, code: code}}
end
defp generate_module_code(module_name, name, description, license, allowed_tools, actions, tags, body) do
opts = [
~s(name: "#{name}"),
~s(description: "#{escape_string(description)}")
]
opts = if license, do: opts ++ [~s(license: "#{license}")], else: opts
opts = if allowed_tools != [], do: opts ++ [format_allowed_tools(allowed_tools)], else: opts
opts = if actions != [], do: opts ++ [format_actions(actions)], else: opts
opts = if tags != [], do: opts ++ [format_tags(tags)], else: opts
opts = opts ++ [format_body(body)]
opts_str = Enum.join(opts, ",\n ")
"""
defmodule #{module_name} do
@moduledoc \"\"\"
#{description}
\"\"\"
use Jido.AI.Skill,
#{opts_str}
end
"""
end
defp escape_string(str) do
str
|> String.replace("\\", "\\\\")
|> String.replace("\"", "\\\"")
|> String.replace("\n", " ")
end
defp format_allowed_tools(tools) do
tools_str = Enum.join(tools, " ")
"allowed_tools: ~w(#{tools_str})"
end
defp format_actions(actions) do
actions_str = Enum.map_join(actions, ",\n", &" #{&1}")
"actions: [\n#{actions_str}\n ]"
end
defp format_tags(tags) do
tags_str = Enum.map_join(tags, ", ", &~s("#{&1}"))
"tags: [#{tags_str}]"
end
defp format_body(body) do
indented_body = String.replace(body, "\n", "\n ")
~s|body: """\n #{indented_body}\n """|
end
end
defmodule Jido.AI.Examples.Tools.WriteFileSkill do
@moduledoc "Generates a SKILL.md file with YAML frontmatter."
use Jido.Action,
name: "write_file_skill",
description: "Generates a SKILL.md file content with YAML frontmatter and markdown body.",
schema: [
name: [type: :string, required: true, doc: "Skill name (lowercase-hyphenated)"],
description: [type: :string, required: true, doc: "Skill description (1-1024 chars)"],
license: [type: :string, required: false, doc: "License identifier, e.g., 'MIT'"],
allowed_tools: [type: {:list, :string}, required: false, doc: "List of allowed tool names"],
tags: [type: {:list, :string}, required: false, doc: "List of tags"],
metadata: [type: :map, required: false, doc: "Additional metadata as key-value pairs"],
body: [type: :string, required: true, doc: "Skill body content (markdown)"]
]
@impl true
def run(params, _context) do
name = params.name
description = params.description
license = Map.get(params, :license)
allowed_tools = Map.get(params, :allowed_tools, [])
tags = Map.get(params, :tags, [])
metadata = Map.get(params, :metadata, %{})
body = params.body
content = generate_skill_md(name, description, license, allowed_tools, tags, metadata, body)
suggested_path = "priv/skills/#{name}/SKILL.md"
{:ok, %{format: "skill_md", suggested_path: suggested_path, content: content}}
end
defp generate_skill_md(name, description, license, allowed_tools, tags, metadata, body) do
frontmatter_lines = [
"---",
"name: #{name}",
"description: #{description}"
]
frontmatter_lines =
if license do
frontmatter_lines ++ ["license: #{license}"]
else
frontmatter_lines
end
frontmatter_lines =
if allowed_tools != [] do
frontmatter_lines ++ ["allowed-tools: #{Enum.join(allowed_tools, " ")}"]
else
frontmatter_lines
end
frontmatter_lines =
if tags != [] do
tag_lines = Enum.map(tags, &" - #{&1}")
frontmatter_lines ++ ["tags:"] ++ tag_lines
else
frontmatter_lines
end
frontmatter_lines =
if metadata != %{} do
metadata_lines =
Enum.flat_map(metadata, fn {k, v} ->
[" #{k}: #{inspect(v)}"]
end)
frontmatter_lines ++ ["metadata:"] ++ metadata_lines
else
frontmatter_lines
end
frontmatter_lines = frontmatter_lines ++ ["---", ""]
Enum.join(frontmatter_lines, "\n") <> "\n" <> body
end
end