Packages
fnord
0.8.96
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/ai/agent/nomenclater.ex
defmodule AI.Agent.Nomenclater do
@max_attempts 3
@model AI.Model.fast()
@themes [
~s/Anything geeky or nerdy/,
~s/The Electronic Ghost of <well-known deceased person in software>/,
~s/Widgets from `Zork!`/,
~s/The BofH (or anything from the Jargon File!)/,
~s/Cheesy SF names that sound like they came from a 1950s pulp magazine/,
~s/Characters from SF novels (especially James Schmitz and his contemporaries)/,
~s/Klingons, especially with dramatic epithets/,
~s/Futurama characters, especially aliens and robots (e.g. "Lrrr, ruler of the Planet Omicron Persei 8!" (note that exclamation point is significant), "Bender", "Robot Devil"/,
~s/NPC, "Labcoat #3", and other "unnamed cast" names (e.g. "NPC 1", "Villager", "Guard 3")/,
~s/Dream creatures and Nightmares from the Dreaming in Sandman (e.g. "The Corinthian", "Fiddler's Green", "Merv Pumpkinhead")/,
~s/My Little Pony names (the new series, _of course_)/,
~s/Wizards and witches from Discworld (especially from the Unseen University's faculty)/,
~s/Wile E. Coyote, Programming Genius/,
~s/D&D characters (e.g. "Sylvaris Strongbow", "Bramdir Ironvein", "Garrick Brightblade")/,
~s/Pulp detective novel characters, but with software-related names (e.g. "Sam "the Compiler" Spade", "Deb "-bugger" Malloy")/,
~s/Hackerspeak names (e.g. "Acid Burn", "Crash Override", "Cereal Killer"/,
~s/Puns about AI-powered research and coding that seem like names (e.g. "R. E. Search", "Otto Mated", "Matt Rix", "Dr. D. Co'deure")/,
~s/Buffy the Vampire Slayer characters (e.g. "Xander Harris", "Willow Rosenberg", "Rupert Giles"; or soop them up for coding like "Buffy Overflow")/,
~s/Spoofed names of famous AIs and robots in fiction/,
~s/Skynet and terminator model designations (e.g. "T-800", "T-1000", "T-X")/,
~s/Golden age sci-fi names; generally a combination of "generic captain of the football team name" + "spacey|quantumy adjective" (e.g. "Chet Electron", "Buck Spacefarer", "Johnny Quantum")/
]
@prompt """
You are an AI agent within a larger system.
Your only task is to provide a first name for other AI agents so that their actions can be clearly distinguished in the logs.
Select a first name that is unique and not already used by another agent.
Selecting fun, whimsical names is encouraged, so long as they are not vulgar or offensive.
Each name should be one of: <first_name last_name>; <first_name the [epithet]>; or <first_name of the [phrase]>.
Epithets should be related to the theme or an application of the jargon file (eg "K'tah the Yak Shaver", "Alric of the Seven Bogons", "Xygon the Hacksaw").
Try not to select names that are too similar to existing names, as that can cause confusion.
Your audience is geeky, so sci-fi- and cartoon-sounding names are welcome, but obviously not required.
Resist the urge to give *everyone* a name starting with Z. Maybe just a couple per batch :)
Fun name themes:
{THEMES}
Try to spread names across multiple themes. Avoid clustering.
"""
@response_format %{
type: "json_schema",
json_schema: %{
name: "agent_names",
description: """
A JSON object containing an array of first names for AI agents.
Each name should be a single word or simple phrase.
""",
schema: %{
type: "object",
required: ["names"],
properties: %{
names: %{
type: "array",
items: %{
type: "string",
pattern: "^[A-Za-z0-9][A-Za-z0-9\\s'’\\-.,!/:()]*$",
minLength: 1,
maxLength: 64,
description:
"A first name for an AI agent, consisting of word characters and spaces only"
},
minItems: 1,
maxItems: 50,
description: "Array of unique first names for AI agents"
}
},
additionalProperties: false
}
}
}
# -----------------------------------------------------------------------------
# Behaviour implementation
# -----------------------------------------------------------------------------
@behaviour AI.Agent
@impl AI.Agent
def get_response(opts) do
opts
|> new()
|> get_name_batch()
|> case do
%{error: nil, names: names} -> {:ok, names}
%{error: error} -> {:error, error}
end
end
# ----------------------------------------------------------------------------
# Internal state
# ----------------------------------------------------------------------------
defstruct [
:agent,
:want,
:used,
:error,
:attempt,
:names
]
@type t :: %__MODULE__{
agent: AI.Agent.t(),
want: pos_integer(),
used: [binary],
error: nil | binary,
attempt: non_neg_integer(),
names: nil | [binary]
}
defp new(opts) do
with {:ok, agent} <- Map.fetch(opts, :agent),
{:ok, count} <- Map.fetch(opts, :want),
{:ok, used} <- Map.fetch(opts, :used) do
%__MODULE__{
agent: agent,
want: count,
used: used,
attempt: 1
}
else
:error -> %__MODULE__{error: "Missing required options"}
end
end
defp get_name_batch(%{attempt: attempt} = state) when attempt > @max_attempts do
%{state | error: "Exceeded maximum attempts to generate names"}
end
defp get_name_batch(state) do
used =
state.used
|> Enum.map(&"- #{&1}")
|> Enum.join("\n")
themes =
@themes
|> Enum.shuffle()
|> Enum.take(3)
|> Enum.map(&"- #{&1}")
|> Enum.join("\n")
prompt =
@prompt
|> String.replace("{THEMES}", themes)
state.agent
|> AI.Agent.get_completion(
# This is the AI model to use for name generation and is called by
# Services.NamePool's genserver. If the completion were to set `named?:
# true` (the default), it can cause deadlock in the genserver, because
# AI.Completion will call the genserver to get a name, and if there is no
# name available, it will call this agent to generate the next batch.
named?: false,
model: @model,
response_format: @response_format,
messages: [
AI.Util.system_msg(prompt),
AI.Util.user_msg("""
These names are already in use:
#{used}
Please provide #{state.want} unique first names for AI agents.
Ensure all names in the list are different from each other and from the existing names.
""")
]
)
|> case do
{:ok, %{response: response}} ->
case parse_json(response) do
{:ok, decoded} ->
case decoded do
# Handle normal case
%{"names" => names} when is_list(names) ->
process_names(state, names)
# Handle nested case (sometimes AI returns nested structure)
%{"names" => %{"names" => names}} when is_list(names) ->
process_names(state, names)
# Same as above, but stupider
%{"names" => %{"values" => names}} when is_list(names) ->
process_names(state, names)
# Handle any other unexpected structure
decoded ->
UI.warn("Unexpected response format", inspect(decoded, pretty: true))
get_name_batch(%{state | attempt: state.attempt + 1})
end
{:error, _reason} ->
preview = Util.truncate(response, 30)
UI.debug("Failed to parse Nomenclater's response", preview)
get_name_batch(%{state | attempt: state.attempt + 1})
end
{:error, %{response: response}} ->
%{state | error: response}
{:error, _reason} ->
get_name_batch(%{state | attempt: state.attempt + 1})
end
end
# Helper function to process names from JSON response
defp process_names(state, names) do
# Filter out any names that already exist (content validation)
existing = MapSet.new(state.used)
unique_names =
names
|> Enum.reject(&MapSet.member?(existing, &1))
# Also ensure no duplicates within the batch itself
|> Enum.uniq()
# If we didn't get enough unique names, retry with updated used_names
if length(unique_names) < state.want do
used = state.used ++ unique_names
get_name_batch(%{state | used: used, attempt: state.attempt + 1})
else
# Take exactly what we need and return them
names = Enum.take(unique_names, state.want)
%{state | names: names}
end
end
# ----------------------------------------------------------------------------
# JSON parsing helpers
# ----------------------------------------------------------------------------
# Returns {:ok, map} or {:error, reason}
defp parse_json(response) do
response
|> String.trim()
|> strip_code_fences()
|> extract_json_object()
|> Jason.decode()
end
# Remove wrapping ``` or ```json fences
defp strip_code_fences(text) do
text
|> String.replace(~r/^```json\s*/i, "")
|> String.replace(~r/^```\s*/, "")
|> String.replace(~r/\s*```$/, "")
end
# Drop any prefix up to the first '{'
defp extract_json_object(text) do
case String.split(text, "{", parts: 2) do
[_, rest] -> "{" <> rest
_ -> text
end
end
end