lib/markov/prompt.ex

defmodule Markov.Prompt do
  @moduledoc """
  Thin wrapper around `Markov` to make the chain respond to prompts assuming
  it's been trained on the appropriate data
  """

  defp map_token({token, index}, {lower_thres, upper_thres}) do
    token = Markov.TextUtil.sanitize_token(token)
    type_to_score = %{noun: 50, verb: 30, adj: 25, adv: 10, prep: 5}
    result = :ets.lookup(Markov.Dictionary, token)

    score = case result do
      [] -> 1
      [{_, type}] -> Map.get(type_to_score, type)
    end

    [
      {token, score},
      case index do
        i when i <= lower_thres -> {{token, :start}, 2}
        i when i >= upper_thres -> {{token, :end}, 2}
        _ -> {{token, :middle}, 2}
      end
    ]
  end

  defp generate_tags(text) do
    tokens = String.split(text)
    max = length(tokens) - 1
    range = 0..max

    thres = {floor(0.2 * max), ceil(0.8 * max)}

    Enum.zip(tokens, range)
      |> Enum.flat_map(fn item -> map_token(item, thres) end)
  end

  @doc """
  Assuming your application receives a stream of strings, call this function
  instead of `Markov.train/3` with the current and last string
  """
  @spec train(model :: Markov.model_reference(), new_text :: String.t(),
    last_text :: String.t() | nil, tags :: [any()])
    :: {:ok, :done | :deferred} | {:error, term()}
  def train(model, new_text, last_text \\ nil, tags \\ []) do
    tags = if last_text do
      generate_tags(last_text) |> Enum.map(fn {token, _score} -> token end)
    else [] end ++ tags

    Markov.train(model, new_text, tags)
  end

  @doc """
  Trains the model on a list of consecutive strings
  """
  @spec train_on_list(model :: Markov.model_reference(), list :: [String.t()]) :: :ok
  def train_on_list(model, list) do
    case list do
      [first | _] ->
        train(model, first)

        _ = Enum.reduce(list, fn string, last_string ->
          train(model, string, last_string)
          string # new "last string"
        end)
        :ok

      _ -> :ok
    end
  end

  @doc """
  Generates the text from a prompt
  """
  @spec generate_prompted(model :: Markov.model_reference(), prompt :: String.t(),
    query :: Markov.tag_query()) :: {:ok, String.t()} | {:error, term()}
  def generate_prompted(model, prompt, tag_query \\ true) do
    tags = generate_tags(prompt)
    Markov.generate_text(model, {tag_query, :score, tags})
  end
end