Packages
fnord
0.5.3
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/store/prompt.ex
defmodule Store.Prompt do
@moduledoc """
The `ask` subcommand saves useful prompts to the store. Saved prompts are
accompanied by a title and a list of example questions for which the prompt
is appropriate.
When a prompt is saved, an embedding is generated from it to make the prompt
(and the questions to which it applies) searchable.
Whenever a prompt is modified or refined, the previous version is archived
to the prompt's store dir with a version number:
`<$STORE>/prompts/<$PROMPT_ID>./v<$VERSION>/`
File structure:
```
$HOME/
-> .fnord/
-> prompts/
-> <$PROMPT_ID>/
-> v1
-> title.md
-> prompt.md
-> questions.md
-> embeddings.json
-> v2
-> title.md
-> prompt.md
-> questions.md
-> embeddings.json
```
There are a number of initial prompts that are installed the first time the
prompt store is searched. These prompts are defined in `data/prompts.yaml`.
When a new version of fnord is installed, the next time the prompt store is
searched, the prompts will be updated to the latest versions. "Default"
prompts can be distinguished by using a title slug for its id, rather than a
UUID.
"""
defstruct [
:id,
:store_path
]
@store_dir "prompts"
# ----------------------------------------------------------------------------
# These are installed the first time the prompt store is searched.
# ----------------------------------------------------------------------------
@initial_strategies YamlElixir.read_from_file!("data/prompts.yaml")
@doc """
Create a new prompt with a random UUID.
"""
def new(), do: new(UUID.uuid4())
@doc """
Create a new prompt with the given ID. This is used to access an existing
prompt. If `id` is `nil`, acts as an alias for `new/0`.
"""
def new(nil), do: new(UUID.uuid4())
def new(id), do: %__MODULE__{id: id, store_path: build_store_dir(id)}
@doc """
Returns true if the prompt has been written to the store.
"""
def exists?(prompt) do
File.exists?(prompt.store_path)
end
@doc """
Returns the current version of the prompt. Prompts are automatically
versioned with incremental numbers for tracking.
"""
def version(prompt) do
get_current_version_number(prompt)
end
@doc """
Saves the prompt to the store. If the prompt already exists but thew `title`,
`prompt_text`, or `questions` have changed, it will be given an incremented
version number. If they have not changed, an error will be returned
(`{:error, {:prompt_exists, id}}`).
"""
def write(prompt, title, prompt_text, questions, indexer \\ Indexer) do
qstr = format_questions(questions)
Store.list_prompts()
|> Enum.find(fn p ->
with {:ok, version} <- get_current_version_number(p),
{:ok, old_title} when old_title == title <- read_title(p, version),
{:ok, old_prompt} when old_prompt == prompt_text <- read_prompt(p, version),
{:ok, old_questions} when old_questions == qstr <- read_questions(p, version) do
true
else
_ -> false
end
end)
|> case do
nil -> do_write(prompt, title, prompt_text, questions, indexer)
p -> {:error, {:prompt_exists, p.id}}
end
end
defp do_write(prompt, title, prompt_text, questions, indexer) do
# --------------------------------------------------------------------------
# Determine the versioned path for the new prompt.
# --------------------------------------------------------------------------
prompt_path = get_next_version_path(prompt)
# --------------------------------------------------------------------------
# Create the prompt's store path if it does not yet exist.
# --------------------------------------------------------------------------
File.mkdir_p!(prompt_path)
# --------------------------------------------------------------------------
# Write the title to the prompt's store path.
# --------------------------------------------------------------------------
prompt_path
|> Path.join("title.md")
|> File.write!(title)
# --------------------------------------------------------------------------
# Write the prompt's text to the prompt's store path.
# --------------------------------------------------------------------------
prompt_path
|> Path.join("prompt.md")
|> File.write!(prompt_text)
# --------------------------------------------------------------------------
# Write the prompt's questions to the prompt's store path.
# --------------------------------------------------------------------------
questions = format_questions(questions)
prompt_path
|> Path.join("questions.md")
|> File.write!(questions)
# --------------------------------------------------------------------------
# Generate and save embeddings for the prompt.
# --------------------------------------------------------------------------
embeddings_text = [title, prompt_text, questions] |> Enum.join("\n")
with idx <- indexer.new(),
{:ok, embeddings} <- indexer.get_embeddings(idx, embeddings_text),
{:ok, json} <- Jason.encode(embeddings),
:ok <- prompt_path |> Path.join("embeddings.json") |> File.write(json) do
{:ok, prompt}
end
end
@doc """
Reads the prompt from the store. If the prompt does not exist, an error will
be returned (`{:error, :not_found}`). Returns an `:ok` tuple with the prompt's
title, prompt text, questions, and embeddings:
```
{:ok,
%{
title: title,
prompt: prompt_text,
questions: questions,
embeddings: embeddings,
version: version
}}
```
"""
def read(prompt) do
with {:ok, version} <- get_current_version_number(prompt) do
read(prompt, version)
end
end
@doc """
Reads the prompt from the store at the given version. Returns the same
information as `read/1`.
"""
def read(prompt, version) do
with {:ok, title} <- read_title(prompt, version),
{:ok, prompt_text} <- read_prompt(prompt, version),
{:ok, questions} <- read_questions(prompt, version),
{:ok, embeddings} <- read_embeddings(prompt, version) do
{:ok,
%{
title: title,
prompt: prompt_text,
questions: questions,
embeddings: embeddings,
version: version
}}
end
end
def read_title(prompt) do
with {:ok, version} <- get_current_version_number(prompt) do
read_title(prompt, version)
end
end
def read_title(prompt, version) do
prompt.store_path
|> Path.join("v#{version}")
|> Path.join("title.md")
|> File.read()
end
def read_prompt(prompt) do
with {:ok, version} <- get_current_version_number(prompt) do
read_prompt(prompt, version)
end
end
def read_prompt(prompt, version) do
prompt.store_path
|> Path.join("v#{version}")
|> Path.join("prompt.md")
|> File.read()
end
def read_questions(prompt) do
with {:ok, version} <- get_current_version_number(prompt) do
read_questions(prompt, version)
end
end
def read_questions(prompt, version) do
prompt.store_path
|> Path.join("v#{version}")
|> Path.join("questions.md")
|> File.read()
end
def read_embeddings(prompt) do
with {:ok, version} <- get_current_version_number(prompt) do
read_embeddings(prompt, version)
end
end
def read_embeddings(prompt, version) do
prompt.store_path
|> Path.join("v#{version}")
|> Path.join("embeddings.json")
|> File.read()
|> case do
{:ok, json} -> Jason.decode(json)
error -> error
end
end
def list_versions(prompt) do
prompt.store_path
|> File.ls()
|> case do
{:ok, dirs} -> Enum.sort(dirs)
_ -> []
end
end
# ----------------------------------------------------------------------------
# Private functions
# ----------------------------------------------------------------------------
defp format_questions(questions) do
questions
|> Enum.map(&"- #{&1}")
|> Enum.join("\n")
end
defp get_current_version_path(prompt) do
prompt
|> list_versions()
|> List.last()
|> case do
nil -> {:error, :not_found}
path -> {:ok, path}
end
end
defp get_current_version_number(prompt) do
prompt
|> get_current_version_path()
|> case do
{:ok, path} ->
path
|> Path.basename()
|> String.replace("v", "")
|> String.to_integer()
|> then(&{:ok, &1})
{:error, :not_found} ->
{:error, :not_found}
end
end
defp get_next_version_number(prompt) do
prompt
|> get_current_version_number()
|> case do
{:ok, version} -> version + 1
{:error, :not_found} -> 0
end
end
defp get_next_version_path(prompt) do
version = get_next_version_number(prompt)
Path.join(prompt.store_path, "v#{version}")
end
defp build_store_dir(id) do
Store.store_home()
|> Path.join(@store_dir)
|> Path.join(id)
end
# ----------------------------------------------------------------------------
# Initial strategies
# ----------------------------------------------------------------------------
def install_initial_strategies() do
@initial_strategies
|> Enum.each(fn %{
"id" => id,
"title" => title,
"prompt" => prompt_str,
"questions" => questions
} ->
prompt = new(id)
# I have NO idea why, but THIS line HAS to be here, otherwise the
# differs? conditional below won't be executed. This is incredibly
# confusing an frustrating, but apparently I've got to leave it in until
# if figure this BS out.
# UI.debug("Analyzing research prompt: #{id}")
if differs?(prompt, prompt_str, questions) do
UI.info("Saving research prompt: #{title}")
write(prompt, title, prompt_str, questions)
end
end)
end
defp differs?(prompt, prompt_str, questions) do
if exists?(prompt) do
with {:ok, version} <- get_current_version_number(prompt),
{:ok, old_questions} <- read_questions(prompt, version),
{:ok, old_prompt} <- read_prompt(prompt, version) do
cond do
old_questions != format_questions(questions) -> true
old_prompt != prompt_str -> true
true -> false
end
else
_ -> true
end
else
true
end
end
end