diff options
Diffstat (limited to 'lib/plugins/gpt.ex')
-rw-r--r-- | lib/plugins/gpt.ex | 355 |
1 files changed, 0 insertions, 355 deletions
diff --git a/lib/plugins/gpt.ex b/lib/plugins/gpt.ex deleted file mode 100644 index 8ee8c10..0000000 --- a/lib/plugins/gpt.ex +++ /dev/null @@ -1,355 +0,0 @@ -defmodule Nola.Plugins.Gpt do - require Logger - import Nola.Plugins.TempRefHelper - - def irc_doc() do - """ - # OpenAI GPT - - Uses OpenAI's GPT-3 API to bring natural language prompts to your IRC channel. - - _prompts_ are pre-defined prompts and parameters defined in the bot' CouchDB. - - _Runs_ (results of the inference of a _prompt_) are also stored in CouchDB and - may be resumed. - - * **!gpt** list GPT prompts - * **!gpt `[prompt]` `<prompt or args>`** run a prompt - * **+gpt `[short ref|run id]` `<prompt or args>`** continue a prompt - * **?gpt offensive `<content>`** is content offensive ? - * **?gpt show `[short ref|run id]`** run information and web link - * **?gpt `[prompt]`** prompt information and web link - """ - end - - @couch_db "bot-plugin-openai-prompts" - @couch_run_db "bot-plugin-gpt-history" - @trigger "gpt" - - def start_link() do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - defstruct [:temprefs] - - def get_result(id) do - Couch.get(@couch_run_db, id) - end - - def get_prompt(id) do - Couch.get(@couch_db, id) - end - - def init(_) do - regopts = [plugin: __MODULE__] - {:ok, _} = Registry.register(Nola.PubSub, "trigger:#{@trigger}", regopts) - {:ok, %__MODULE__{temprefs: new_temp_refs()}} - end - - def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: [prompt | args]}}}, state) do - case Couch.get(@couch_db, prompt) do - {:ok, prompt} -> {:noreply, prompt(m, prompt, Enum.join(args, " "), state)} - {:error, :not_found} -> - m.replyfun.("gpt: prompt '#{prompt}' does not exists") - {:noreply, state} - error -> - Logger.info("gpt: prompt load error: #{inspect error}") - m.replyfun.("gpt: database error") - {:noreply, state} - end - end - - def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :bang, args: []}}}, state) do - case Couch.get(@couch_db, "_all_docs") do - {:ok, %{"rows" => []}} -> m.replyfun.("gpt: no prompts available") - {:ok, %{"rows" => prompts}} -> - prompts = prompts |> Enum.map(fn(prompt) -> Map.get(prompt, "id") end) |> Enum.join(", ") - m.replyfun.("gpt: prompts: #{prompts}") - error -> - Logger.info("gpt: prompt load error: #{inspect error}") - m.replyfun.("gpt: database error") - end - {:noreply, state} - end - - def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :plus, args: [ref_or_id | args]}}}, state) do - id = lookup_temp_ref(ref_or_id, state.temprefs, ref_or_id) - case Couch.get(@couch_run_db, id) do - {:ok, run} -> - Logger.debug("+gpt run: #{inspect run}") - {:noreply, continue_prompt(m, run, Enum.join(args, " "), state)} - {:error, :not_found} -> - m.replyfun.("gpt: ref or id not found or expired: #{inspect ref_or_id} (if using short ref, try using full id)") - {:noreply, state} - error -> - Logger.info("+gpt: run load error: #{inspect error}") - m.replyfun.("gpt: database error") - {:noreply, state} - end - end - - def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :query, args: ["offensive" | text]}}}, state) do - text = Enum.join(text, " ") - {moderate?, moderation} = moderation(text, m.account.id) - reply = cond do - moderate? -> "⚠️ #{Enum.join(moderation, ", ")}" - !moderate? && moderation -> "👍" - !moderate? -> "☠️ error" - end - m.replyfun.(reply) - {:noreply, state} - end - - def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :query, args: ["show", ref_or_id]}}}, state) do - id = lookup_temp_ref(ref_or_id, state.temprefs, ref_or_id) - url = if m.channel do - NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :result, m.network, NolaWeb.format_chan(m.channel), id) - else - NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :result, id) - end - m.replyfun.("→ #{url}") - {:noreply, state} - end - - def handle_info({:irc, :trigger, @trigger, m = %Nola.Message{trigger: %Nola.Trigger{type: :query, args: [prompt]}}}, state) do - url = if m.channel do - NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :prompt, m.network, NolaWeb.format_chan(m.channel), prompt) - else - NolaWeb.Router.Helpers.gpt_url(NolaWeb.Endpoint, :prompt, prompt) - end - m.replyfun.("→ #{url}") - {:noreply, state} - end - - def handle_info(info, state) do - Logger.debug("gpt: unhandled info: #{inspect info}") - {:noreply, state} - end - - defp continue_prompt(msg, run, content, state) do - prompt_id = Map.get(run, "prompt_id") - prompt_rev = Map.get(run, "prompt_rev") - - original_prompt = case Couch.get(@couch_db, prompt_id, rev: prompt_rev) do - {:ok, prompt} -> prompt - _ -> nil - end - - if original_prompt do - continue_prompt = %{"_id" => prompt_id, - "_rev" => prompt_rev, - "type" => Map.get(original_prompt, "type"), - "parent_run_id" => Map.get(run, "_id"), - "openai_params" => Map.get(run, "request") |> Map.delete("prompt")} - - continue_prompt = case original_prompt do - %{"continue_prompt" => prompt_string} when is_binary(prompt_string) -> - full_text = get_in(run, ~w(request prompt)) <> "\n" <> Map.get(run, "response") - continue_prompt - |> Map.put("prompt", prompt_string) - |> Map.put("prompt_format", "liquid") - |> Map.put("prompt_liquid_variables", %{"previous" => full_text}) - %{"messages" => _} -> - continue_prompt - |> Map.put("prompt", "{{content}}") - |> Map.put("prompt_format", "liquid") - |> Map.put("messages", Map.get(run, "messages")) - _ -> - prompt_content_tag = if content != "", do: " {{content}}", else: "" - string = get_in(run, ~w(request prompt)) <> "\n" <> Map.get(run, "response") <> prompt_content_tag - continue_prompt - |> Map.put("prompt", string) - |> Map.put("prompt_format", "liquid") - end - - prompt(msg, continue_prompt, content, state) - else - msg.replyfun.("gpt: cannot continue this prompt: original prompt not found #{prompt_id}@v#{prompt_rev}") - state - end - end - - # Chat prompt - # "prompt" is the template for the initial user message - # "messages" is original messages to be put before the initial user one - defp prompt(msg, prompt = %{"type" => "chat", "prompt" => prompt_template, "messages" => messages}, content, state) do - Logger.debug("gpt_plugin:prompt/4 (chat) #{inspect prompt}") - prompt_text = case Map.get(prompt, "prompt_format", "liquid") do - "liquid" -> Tmpl.render(prompt_template, msg, Map.merge(Map.get(prompt, "prompt_liquid_variables", %{}), %{"content" => content})) - "norender" -> prompt_template - end - - messages = Enum.map(messages, fn(%{"role" => role, "content" => text}) -> - text = case Map.get(prompt, "prompt_format", "liquid") do - "liquid" -> Tmpl.render(text, msg, Map.get(prompt, "prompt_liquid_variables", %{})) - "norender" -> text - end - %{"role" => role, "content" => text} - end) ++ [%{"role" => "user", "content" => prompt_text}] - - args = Map.get(prompt, "openai_params") - |> Map.put_new("model", "gpt-3.5-turbo") - |> Map.put("messages", messages) - |> Map.put("user", msg.account.id) - - {moderate?, moderation} = moderation(content, msg.account.id) - if moderate?, do: msg.replyfun.("⚠️ offensive input: #{Enum.join(moderation, ", ")}") - - Logger.debug("GPT: request #{inspect args}") - case OpenAi.post("/v1/chat/completions", args) do - {:ok, %{"choices" => [%{"message" => %{"content" => text}, "finish_reason" => finish_reason} | _], "usage" => usage, "id" => gpt_id, "created" => created}} -> - text = String.trim(text) - {o_moderate?, o_moderation} = moderation(text, msg.account.id) - if o_moderate?, do: msg.replyfun.("🚨 offensive output: #{Enum.join(o_moderation, ", ")}") - msg.replyfun.(text) - doc = %{"id" => FlakeId.get(), - "prompt_id" => Map.get(prompt, "_id"), - "prompt_rev" => Map.get(prompt, "_rev"), - "network" => msg.network, - "channel" => msg.channel, - "nick" => msg.sender.nick, - "account_id" => (if msg.account, do: msg.account.id), - "request" => args, - "messages" => messages ++ [%{"role" => "assistant", "content" => text}], - "message_at" => msg.at, - "reply_at" => DateTime.utc_now(), - "gpt_id" => gpt_id, - "gpt_at" => created, - "gpt_usage" => usage, - "type" => "chat", - "parent_run_id" => Map.get(prompt, "parent_run_id"), - "moderation" => %{"input" => %{flagged: moderate?, categories: moderation}, - "output" => %{flagged: o_moderate?, categories: o_moderation} - } - } - Logger.debug("Saving result to couch: #{inspect doc}") - {id, ref, temprefs} = case Couch.post(@couch_run_db, doc) do - {:ok, id, _rev} -> - {ref, temprefs} = put_temp_ref(id, state.temprefs) - {id, ref, temprefs} - error -> - Logger.error("Failed to save to Couch: #{inspect error}") - {nil, nil, state.temprefs} - end - stop = cond do - finish_reason == "stop" -> "" - finish_reason == "length" -> " — truncated" - true -> " — #{finish_reason}" - end - ref_and_prefix = if Map.get(usage, "completion_tokens", 0) == 0 do - "GPT had nothing else to say :( ↪ #{ref || "✗"}" - else - " ↪ #{ref || "✗"}" - end - msg.replyfun.(ref_and_prefix <> - stop <> - " — #{Map.get(usage, "total_tokens", 0)}" <> - " (#{Map.get(usage, "prompt_tokens", 0)}/#{Map.get(usage, "completion_tokens", 0)}) tokens" <> - " — #{id || "save failed"}") - %__MODULE__{state | temprefs: temprefs} - {:error, atom} when is_atom(atom) -> - Logger.error("gpt error: #{inspect atom}") - msg.replyfun.("gpt: ☠️ #{to_string(atom)}") - state - error -> - Logger.error("gpt error: #{inspect error}") - msg.replyfun.("gpt: ☠️ ") - state - end - end - - - defp prompt(msg, prompt = %{"type" => "completions", "prompt" => prompt_template}, content, state) do - Logger.debug("gpt:prompt/4 #{inspect prompt}") - prompt_text = case Map.get(prompt, "prompt_format", "liquid") do - "liquid" -> Tmpl.render(prompt_template, msg, Map.merge(Map.get(prompt, "prompt_liquid_variables", %{}), %{"content" => content})) - "norender" -> prompt_template - end - - args = Map.get(prompt, "openai_params") - |> Map.put("prompt", prompt_text) - |> Map.put("user", msg.account.id) - - {moderate?, moderation} = moderation(content, msg.account.id) - if moderate?, do: msg.replyfun.("⚠️ offensive input: #{Enum.join(moderation, ", ")}") - - Logger.debug("GPT: request #{inspect args}") - case OpenAi.post("/v1/completions", args) do - {:ok, %{"choices" => [%{"text" => text, "finish_reason" => finish_reason} | _], "usage" => usage, "id" => gpt_id, "created" => created}} -> - text = String.trim(text) - {o_moderate?, o_moderation} = moderation(text, msg.account.id) - if o_moderate?, do: msg.replyfun.("🚨 offensive output: #{Enum.join(o_moderation, ", ")}") - msg.replyfun.(text) - doc = %{"id" => FlakeId.get(), - "prompt_id" => Map.get(prompt, "_id"), - "prompt_rev" => Map.get(prompt, "_rev"), - "network" => msg.network, - "channel" => msg.channel, - "nick" => msg.sender.nick, - "account_id" => (if msg.account, do: msg.account.id), - "request" => args, - "response" => text, - "message_at" => msg.at, - "reply_at" => DateTime.utc_now(), - "gpt_id" => gpt_id, - "gpt_at" => created, - "gpt_usage" => usage, - "type" => "completions", - "parent_run_id" => Map.get(prompt, "parent_run_id"), - "moderation" => %{"input" => %{flagged: moderate?, categories: moderation}, - "output" => %{flagged: o_moderate?, categories: o_moderation} - } - } - Logger.debug("Saving result to couch: #{inspect doc}") - {id, ref, temprefs} = case Couch.post(@couch_run_db, doc) do - {:ok, id, _rev} -> - {ref, temprefs} = put_temp_ref(id, state.temprefs) - {id, ref, temprefs} - error -> - Logger.error("Failed to save to Couch: #{inspect error}") - {nil, nil, state.temprefs} - end - stop = cond do - finish_reason == "stop" -> "" - finish_reason == "length" -> " — truncated" - true -> " — #{finish_reason}" - end - ref_and_prefix = if Map.get(usage, "completion_tokens", 0) == 0 do - "GPT had nothing else to say :( ↪ #{ref || "✗"}" - else - " ↪ #{ref || "✗"}" - end - msg.replyfun.(ref_and_prefix <> - stop <> - " — #{Map.get(usage, "total_tokens", 0)}" <> - " (#{Map.get(usage, "prompt_tokens", 0)}/#{Map.get(usage, "completion_tokens", 0)}) tokens" <> - " — #{id || "save failed"}") - %__MODULE__{state | temprefs: temprefs} - {:error, atom} when is_atom(atom) -> - Logger.error("gpt error: #{inspect atom}") - msg.replyfun.("gpt: ☠️ #{to_string(atom)}") - state - error -> - Logger.error("gpt error: #{inspect error}") - msg.replyfun.("gpt: ☠️ ") - state - end - end - - defp moderation(content, user_id) do - case OpenAi.post("/v1/moderations", %{"input" => content, "user" => user_id}) do - {:ok, %{"results" => [%{"flagged" => true, "categories" => categories} | _]}} -> - cat = categories - |> Enum.filter(fn({_key, value}) -> value end) - |> Enum.map(fn({key, _}) -> key end) - {true, cat} - {:ok, moderation} -> - Logger.debug("gpt: moderation: not flagged, #{inspect moderation}") - {false, true} - error -> - Logger.error("gpt: moderation error: #{inspect error}") - {false, false} - end - end - -end |