summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorhref <href@random.sh>2022-12-03 02:00:37 +0000
committerJordan Bracco <href@random.sh>2022-12-11 02:03:36 +0000
commit251ea43c1308eb96e4ada16edf6481a8be1fa765 (patch)
tree2148e304574c684f7eb1e74607634a907df9085d /lib
parentnew plugin: radio france (diff)
new plugin: openai gpt
Diffstat (limited to 'lib')
-rw-r--r--lib/couch.ex18
-rw-r--r--lib/lsg_irc/gpt_plugin.ex103
-rw-r--r--lib/open_ai.ex17
3 files changed, 138 insertions, 0 deletions
diff --git a/lib/couch.ex b/lib/couch.ex
new file mode 100644
index 0000000..fdd8579
--- /dev/null
+++ b/lib/couch.ex
@@ -0,0 +1,18 @@
+defmodule Couch do
+ def get(db, doc) do
+ config = Application.get_env(:lsg, :couch)
+ url = [Keyword.get(config, :url), db, doc] |> Enum.join("/")
+ user = Keyword.get(config, :user)
+ pass = Keyword.get(config, :pass)
+ client_options = Keyword.get(config, :client_options, [])
+ headers = [{"accept", "application/json"}, {"user-agent", "beautte"}]
+ options = [hackney: [:insecure, {:basic_auth, {user, pass}}]] ++ client_options
+ case HTTPoison.get(url, headers, options) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ {:ok, Poison.decode!(body)}
+ {:ok, %HTTPoison.Response{status_code: 404}} ->
+ {:error, :not_found}
+ error -> {:error, {:couchdb_error, error}}
+ end
+ end
+end
diff --git a/lib/lsg_irc/gpt_plugin.ex b/lib/lsg_irc/gpt_plugin.ex
new file mode 100644
index 0000000..f628f8d
--- /dev/null
+++ b/lib/lsg_irc/gpt_plugin.ex
@@ -0,0 +1,103 @@
+defmodule LSG.IRC.GptPlugin do
+ require Logger
+
+ def irc_doc() do
+ """
+ # OpenAI GPT
+
+ * **!gpt** list GPT tasks
+ * **!gpt `[task]` `<task args>`** run a task
+ * **?offensive `<content>`** is content offensive
+ """
+ end
+
+ @couch_db "bot-plugin-openai-prompts"
+ @trigger "gpt"
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
+ end
+
+ def init(_) do
+ regopts = [plugin: __MODULE__]
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:#{@trigger}", regopts)
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:offensive", regopts)
+ {:ok, nil}
+ end
+
+ def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [task | args]}}}, state) do
+ case Couch.get(@couch_db, task) do
+ {:ok, task} -> task(m, task, Enum.join(args, " "))
+ {:error, :not_found} -> m.replyfun.("gpt: no such task: #{task}")
+ error ->
+ Logger.info("gpt: task load error: #{inspect error}")
+ m.replyfun.("gpt: database error")
+ end
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, @trigger, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: []}}}, state) do
+ case Couch.get(@couch_db, "_all_docs") do
+ {:ok, %{"rows" => []}} -> m.replyfun.("gpt: no tasks available")
+ {:ok, %{"rows" => tasks}} ->
+ tasks = tasks |> Enum.map(fn(task) -> Map.get(task, "id") end) |> Enum.join(", ")
+ m.replyfun.("gpt: tasks: #{tasks}")
+ error ->
+ Logger.info("gpt: task load error: #{inspect error}")
+ m.replyfun.("gpt: database error")
+ end
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "offensive", m = %IRC.Message{trigger: %IRC.Trigger{type: :query, args: 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(_, state) do
+ {:noreply, state}
+ end
+
+ defp task(msg, task = %{"type" => "completions", "prompt" => prompt}, content) do
+ prompt = Tmpl.render(prompt, msg, %{"content" => content})
+ args = Map.get(task, "openai_params")
+ |> Map.put("prompt", prompt)
+ |> 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} | _]}} ->
+ {moderate?, moderation} = moderation(text, msg.account.id)
+ if moderate?, do: msg.replyfun.("🚨 offensive output: #{Enum.join(moderation, ", ")}")
+ msg.replyfun.(String.trim(text))
+ error ->
+ Logger.error("gpt error: #{inspect error}")
+ msg.replyfun.("gpt: ☠️ ")
+ 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
diff --git a/lib/open_ai.ex b/lib/open_ai.ex
new file mode 100644
index 0000000..9feb9a4
--- /dev/null
+++ b/lib/open_ai.ex
@@ -0,0 +1,17 @@
+defmodule OpenAi do
+
+ def post(path, data, options \\ []) do
+ config = Application.get_env(:lsg, :openai, [])
+ url = "https://api.openai.com#{path}"
+ headers = [{"user-agent", "internal private experiment bot, href@random.sh"},
+ {"content-type", "application/json"},
+ {"authorization", "Bearer " <> Keyword.get(config, :key, "unset-api-key")}]
+ options = options ++ [timeout: :timer.seconds(180), recv_timeout: :timer.seconds(180)]
+ with {:ok, json} <- Poison.encode(data),
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.post(url, json, headers, options),
+ {:ok, data} <- Poison.decode(body) do
+ {:ok, data}
+ end
+ end
+
+end