Packages
fnord
0.8.20
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/project/conversation.ex.bak.5
defmodule Store.Project.Conversation do
@moduledoc """
Conversations are stored per project in the project's store dir, under
`converations/`. Each conversation is given a UUID identifier and stored as a
JSON file with the keys:
- `messages`: a list of messages in the conversation
- `timestamp`: the time the conversation was last written to
Existing conversations are retrieved by their UUID identifier.
"""
defstruct [
:project_home,
:store_path,
:id,
:last_summarized_index
]
@type t :: %__MODULE__{
project_home: binary(),
store_path: binary(),
id: binary(),
last_summarized_index: non_neg_integer() | nil
}
@store_dir "conversations"
@doc """
Lists all conversations in the given project in ascending order by timestamp.
"""
@spec list(binary) :: [t]
def list(project_home) do
project_home
|> Path.join(["conversations/*.json"])
|> Path.wildcard()
|> Enum.map(&Path.basename(&1, ".json"))
|> Enum.map(&Store.Project.Conversation.new(&1, project_home))
|> Enum.sort_by(
fn conv ->
case Store.Project.Conversation.timestamp(conv) do
%DateTime{} = dt -> DateTime.to_unix(dt)
int when is_integer(int) -> int
end
end,
:asc
)
end
@doc """
Create a new conversation with a new UUID identifier and the globally
selected project.
"""
def new do
with {:ok, project} <- Store.get_project() do
new(Uniq.UUID.uuid4(), project.store_path)
else
{:error, _} -> raise "No project selected"
end
end
@doc """
Create a new conversation from an existing UUID identifier and the globally
selected project.
"""
def new(id) do
with {:ok, project} <- Store.get_project() do
new(id, project.store_path)
else
{:error, _} -> raise "No project selected"
end
end
@doc """
Create a new conversation from an existing UUID identifier and an explicitly
specified project.
"""
def new(id, project_home) when is_binary(project_home) do
%__MODULE__{
project_home: project_home,
store_path: build_store_path(project_home, id),
id: id
}
end
def new(id, %Store.Project{store_path: store_path}) do
new(id, store_path)
end
@doc """
Returns true if the conversation exists on disk, false otherwise.
"""
@spec exists?(t) :: boolean()
def exists?(%__MODULE__{store_path: store_path}) do
File.exists?(store_path)
end
@doc """
Saves the conversation in the store. The conversation's timestamp is updated
to the current time.
"""
@spec write(t, list, list, integer() | nil) :: {:ok, t} | {:error, any()}
def write(conversation, messages, summaries \\ [], last_summarized_index \\ nil) do
conversation.project_home
|> build_store_dir()
|> File.mkdir_p()
timestamp =
DateTime.utc_now()
|> DateTime.to_unix()
effective_index =
if last_summarized_index != nil do
last_summarized_index
else
case conversation.last_summarized_index do
nil -> -1
idx -> idx
end
end
data = %{messages: messages, summaries: summaries, last_summarized_index: effective_index}
with {:ok, json} <- Jason.encode(data),
:ok <- File.write(conversation.store_path, "#{timestamp}:#{json}") do
{:ok, %{conversation | last_summarized_index: effective_index}}
end
end
@doc """
Reads the conversation from the store. Returns a tuple with the timestamp, messages, and summaries in the conversation.
"""
@spec read(t) :: {:ok, DateTime.t(), list, list} | {:error, any()}
def read(conversation) do
with {:ok, contents} <- File.read(conversation.store_path),
[timestamp_str, json] <- String.split(contents, ":", parts: 2),
{:ok, timestamp} <- timestamp_str |> String.to_integer() |> DateTime.from_unix(),
{:ok, decoded} <- Jason.decode(json) do
messages = decoded["messages"] |> Util.string_keys_to_atoms()
summaries = Map.get(decoded, "summaries", [])
{:ok, timestamp, messages, summaries}
end
end
@doc """
Returns the timestamp of the conversation. If the conversation has not yet
been saved to the store, returns 0.
"""
@spec timestamp(t) :: DateTime.t() | 0
def timestamp(conversation) do
if exists?(conversation) do
with {:ok, contents} <- File.read(conversation.store_path),
[timestamp, _] <- String.split(contents, ":", parts: 2),
{:ok, timestamp} <- timestamp |> String.to_integer() |> DateTime.from_unix() do
timestamp
else
_ -> 0
end
else
0
end
end
@doc """
Returns the user's prompting message in the conversation. This is considered
to be the first "user" role message in the conversation.
"""
@spec question(t) :: {:ok, binary} | {:error, :no_question}
def question(conversation) do
with {:ok, _, msgs, _} <- read(conversation) do
msgs
|> Enum.find(&(Map.get(&1, :role) == "user"))
|> case do
nil -> {:error, :no_question}
msg -> Map.fetch(msg, :content)
end
end
end
@doc """
Deletes the conversation from the store. If the conversation does not exist,
returns an error tuple.
"""
@spec delete(t) :: :ok | {:error, :not_found}
def delete(conversation) do
if exists?(conversation) do
File.rm!(conversation.store_path)
:ok
else
{:error, :not_found}
end
end
@doc """
Summarizes the unsummarized messages (those after the last_summarized_index),
appends the new summary to existing summaries, updates last_summarized_index,
and persists the update without clearing messages.
"""
@spec summarize_and_append(t) :: {:ok, t} | {:error, any()}
def summarize_and_append(conversation) do
with {:ok, _timestamp, messages, summaries} <- Store.Project.Conversation.read(conversation) do
last_index = conversation.last_summarized_index || -1
unsummarized_messages = Enum.drop(messages, last_index + 1)
# Return unchanged if there are no new messages to summarize
if unsummarized_messages == [] do
{:ok, conversation}
else
previous_summary = Enum.join(summaries, "\n\n")
with {:ok, new_summary} <- AI.Agent.SagaScribe.get_response(%{
messages: unsummarized_messages,
previous_summary: previous_summary
}),
{:ok, updated_conversation} <-
write(conversation, messages, summaries ++ [new_summary], last_index + length(unsummarized_messages)) do
{:ok, updated_conversation}
end
end
else
{:error, reason} -> {:error, reason}
end
end
# Private functions
# -----------------------------------------------------------------------------
@spec build_store_dir(binary) :: binary
defp build_store_dir(project_home) do
Path.join([project_home, @store_dir])
end
@spec build_store_path(binary, binary) :: binary
defp build_store_path(project_home, id) do
file = build_store_dir(project_home) |> Path.join(id)
file <> ".json"
end
end